Browse Source

merged in master

Anatoly Pulyaevskiy 5 years ago
parent
commit
b40428af97
39 changed files with 751 additions and 436 deletions
  1. 4
    0
      .gitbook.yaml
  2. 3
    13
      .travis.yml
  3. 82
    22
      doc/images.md
  4. 21
    0
      packages/zefyr/CHANGELOG.md
  5. 5
    0
      packages/zefyr/example/lib/src/form.dart
  6. 7
    18
      packages/zefyr/example/lib/src/full_page.dart
  7. 40
    0
      packages/zefyr/example/lib/src/images.dart
  8. 5
    17
      packages/zefyr/example/lib/src/view.dart
  9. 1
    0
      packages/zefyr/example/pubspec.yaml
  10. 12
    11
      packages/zefyr/lib/src/widgets/buttons.dart
  11. 2
    2
      packages/zefyr/lib/src/widgets/caret.dart
  12. 4
    4
      packages/zefyr/lib/src/widgets/code.dart
  13. 6
    6
      packages/zefyr/lib/src/widgets/common.dart
  14. 11
    5
      packages/zefyr/lib/src/widgets/controller.dart
  15. 2
    2
      packages/zefyr/lib/src/widgets/editable_box.dart
  16. 57
    27
      packages/zefyr/lib/src/widgets/editable_text.dart
  17. 44
    8
      packages/zefyr/lib/src/widgets/editor.dart
  18. 5
    4
      packages/zefyr/lib/src/widgets/field.dart
  19. 35
    37
      packages/zefyr/lib/src/widgets/image.dart
  20. 1
    1
      packages/zefyr/lib/src/widgets/input.dart
  21. 5
    5
      packages/zefyr/lib/src/widgets/list.dart
  22. 61
    0
      packages/zefyr/lib/src/widgets/mode.dart
  23. 3
    3
      packages/zefyr/lib/src/widgets/paragraph.dart
  24. 12
    12
      packages/zefyr/lib/src/widgets/rich_text.dart
  25. 3
    0
      packages/zefyr/lib/src/widgets/scaffold.dart
  26. 19
    7
      packages/zefyr/lib/src/widgets/scope.dart
  27. 134
    122
      packages/zefyr/lib/src/widgets/selection.dart
  28. 10
    10
      packages/zefyr/lib/src/widgets/toolbar.dart
  29. 3
    0
      packages/zefyr/lib/src/widgets/view.dart
  30. 3
    3
      packages/zefyr/lib/util.dart
  31. 1
    0
      packages/zefyr/lib/zefyr.dart
  32. 3
    3
      packages/zefyr/pubspec.yaml
  33. 41
    0
      packages/zefyr/test/rendering/render_zefyr_paragraph_test.dart
  34. 12
    4
      packages/zefyr/test/testing.dart
  35. 34
    43
      packages/zefyr/test/widgets/buttons_test.dart
  36. 1
    1
      packages/zefyr/test/widgets/editor_test.dart
  37. 38
    43
      packages/zefyr/test/widgets/image_test.dart
  38. 20
    2
      packages/zefyr/test/widgets/scope_test.dart
  39. 1
    1
      packages/zefyr/test/widgets/selection_test.dart

+ 4
- 0
.gitbook.yaml View File

1
+root: ./doc/
2
+
3
+structure:
4
+  readme: ./../README.md

+ 3
- 13
.travis.yml View File

14
       - ubuntu-toolchain-r-test
14
       - ubuntu-toolchain-r-test
15
     packages:
15
     packages:
16
       - libstdc++6
16
       - libstdc++6
17
-      - fonts-droid
17
+      - fonts-droid-fallback
18
 
18
 
19
 cache:
19
 cache:
20
   directories:
20
   directories:
21
     - $HOME/.pub-cache
21
     - $HOME/.pub-cache
22
 
22
 
23
-#env:
24
-#  - FLUTTER_VERSION=stable
25
-#  - FLUTTER_VERSION=master
26
-
27
-matrix:
28
-  include:
29
-    - name: "Master build"
30
-      if: branch = flutter-master
31
-      env: FLUTTER_VERSION=master
32
-    - name: "Stable build"
33
-      if: branch != flutter-master
34
-      env: FLUTTER_VERSION=stable
23
+env:
24
+  - FLUTTER_VERSION=stable
35
 
25
 
36
 before_script:
26
 before_script:
37
   - pwd
27
   - pwd

+ 82
- 22
doc/images.md View File

1
 ## Images
1
 ## Images
2
 
2
 
3
-> Note that described API is considered experimental and is likely to be
3
+> Note that Image API is considered experimental and is likely to be
4
 > changed in backward incompatible ways. If this happens all changes will be
4
 > changed in backward incompatible ways. If this happens all changes will be
5
 > described in detail in the changelog to simplify upgrading.
5
 > described in detail in the changelog to simplify upgrading.
6
 
6
 
7
-Zefyr (and Notus) supports embedded images. In order to handle images in
7
+Zefyr supports embedding images. In order to handle images in
8
 your application you need to implement `ZefyrImageDelegate` interface which
8
 your application you need to implement `ZefyrImageDelegate` interface which
9
 looks like this:
9
 looks like this:
10
 
10
 
11
 ```dart
11
 ```dart
12
 abstract class ZefyrImageDelegate<S> {
12
 abstract class ZefyrImageDelegate<S> {
13
-  /// Builds image widget for specified [imageSource] and [context].
14
-  Widget buildImage(BuildContext context, String imageSource);
13
+  /// Unique key to identify camera source.
14
+  S get cameraSource;
15
+
16
+  /// Unique key to identify gallery source.
17
+  S get gallerySource;
18
+
19
+  /// Builds image widget for specified image [key].
20
+  ///
21
+  /// The [key] argument contains value which was previously returned from
22
+  /// [pickImage].
23
+  Widget buildImage(BuildContext context, String key);
15
 
24
 
16
   /// Picks an image from specified [source].
25
   /// Picks an image from specified [source].
17
   ///
26
   ///
18
   /// Returns unique string key for the selected image. Returned key is stored
27
   /// Returns unique string key for the selected image. Returned key is stored
19
   /// in the document.
28
   /// in the document.
29
+  ///
30
+  /// Depending on your application returned key may represent a path to
31
+  /// an image file on user's device, an HTTP link, or an identifier generated
32
+  /// by a file hosting service like AWS S3 or Google Drive.
20
   Future<String> pickImage(S source);
33
   Future<String> pickImage(S source);
21
 }
34
 }
22
 ```
35
 ```
23
 
36
 
24
-Zefyr comes with default implementation which exists mostly to provide an
25
-example and a starting point for your own version.
37
+There is no default implementation of this interface since resolving image
38
+sources is always application-specific.
26
 
39
 
27
-It is recommended to always have your own implementation specific to your
28
-application.
40
+> Note that prior to 0.7.0 Zefyr did provide simple default implementation of
41
+> `ZefyrImageDelegate` however it was removed as it introduced unnecessary
42
+> dependency on `image_picker` plugin.
29
 
43
 
30
 ### Implementing ZefyrImageDelegate
44
 ### Implementing ZefyrImageDelegate
31
 
45
 
46
+For this example we will use [image_picker](https://pub.dev/packages/image_picker)
47
+plugin which allows us to select images from device's camera or photo gallery.
48
+
32
 Let's start from the `pickImage` method:
49
 Let's start from the `pickImage` method:
33
 
50
 
34
 ```dart
51
 ```dart
35
-// Currently Zefyr depends on image_picker plugin to show camera or image gallery.
36
-// (note that in future versions this may change so that users can choose their
37
-// own plugin and define custom sources)
38
 import 'package:image_picker/image_picker.dart';
52
 import 'package:image_picker/image_picker.dart';
39
 
53
 
40
 class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
54
 class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
53
 which essentially serves as an identifier for the image.
67
 which essentially serves as an identifier for the image.
54
 
68
 
55
 Returned value is stored in the document Delta and later on used to build the
69
 Returned value is stored in the document Delta and later on used to build the
56
-appropriate `Widget`.
70
+appropriate image widget.
57
 
71
 
58
 It is up to the developer to define what this value represents.
72
 It is up to the developer to define what this value represents.
59
 
73
 
60
 In the above example we simply return a full path to the file on user's device,
74
 In the above example we simply return a full path to the file on user's device,
61
 e.g. `file:///Users/something/something/image.jpg`. Some other examples
75
 e.g. `file:///Users/something/something/image.jpg`. Some other examples
62
-may include a web link, `https://myapp.com/images/some.jpg` or just some
63
-arbitrary string like an ID.
76
+may include a web link, `https://myapp.com/images/some.jpg` or an
77
+arbitrary string like an identifier of an image in a cloud storage like AWS S3.
64
 
78
 
65
 For instance, if you upload files to your server you can initiate this task
79
 For instance, if you upload files to your server you can initiate this task
66
-in `pickImage`, for instance:
80
+in `pickImage` as follows:
67
 
81
 
68
 ```dart
82
 ```dart
69
 class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
83
 class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
84
 
98
 
85
 Next we need to implement `buildImage`. This method takes `imageSource` argument
99
 Next we need to implement `buildImage`. This method takes `imageSource` argument
86
 which contains that same string you returned from `pickImage`. Here you can
100
 which contains that same string you returned from `pickImage`. Here you can
87
-use this value to create a Flutter `Widget` which renders the image. Normally
101
+use this value to create a Flutter widget which renders the image. Normally
88
 you would return the standard `Image` widget from this method, but it is not
102
 you would return the standard `Image` widget from this method, but it is not
89
 a requirement. You are free to create a custom widget which, for instance,
103
 a requirement. You are free to create a custom widget which, for instance,
90
 shows progress of upload operation that you initiated in the `pickImage` call.
104
 shows progress of upload operation that you initiated in the `pickImage` call.
97
   // ...
111
   // ...
98
 
112
 
99
   @override
113
   @override
100
-  Widget buildImage(BuildContext context, String imageSource) {
101
-    final file = new File.fromUri(Uri.parse(imageSource));
102
-    /// Create standard [FileImage] provider. If [imageSource] was an HTTP link
114
+  Widget buildImage(BuildContext context, String key) {
115
+    final file = File.fromUri(Uri.parse(key));
116
+    /// Create standard [FileImage] provider. If [key] was an HTTP link
103
     /// we could use [NetworkImage] instead.
117
     /// we could use [NetworkImage] instead.
104
-    final image = new FileImage(file);
105
-    return new Image(image: image);
118
+    final image = FileImage(file);
119
+    return Image(image: image);
120
+  }
121
+}
122
+```
123
+
124
+There is two more overrides we need to implement which configure source types
125
+used by Zefyr toolbar:
126
+
127
+```dart
128
+class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
129
+  // ...
130
+  @override
131
+  ImageSource get cameraSource => ImageSource.camera;
132
+
133
+  @override
134
+  ImageSource get gallerySource => ImageSource.gallery;
135
+}
136
+```
137
+
138
+Now our image delegate is ready to be used by Zefyr so the last step is to
139
+pass it to Zefyr editor:
140
+
141
+```dart
142
+import 'package:zefyr/zefyr.dart'
143
+
144
+class MyAppPageState extends State<MyAppPage> {
145
+  FocueNode _focusNode = FocusNode();
146
+  ZefyrController _controller;
147
+
148
+  // ...
149
+
150
+  @override
151
+  Widget build(BuildContext context) {
152
+    final editor = new ZefyrEditor(
153
+      focusNode: _focusNode,
154
+      controller: _controller,
155
+      imageDelegate: MyAppZefyrImageDelegate(),
156
+    );
157
+
158
+    // ... do more with this page's layout
159
+
160
+    return ZefyrScaffold(
161
+      child: Container(
162
+        // ... customize
163
+        child: editor,
164
+      )
165
+    );
106
   }
166
   }
107
 }
167
 }
108
 ```
168
 ```
111
 
171
 
112
 * [Heuristics][heuristics]
172
 * [Heuristics][heuristics]
113
 
173
 
114
-[heuristics]: /doc/heuristics.md
174
+[heuristics]: /doc/heuristics.md

+ 21
- 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
+* Added support for "selectAll" action in selection toolbar.
14
+* Breaking change: removed `ZefyrDefaultImageDelegate` as well as dependency on
15
+  `image_picker` plugin. Users are required to provide their own implementation. If image delegate
16
+  is not provided then image toolbar button is disabled.
17
+* Breaking change: added `ZefyrImageDelegate.cameraSource` and `ZefyrImageDelegate.gallerySource`
18
+  fields. For users of `image_picker` plugin these should return `ImageSource.camera` and
19
+  `ImageSource.gallery` respectively. See documentation on implementing image support for more
20
+  details.
21
+
1
 ## 0.6.1
22
 ## 0.6.1
2
 
23
 
3
 * Relaxed dependency constraint on `image_picker` library to allow latest version. Note that
24
 * Relaxed dependency constraint on `image_picker` library to allow latest version. Note that

+ 5
- 0
packages/zefyr/example/lib/src/form.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
+
1
 import 'package:flutter/material.dart';
5
 import 'package:flutter/material.dart';
2
 import 'package:zefyr/zefyr.dart';
6
 import 'package:zefyr/zefyr.dart';
3
 
7
 
4
 import 'full_page.dart';
8
 import 'full_page.dart';
9
+import 'images.dart';
5
 
10
 
6
 class FormEmbeddedScreen extends StatefulWidget {
11
 class FormEmbeddedScreen extends StatefulWidget {
7
   @override
12
   @override

+ 7
- 18
packages/zefyr/example/lib/src/full_page.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
+
1
 import 'dart:async';
5
 import 'dart:async';
2
 import 'dart:convert';
6
 import 'dart:convert';
3
 
7
 
5
 import 'package:quill_delta/quill_delta.dart';
9
 import 'package:quill_delta/quill_delta.dart';
6
 import 'package:zefyr/zefyr.dart';
10
 import 'package:zefyr/zefyr.dart';
7
 
11
 
12
+import 'images.dart';
13
+
8
 class ZefyrLogo extends StatelessWidget {
14
 class ZefyrLogo extends StatelessWidget {
9
   @override
15
   @override
10
   Widget build(BuildContext context) {
16
   Widget build(BuildContext context) {
86
           child: ZefyrEditor(
92
           child: ZefyrEditor(
87
             controller: _controller,
93
             controller: _controller,
88
             focusNode: _focusNode,
94
             focusNode: _focusNode,
89
-            enabled: _editing,
95
+            mode: _editing ? ZefyrMode.edit : ZefyrMode.select,
90
             imageDelegate: new CustomImageDelegate(),
96
             imageDelegate: new CustomImageDelegate(),
91
           ),
97
           ),
92
         ),
98
         ),
106
     });
112
     });
107
   }
113
   }
108
 }
114
 }
109
-
110
-/// Custom image delegate used by this example to load image from application
111
-/// assets.
112
-///
113
-/// Default image delegate only supports [FileImage]s.
114
-class CustomImageDelegate extends ZefyrDefaultImageDelegate {
115
-  @override
116
-  Widget buildImage(BuildContext context, String imageSource) {
117
-    // We use custom "asset" scheme to distinguish asset images from other files.
118
-    if (imageSource.startsWith('asset://')) {
119
-      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
120
-      return new Image(image: asset);
121
-    } else {
122
-      return super.buildImage(context, imageSource);
123
-    }
124
-  }
125
-}

+ 40
- 0
packages/zefyr/example/lib/src/images.dart View File

1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+
5
+import 'dart:io';
6
+
7
+import 'package:flutter/widgets.dart';
8
+import 'package:image_picker/image_picker.dart';
9
+import 'package:zefyr/zefyr.dart';
10
+
11
+/// Custom image delegate used by this example to load image from application
12
+/// assets.
13
+class CustomImageDelegate implements ZefyrImageDelegate<ImageSource> {
14
+  @override
15
+  ImageSource get cameraSource => ImageSource.camera;
16
+
17
+  @override
18
+  ImageSource get gallerySource => ImageSource.gallery;
19
+
20
+  @override
21
+  Future<String> pickImage(ImageSource source) async {
22
+    final file = await ImagePicker.pickImage(source: source);
23
+    if (file == null) return null;
24
+    return file.uri.toString();
25
+  }
26
+
27
+  @override
28
+  Widget buildImage(BuildContext context, String key) {
29
+    // We use custom "asset" scheme to distinguish asset images from other files.
30
+    if (key.startsWith('asset://')) {
31
+      final asset = AssetImage(key.replaceFirst('asset://', ''));
32
+      return Image(image: asset);
33
+    } else {
34
+      // Otherwise assume this is a file stored locally on user's device.
35
+      final file = File.fromUri(Uri.parse(key));
36
+      final image = FileImage(file);
37
+      return new Image(image: image);
38
+    }
39
+  }
40
+}

+ 5
- 17
packages/zefyr/example/lib/src/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.
4
+
1
 import 'dart:convert';
5
 import 'dart:convert';
2
 
6
 
3
 import 'package:flutter/material.dart';
7
 import 'package:flutter/material.dart';
5
 import 'package:zefyr/zefyr.dart';
9
 import 'package:zefyr/zefyr.dart';
6
 
10
 
7
 import 'full_page.dart';
11
 import 'full_page.dart';
12
+import 'images.dart';
8
 
13
 
9
 class ViewScreen extends StatefulWidget {
14
 class ViewScreen extends StatefulWidget {
10
   @override
15
   @override
68
     );
73
     );
69
   }
74
   }
70
 }
75
 }
71
-
72
-/// Custom image delegate used by this example to load image from application
73
-/// assets.
74
-///
75
-/// Default image delegate only supports [FileImage]s.
76
-class CustomImageDelegate extends ZefyrDefaultImageDelegate {
77
-  @override
78
-  Widget buildImage(BuildContext context, String imageSource) {
79
-    // We use custom "asset" scheme to distinguish asset images from other files.
80
-    if (imageSource.startsWith('asset://')) {
81
-      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
82
-      return new Image(image: asset);
83
-    } else {
84
-      return super.buildImage(context, imageSource);
85
-    }
86
-  }
87
-}

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

16
   # The following adds the Cupertino Icons font to your application.
16
   # The following adds the Cupertino Icons font to your application.
17
   # Use with the CupertinoIcons class for iOS style icons.
17
   # Use with the CupertinoIcons class for iOS style icons.
18
   cupertino_icons: ^0.1.2
18
   cupertino_icons: ^0.1.2
19
+  image_picker: ^0.6.1
19
   zefyr:
20
   zefyr:
20
     path: ../
21
     path: ../
21
 
22
 

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

3
 // BSD-style license that can be found in the LICENSE file.
3
 // BSD-style license that can be found in the LICENSE file.
4
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
5
 import 'package:flutter/services.dart';
5
 import 'package:flutter/services.dart';
6
-import 'package:image_picker/image_picker.dart';
7
 import 'package:notus/notus.dart';
6
 import 'package:notus/notus.dart';
8
 import 'package:url_launcher/url_launcher.dart';
7
 import 'package:url_launcher/url_launcher.dart';
9
 
8
 
83
       );
82
       );
84
     } else {
83
     } else {
85
       assert(_text != null);
84
       assert(_text != null);
86
-      var style = _textStyle ?? new TextStyle();
85
+      var style = _textStyle ?? TextStyle();
87
       style = style.copyWith(color: iconColor);
86
       style = style.copyWith(color: iconColor);
88
       return RawZefyrButton(
87
       return RawZefyrButton(
89
         action: action,
88
         action: action,
90
-        child: new Text(_text, style: style),
89
+        child: Text(_text, style: style),
91
         color: _getColor(editor, toolbarTheme),
90
         color: _getColor(editor, toolbarTheme),
92
         onPressed: _getPressedHandler(editor, toolbar),
91
         onPressed: _getPressedHandler(editor, toolbar),
93
       );
92
       );
156
     Color iconColor,
155
     Color iconColor,
157
     @required this.color,
156
     @required this.color,
158
     @required this.onPressed,
157
     @required this.onPressed,
159
-  })  : child = new Icon(icon, size: size, color: iconColor),
158
+  })  : child = Icon(icon, size: size, color: iconColor),
160
         super();
159
         super();
161
 
160
 
162
   /// Toolbar action associated with this button.
161
   /// Toolbar action associated with this button.
279
 
278
 
280
   void _pickFromCamera() async {
279
   void _pickFromCamera() async {
281
     final editor = ZefyrToolbar.of(context).editor;
280
     final editor = ZefyrToolbar.of(context).editor;
282
-    final image = await editor.imageDelegate.pickImage(ImageSource.camera);
281
+    final image =
282
+        await editor.imageDelegate.pickImage(editor.imageDelegate.cameraSource);
283
     if (image != null)
283
     if (image != null)
284
       editor.formatSelection(NotusAttribute.embed.image(image));
284
       editor.formatSelection(NotusAttribute.embed.image(image));
285
   }
285
   }
286
 
286
 
287
   void _pickFromGallery() async {
287
   void _pickFromGallery() async {
288
     final editor = ZefyrToolbar.of(context).editor;
288
     final editor = ZefyrToolbar.of(context).editor;
289
-    final image = await editor.imageDelegate.pickImage(ImageSource.gallery);
289
+    final image = await editor.imageDelegate
290
+        .pickImage(editor.imageDelegate.gallerySource);
290
     if (image != null)
291
     if (image != null)
291
       editor.formatSelection(NotusAttribute.embed.image(image));
292
       editor.formatSelection(NotusAttribute.embed.image(image));
292
   }
293
   }
345
   void edit() {
346
   void edit() {
346
     final toolbar = ZefyrToolbar.of(context);
347
     final toolbar = ZefyrToolbar.of(context);
347
     setState(() {
348
     setState(() {
348
-      _inputKey = new UniqueKey();
349
+      _inputKey = UniqueKey();
349
       _inputController.text = getLink('https://');
350
       _inputController.text = getLink('https://');
350
       _inputController.addListener(_handleInputChange);
351
       _inputController.addListener(_handleInputChange);
351
       toolbar.markNeedsRebuild();
352
       toolbar.markNeedsRebuild();
404
   void copyToClipboard() {
405
   void copyToClipboard() {
405
     var link = getLink();
406
     var link = getLink();
406
     assert(link != null);
407
     assert(link != null);
407
-    Clipboard.setData(new ClipboardData(text: link));
408
+    Clipboard.setData(ClipboardData(text: link));
408
   }
409
   }
409
 
410
 
410
   void openInBrowser() async {
411
   void openInBrowser() async {
484
 
485
 
485
   @override
486
   @override
486
   _LinkInputState createState() {
487
   _LinkInputState createState() {
487
-    return new _LinkInputState();
488
+    return _LinkInputState();
488
   }
489
   }
489
 }
490
 }
490
 
491
 
532
       focusNode: _focusNode,
533
       focusNode: _focusNode,
533
       controller: widget.controller,
534
       controller: widget.controller,
534
       autofocus: true,
535
       autofocus: true,
535
-      decoration: new InputDecoration(
536
+      decoration: InputDecoration(
536
         hintText: 'https://',
537
         hintText: 'https://',
537
         filled: true,
538
         filled: true,
538
         fillColor: toolbarTheme.color,
539
         fillColor: toolbarTheme.color,
553
   Widget build(BuildContext context) {
554
   Widget build(BuildContext context) {
554
     final theme = Theme.of(context);
555
     final theme = Theme.of(context);
555
     final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
556
     final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
556
-    Widget widget = new ClipRect(
557
+    Widget widget = ClipRect(
557
       child: ListView(
558
       child: ListView(
558
         scrollDirection: Axis.horizontal,
559
         scrollDirection: Axis.horizontal,
559
         children: <Widget>[
560
         children: <Widget>[

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

11
   static const double _kCaretWidth = 1.0; // pixels
11
   static const double _kCaretWidth = 1.0; // pixels
12
 
12
 
13
   static Rect buildPrototype(double lineHeight) {
13
   static Rect buildPrototype(double lineHeight) {
14
-    return new Rect.fromLTWH(
14
+    return Rect.fromLTWH(
15
         0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset);
15
         0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset);
16
   }
16
   }
17
 
17
 
35
   }
35
   }
36
 
36
 
37
   void paint(Canvas canvas, Offset offset) {
37
   void paint(Canvas canvas, Offset offset) {
38
-    final Paint paint = new Paint()..color = _color;
38
+    final Paint paint = Paint()..color = _color;
39
     final Rect caretRect = _prototype.shift(offset);
39
     final Rect caretRect = _prototype.shift(offset);
40
     canvas.drawRect(caretRect, paint);
40
     canvas.drawRect(caretRect, paint);
41
   }
41
   }

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

23
       items.add(_buildLine(line, theme.blockTheme.code.textStyle));
23
       items.add(_buildLine(line, theme.blockTheme.code.textStyle));
24
     }
24
     }
25
 
25
 
26
-    return new Padding(
26
+    return Padding(
27
       padding: theme.blockTheme.code.padding,
27
       padding: theme.blockTheme.code.padding,
28
-      child: new Container(
28
+      child: Container(
29
         // TODO: make decorations configurable
29
         // TODO: make decorations configurable
30
         decoration: BoxDecoration(
30
         decoration: BoxDecoration(
31
           color: Colors.blueGrey.shade50,
31
           color: Colors.blueGrey.shade50,
32
           borderRadius: BorderRadius.circular(3.0),
32
           borderRadius: BorderRadius.circular(3.0),
33
         ),
33
         ),
34
         padding: const EdgeInsets.all(16.0),
34
         padding: const EdgeInsets.all(16.0),
35
-        child: new Column(
35
+        child: Column(
36
           crossAxisAlignment: CrossAxisAlignment.stretch,
36
           crossAxisAlignment: CrossAxisAlignment.stretch,
37
           children: items,
37
           children: items,
38
         ),
38
         ),
42
 
42
 
43
   Widget _buildLine(Node node, TextStyle style) {
43
   Widget _buildLine(Node node, TextStyle style) {
44
     LineNode line = node;
44
     LineNode line = node;
45
-    return new RawZefyrLine(node: line, style: style);
45
+    return RawZefyrLine(node: line, style: style);
46
   }
46
   }
47
 }
47
 }

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

35
   final EdgeInsets padding;
35
   final EdgeInsets padding;
36
 
36
 
37
   @override
37
   @override
38
-  _RawZefyrLineState createState() => new _RawZefyrLineState();
38
+  _RawZefyrLineState createState() => _RawZefyrLineState();
39
 }
39
 }
40
 
40
 
41
 class _RawZefyrLineState extends State<RawZefyrLine> {
41
 class _RawZefyrLineState extends State<RawZefyrLine> {
42
-  final LayerLink _link = new LayerLink();
42
+  final LayerLink _link = LayerLink();
43
 
43
 
44
   @override
44
   @override
45
   Widget build(BuildContext context) {
45
   Widget build(BuildContext context) {
113
     final List<TextSpan> children = widget.node.children
113
     final List<TextSpan> children = widget.node.children
114
         .map((node) => _segmentToTextSpan(node, theme))
114
         .map((node) => _segmentToTextSpan(node, theme))
115
         .toList(growable: false);
115
         .toList(growable: false);
116
-    return new TextSpan(style: widget.style, children: children);
116
+    return TextSpan(style: widget.style, children: children);
117
   }
117
   }
118
 
118
 
119
   TextSpan _segmentToTextSpan(Node node, ZefyrThemeData theme) {
119
   TextSpan _segmentToTextSpan(Node node, ZefyrThemeData theme) {
120
     final TextNode segment = node;
120
     final TextNode segment = node;
121
     final attrs = segment.style;
121
     final attrs = segment.style;
122
 
122
 
123
-    return new TextSpan(
123
+    return TextSpan(
124
       text: segment.value,
124
       text: segment.value,
125
       style: _getTextStyle(attrs, theme),
125
       style: _getTextStyle(attrs, theme),
126
     );
126
     );
127
   }
127
   }
128
 
128
 
129
   TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) {
129
   TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) {
130
-    TextStyle result = new TextStyle();
130
+    TextStyle result = TextStyle();
131
     if (style.containsSame(NotusAttribute.bold)) {
131
     if (style.containsSame(NotusAttribute.bold)) {
132
       result = result.merge(theme.boldStyle);
132
       result = result.merge(theme.boldStyle);
133
     }
133
     }
149
     } else if (embed.type == EmbedType.image) {
149
     } else if (embed.type == EmbedType.image) {
150
       return ZefyrImage(node: node, delegate: scope.imageDelegate);
150
       return ZefyrImage(node: node, delegate: scope.imageDelegate);
151
     } else {
151
     } else {
152
-      throw new UnimplementedError('Unimplemented embed type ${embed.type}');
152
+      throw UnimplementedError('Unimplemented embed type ${embed.type}');
153
     }
153
     }
154
   }
154
   }
155
 }
155
 }

+ 11
- 5
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;
112
       } else {
119
       } else {
113
         // need to transform selection position in case actual delta
120
         // need to transform selection position in case actual delta
114
         // is different from user's version (in deletes and inserts).
121
         // is different from user's version (in deletes and inserts).
115
-        Delta user = new Delta()
122
+        Delta user = Delta()
116
           ..retain(index)
123
           ..retain(index)
117
           ..insert(text)
124
           ..insert(text)
118
           ..delete(length);
125
           ..delete(length);
137
     // the change. This is needed in cases when format operation actually
144
     // the change. This is needed in cases when format operation actually
138
     // inserts data into the document (e.g. embeds).
145
     // inserts data into the document (e.g. embeds).
139
     final base = change.transformPosition(_selection.baseOffset);
146
     final base = change.transformPosition(_selection.baseOffset);
140
-    final extent =
141
-        change.transformPosition(_selection.extentOffset);
147
+    final extent = change.transformPosition(_selection.extentOffset);
142
     final adjustedSelection =
148
     final adjustedSelection =
143
         _selection.copyWith(baseOffset: base, extentOffset: extent);
149
         _selection.copyWith(baseOffset: base, extentOffset: extent);
144
     if (_selection != adjustedSelection) {
150
     if (_selection != adjustedSelection) {
161
   }
167
   }
162
 
168
 
163
   TextEditingValue get plainTextEditingValue {
169
   TextEditingValue get plainTextEditingValue {
164
-    return new TextEditingValue(
170
+    return TextEditingValue(
165
       text: document.toPlainText(),
171
       text: document.toPlainText(),
166
       selection: selection,
172
       selection: selection,
167
-      composing: new TextRange.collapsed(0),
173
+      composing: TextRange.collapsed(0),
168
     );
174
     );
169
   }
175
   }
170
 
176
 

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

33
 
33
 
34
   @override
34
   @override
35
   RenderEditableProxyBox createRenderObject(BuildContext context) {
35
   RenderEditableProxyBox createRenderObject(BuildContext context) {
36
-    return new RenderEditableProxyBox(
36
+    return RenderEditableProxyBox(
37
       node: node,
37
       node: node,
38
       layerLink: layerLink,
38
       layerLink: layerLink,
39
       renderContext: renderContext,
39
       renderContext: renderContext,
238
   @override
238
   @override
239
   bool hitTest(HitTestResult result, {Offset position}) {
239
   bool hitTest(HitTestResult result, {Offset position}) {
240
     if (size.contains(position)) {
240
     if (size.contains(position)) {
241
-      result.add(new BoxHitTestEntry(this, position));
241
+      result.add(BoxHitTestEntry(this, position));
242
       return true;
242
       return true;
243
     }
243
     }
244
     return false;
244
     return false;

+ 57
- 27
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
 
52
   @override
78
   @override
53
-  _ZefyrEditableTextState createState() => new _ZefyrEditableTextState();
79
+  _ZefyrEditableTextState createState() => _ZefyrEditableTextState();
54
 }
80
 }
55
 
81
 
56
 class _ZefyrEditableTextState extends State<ZefyrEditableText>
82
 class _ZefyrEditableTextState extends State<ZefyrEditableText>
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
   }
130
     _focusNode = widget.focusNode;
160
     _focusNode = widget.focusNode;
131
     super.initState();
161
     super.initState();
132
     _focusAttachment = _focusNode.attach(context);
162
     _focusAttachment = _focusNode.attach(context);
133
-    _input = new InputConnectionController(_handleRemoteValueChange);
163
+    _input = InputConnectionController(_handleRemoteValueChange);
134
     _updateSubscriptions();
164
     _updateSubscriptions();
135
   }
165
   }
136
 
166
 
198
   Widget _defaultChildBuilder(BuildContext context, Node node) {
228
   Widget _defaultChildBuilder(BuildContext context, Node node) {
199
     if (node is LineNode) {
229
     if (node is LineNode) {
200
       if (node.hasEmbed) {
230
       if (node.hasEmbed) {
201
-        return new RawZefyrLine(node: node);
231
+        return RawZefyrLine(node: node);
202
       } else if (node.style.contains(NotusAttribute.heading)) {
232
       } else if (node.style.contains(NotusAttribute.heading)) {
203
-        return new ZefyrHeading(node: node);
233
+        return ZefyrHeading(node: node);
204
       }
234
       }
205
-      return new ZefyrParagraph(node: node);
235
+      return ZefyrParagraph(node: node);
206
     }
236
     }
207
 
237
 
208
     final BlockNode block = node;
238
     final BlockNode block = node;
209
     final blockStyle = block.style.get(NotusAttribute.block);
239
     final blockStyle = block.style.get(NotusAttribute.block);
210
     if (blockStyle == NotusAttribute.block.code) {
240
     if (blockStyle == NotusAttribute.block.code) {
211
-      return new ZefyrCode(node: block);
241
+      return ZefyrCode(node: block);
212
     } else if (blockStyle == NotusAttribute.block.bulletList) {
242
     } else if (blockStyle == NotusAttribute.block.bulletList) {
213
-      return new ZefyrList(node: block);
243
+      return ZefyrList(node: block);
214
     } else if (blockStyle == NotusAttribute.block.numberList) {
244
     } else if (blockStyle == NotusAttribute.block.numberList) {
215
-      return new ZefyrList(node: block);
245
+      return ZefyrList(node: block);
216
     } else if (blockStyle == NotusAttribute.block.quote) {
246
     } else if (blockStyle == NotusAttribute.block.quote) {
217
-      return new ZefyrQuote(node: block);
247
+      return ZefyrQuote(node: block);
218
     }
248
     }
219
 
249
 
220
-    throw new UnimplementedError('Block format $blockStyle.');
250
+    throw UnimplementedError('Block format $blockStyle.');
221
   }
251
   }
222
 
252
 
223
   void _updateSubscriptions([ZefyrEditableText oldWidget]) {
253
   void _updateSubscriptions([ZefyrEditableText oldWidget]) {
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();

+ 44
- 8
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.
37
   final EdgeInsets padding;
70
   final EdgeInsets padding;
38
 
71
 
39
   @override
72
   @override
40
-  _ZefyrEditorState createState() => new _ZefyrEditorState();
73
+  _ZefyrEditorState createState() => _ZefyrEditorState();
41
 }
74
 }
42
 
75
 
43
 class _ZefyrEditorState extends State<ZefyrEditor> {
76
 class _ZefyrEditorState extends State<ZefyrEditor> {
88
   @override
121
   @override
89
   void initState() {
122
   void initState() {
90
     super.initState();
123
     super.initState();
91
-    _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
124
+    _imageDelegate = widget.imageDelegate;
92
   }
125
   }
93
 
126
 
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) {
100
-      _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
134
+      _imageDelegate = widget.imageDelegate;
101
       _scope.imageDelegate = _imageDelegate;
135
       _scope.imageDelegate = _imageDelegate;
102
     }
136
     }
103
   }
137
   }
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,
143
 
178
 
144
   @override
179
   @override
145
   Widget build(BuildContext context) {
180
   Widget build(BuildContext context) {
146
-    Widget editable = new ZefyrEditableText(
181
+    Widget editable = ZefyrEditableText(
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;

+ 35
- 37
packages/zefyr/lib/src/widgets/image.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 'dart:async';
4
 import 'dart:async';
5
-import 'dart:io';
6
 import 'dart:math' as math;
5
 import 'dart:math' as math;
7
 import 'dart:ui' as ui;
6
 import 'dart:ui' as ui;
8
 
7
 
9
 import 'package:flutter/rendering.dart';
8
 import 'package:flutter/rendering.dart';
10
 import 'package:flutter/widgets.dart';
9
 import 'package:flutter/widgets.dart';
10
+import 'package:meta/meta.dart';
11
 import 'package:notus/notus.dart';
11
 import 'package:notus/notus.dart';
12
-import 'package:image_picker/image_picker.dart';
13
 
12
 
14
 import 'editable_box.dart';
13
 import 'editable_box.dart';
15
 
14
 
15
+/// Provides interface for embedding images into Zefyr editor.
16
+// TODO: allow configuring image sources and related toolbar buttons.
17
+@experimental
16
 abstract class ZefyrImageDelegate<S> {
18
 abstract class ZefyrImageDelegate<S> {
17
-  /// Builds image widget for specified [imageSource] and [context].
18
-  Widget buildImage(BuildContext context, String imageSource);
19
+  /// Unique key to identify camera source.
20
+  S get cameraSource;
21
+
22
+  /// Unique key to identify gallery source.
23
+  S get gallerySource;
24
+
25
+  /// Builds image widget for specified image [key].
26
+  ///
27
+  /// The [key] argument contains value which was previously returned from
28
+  /// [pickImage] method.
29
+  Widget buildImage(BuildContext context, String key);
19
 
30
 
20
   /// Picks an image from specified [source].
31
   /// Picks an image from specified [source].
21
   ///
32
   ///
22
   /// Returns unique string key for the selected image. Returned key is stored
33
   /// Returns unique string key for the selected image. Returned key is stored
23
   /// in the document.
34
   /// in the document.
35
+  ///
36
+  /// Depending on your application returned key may represent a path to
37
+  /// an image file on user's device, an HTTP link, or an identifier generated
38
+  /// by a file hosting service like AWS S3 or Google Drive.
24
   Future<String> pickImage(S source);
39
   Future<String> pickImage(S source);
25
 }
40
 }
26
 
41
 
27
-class ZefyrDefaultImageDelegate implements ZefyrImageDelegate<ImageSource> {
28
-  @override
29
-  Widget buildImage(BuildContext context, String imageSource) {
30
-    final file = new File.fromUri(Uri.parse(imageSource));
31
-    final image = new FileImage(file);
32
-    return new Image(image: image);
33
-  }
34
-
35
-  @override
36
-  Future<String> pickImage(ImageSource source) async {
37
-    final file = await ImagePicker.pickImage(source: source);
38
-    if (file == null) return null;
39
-    return file.uri.toString();
40
-  }
41
-}
42
-
43
 class ZefyrImage extends StatefulWidget {
42
 class ZefyrImage extends StatefulWidget {
44
   const ZefyrImage({Key key, @required this.node, @required this.delegate})
43
   const ZefyrImage({Key key, @required this.node, @required this.delegate})
45
       : super(key: key);
44
       : super(key: key);
76
 
75
 
77
   @override
76
   @override
78
   RenderEditableImage createRenderObject(BuildContext context) {
77
   RenderEditableImage createRenderObject(BuildContext context) {
79
-    return new RenderEditableImage(node: node);
78
+    return RenderEditableImage(node: node);
80
   }
79
   }
81
 
80
 
82
   @override
81
   @override
130
     if (local.isCollapsed) {
129
     if (local.isCollapsed) {
131
       final dx = local.extentOffset == 0 ? _childOffset.dx : size.width;
130
       final dx = local.extentOffset == 0 ? _childOffset.dx : size.width;
132
       return [
131
       return [
133
-        new ui.TextBox.fromLTRBD(
132
+        ui.TextBox.fromLTRBD(
134
             dx, 0.0, dx, size.height - kPaddingBottom, TextDirection.ltr),
133
             dx, 0.0, dx, size.height - kPaddingBottom, TextDirection.ltr),
135
       ];
134
       ];
136
     }
135
     }
137
 
136
 
138
     final rect = _childRect;
137
     final rect = _childRect;
139
     return [
138
     return [
140
-      new ui.TextBox.fromLTRBD(
139
+      ui.TextBox.fromLTRBD(
141
           rect.left, rect.top, rect.left, rect.bottom, TextDirection.ltr),
140
           rect.left, rect.top, rect.left, rect.bottom, TextDirection.ltr),
142
-      new ui.TextBox.fromLTRBD(
141
+      ui.TextBox.fromLTRBD(
143
           rect.right, rect.top, rect.right, rect.bottom, TextDirection.ltr),
142
           rect.right, rect.top, rect.right, rect.bottom, TextDirection.ltr),
144
     ];
143
     ];
145
   }
144
   }
151
     if (offset.dx > size.width / 2) {
150
     if (offset.dx > size.width / 2) {
152
       position++;
151
       position++;
153
     }
152
     }
154
-    return new TextPosition(offset: position);
153
+    return TextPosition(offset: position);
155
   }
154
   }
156
 
155
 
157
   @override
156
   @override
158
   TextRange getWordBoundary(TextPosition position) {
157
   TextRange getWordBoundary(TextPosition position) {
159
     final start = _node.documentOffset;
158
     final start = _node.documentOffset;
160
-    return new TextRange(start: start, end: start + 1);
159
+    return TextRange(start: start, end: start + 1);
161
   }
160
   }
162
 
161
 
163
   @override
162
   @override
170
   @override
169
   @override
171
   Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
170
   Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
172
     final pos = position.offset - node.documentOffset;
171
     final pos = position.offset - node.documentOffset;
173
-    Offset caretOffset = _childOffset - new Offset(kHorizontalPadding, 0.0);
172
+    Offset caretOffset = _childOffset - Offset(kHorizontalPadding, 0.0);
174
     if (pos == 1) {
173
     if (pos == 1) {
175
-      caretOffset = caretOffset +
176
-          new Offset(_lastChildSize.width + kHorizontalPadding, 0.0);
174
+      caretOffset =
175
+          caretOffset + Offset(_lastChildSize.width + kHorizontalPadding, 0.0);
177
     }
176
     }
178
     return caretOffset;
177
     return caretOffset;
179
   }
178
   }
184
     final localSelection = getLocalSelection(selection);
183
     final localSelection = getLocalSelection(selection);
185
     assert(localSelection != null);
184
     assert(localSelection != null);
186
     if (!localSelection.isCollapsed) {
185
     if (!localSelection.isCollapsed) {
187
-      final Paint paint = new Paint()
186
+      final Paint paint = Paint()
188
         ..color = selectionColor
187
         ..color = selectionColor
189
         ..style = PaintingStyle.stroke
188
         ..style = PaintingStyle.stroke
190
         ..strokeWidth = 3.0;
189
         ..strokeWidth = 3.0;
191
-      final rect = new Rect.fromLTWH(
192
-          0.0, 0.0, _lastChildSize.width, _lastChildSize.height);
190
+      final rect =
191
+          Rect.fromLTWH(0.0, 0.0, _lastChildSize.width, _lastChildSize.height);
193
       context.canvas.drawRect(rect.shift(offset + _childOffset), paint);
192
       context.canvas.drawRect(rect.shift(offset + _childOffset), paint);
194
     }
193
     }
195
   }
194
   }
205
   Offset get _childOffset {
204
   Offset get _childOffset {
206
     final dx = (size.width - _lastChildSize.width) / 2 + kHorizontalPadding;
205
     final dx = (size.width - _lastChildSize.width) / 2 + kHorizontalPadding;
207
     final dy = (size.height - _lastChildSize.height - kPaddingBottom) / 2;
206
     final dy = (size.height - _lastChildSize.height - kPaddingBottom) / 2;
208
-    return new Offset(dx, dy);
207
+    return Offset(dx, dy);
209
   }
208
   }
210
 
209
 
211
   Rect get _childRect {
210
   Rect get _childRect {
212
-    return new Rect.fromLTWH(_childOffset.dx, _childOffset.dy,
213
-        _lastChildSize.width, _lastChildSize.height);
211
+    return Rect.fromLTWH(_childOffset.dx, _childOffset.dy, _lastChildSize.width,
212
+        _lastChildSize.height);
214
   }
213
   }
215
 
214
 
216
   @override
215
   @override
227
       );
226
       );
228
       child.layout(childConstraints, parentUsesSize: true);
227
       child.layout(childConstraints, parentUsesSize: true);
229
       _lastChildSize = child.size;
228
       _lastChildSize = child.size;
230
-      size = new Size(
231
-          constraints.maxWidth, _lastChildSize.height + kPaddingBottom);
229
+      size = Size(constraints.maxWidth, _lastChildSize.height + kPaddingBottom);
232
     } else {
230
     } else {
233
       performResize();
231
       performResize();
234
     }
232
     }

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

37
       _lastKnownRemoteTextEditingValue = value;
37
       _lastKnownRemoteTextEditingValue = value;
38
       _textInputConnection = TextInput.attach(
38
       _textInputConnection = TextInput.attach(
39
         this,
39
         this,
40
-        new TextInputConfiguration(
40
+        TextInputConfiguration(
41
           inputType: TextInputType.multiline,
41
           inputType: TextInputType.multiline,
42
           obscureText: false,
42
           obscureText: false,
43
           autocorrect: true,
43
           autocorrect: true,

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

31
         : theme.blockTheme.bulletList.padding;
31
         : theme.blockTheme.bulletList.padding;
32
     padding = padding.copyWith(left: theme.indentSize);
32
     padding = padding.copyWith(left: theme.indentSize);
33
 
33
 
34
-    return new Padding(
34
+    return Padding(
35
       padding: padding,
35
       padding: padding,
36
-      child: new Column(children: items),
36
+      child: Column(children: items),
37
     );
37
     );
38
   }
38
   }
39
 
39
 
40
   Widget _buildItem(Node node, int index) {
40
   Widget _buildItem(Node node, int index) {
41
     LineNode line = node;
41
     LineNode line = node;
42
-    return new ZefyrListItem(index: index, node: line);
42
+    return ZefyrListItem(index: index, node: line);
43
   }
43
   }
44
 }
44
 }
45
 
45
 
66
       final headingTheme = ZefyrHeading.themeOf(node, context);
66
       final headingTheme = ZefyrHeading.themeOf(node, context);
67
       textStyle = headingTheme.textStyle;
67
       textStyle = headingTheme.textStyle;
68
       padding = headingTheme.padding;
68
       padding = headingTheme.padding;
69
-      content = new ZefyrHeading(node: node);
69
+      content = ZefyrHeading(node: node);
70
     } else {
70
     } else {
71
       textStyle = theme.paragraphTheme.textStyle;
71
       textStyle = theme.paragraphTheme.textStyle;
72
-      content = new RawZefyrLine(node: node, style: textStyle);
72
+      content = RawZefyrLine(node: node, style: textStyle);
73
     }
73
     }
74
 
74
 
75
     Widget bullet =
75
     Widget bullet =

+ 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
- 3
packages/zefyr/lib/src/widgets/paragraph.dart View File

22
     if (blockStyle != null) {
22
     if (blockStyle != null) {
23
       style = style.merge(blockStyle);
23
       style = style.merge(blockStyle);
24
     }
24
     }
25
-    return new RawZefyrLine(
25
+    return RawZefyrLine(
26
       node: node,
26
       node: node,
27
       style: style,
27
       style: style,
28
       padding: theme.paragraphTheme.padding,
28
       padding: theme.paragraphTheme.padding,
46
     if (blockStyle != null) {
46
     if (blockStyle != null) {
47
       style = style.merge(blockStyle);
47
       style = style.merge(blockStyle);
48
     }
48
     }
49
-    return new RawZefyrLine(
49
+    return RawZefyrLine(
50
       node: node,
50
       node: node,
51
       style: style,
51
       style: style,
52
       padding: theme.padding,
52
       padding: theme.padding,
63
     } else if (style == NotusAttribute.heading.level3) {
63
     } else if (style == NotusAttribute.heading.level3) {
64
       return theme.headingTheme.level3;
64
       return theme.headingTheme.level3;
65
     }
65
     }
66
-    throw new UnimplementedError('Unsupported heading style $style');
66
+    throw UnimplementedError('Unsupported heading style $style');
67
   }
67
   }
68
 }
68
 }

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

23
 
23
 
24
   @override
24
   @override
25
   RenderObject createRenderObject(BuildContext context) {
25
   RenderObject createRenderObject(BuildContext context) {
26
-    return new RenderZefyrParagraph(
26
+    return RenderZefyrParagraph(
27
       text,
27
       text,
28
       node: node,
28
       node: node,
29
       textDirection: Directionality.of(context),
29
       textDirection: Directionality.of(context),
51
     double textScaleFactor: 1.0,
51
     double textScaleFactor: 1.0,
52
     int maxLines,
52
     int maxLines,
53
   })  : _node = node,
53
   })  : _node = node,
54
-        _prototypePainter = new TextPainter(
55
-          text: new TextSpan(text: '.', style: text.style),
54
+        _prototypePainter = TextPainter(
55
+          text: TextSpan(text: '.', style: text.style),
56
           textAlign: textAlign,
56
           textAlign: textAlign,
57
           textDirection: textDirection,
57
           textDirection: textDirection,
58
           textScaleFactor: textScaleFactor,
58
           textScaleFactor: textScaleFactor,
94
   @override
94
   @override
95
   TextPosition getPositionForOffset(Offset offset) {
95
   TextPosition getPositionForOffset(Offset offset) {
96
     final position = super.getPositionForOffset(offset);
96
     final position = super.getPositionForOffset(offset);
97
-    return new TextPosition(
97
+    return TextPosition(
98
       offset: _node.documentOffset + position.offset,
98
       offset: _node.documentOffset + position.offset,
99
       affinity: position.affinity,
99
       affinity: position.affinity,
100
     );
100
     );
102
 
102
 
103
   @override
103
   @override
104
   TextRange getWordBoundary(TextPosition position) {
104
   TextRange getWordBoundary(TextPosition position) {
105
-    final localPosition = new TextPosition(
105
+    final localPosition = TextPosition(
106
       offset: position.offset - _node.documentOffset,
106
       offset: position.offset - _node.documentOffset,
107
       affinity: position.affinity,
107
       affinity: position.affinity,
108
     );
108
     );
109
     final localRange = super.getWordBoundary(localPosition);
109
     final localRange = super.getWordBoundary(localPosition);
110
-    return new TextRange(
110
+    return TextRange(
111
       start: _node.documentOffset + localRange.start,
111
       start: _node.documentOffset + localRange.start,
112
       end: _node.documentOffset + localRange.end,
112
       end: _node.documentOffset + localRange.end,
113
     );
113
     );
115
 
115
 
116
   @override
116
   @override
117
   Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
117
   Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
118
-    final localPosition = new TextPosition(
118
+    final localPosition = TextPosition(
119
       offset: position.offset - _node.documentOffset,
119
       offset: position.offset - _node.documentOffset,
120
       affinity: position.affinity,
120
       affinity: position.affinity,
121
     );
121
     );
131
       final caret = CursorPainter.buildPrototype(preferredLineHeight);
131
       final caret = CursorPainter.buildPrototype(preferredLineHeight);
132
       final offset = getOffsetForCaret(local.extent, caret);
132
       final offset = getOffsetForCaret(local.extent, caret);
133
       return [
133
       return [
134
-        new ui.TextBox.fromLTRBD(
134
+        ui.TextBox.fromLTRBD(
135
           offset.dx,
135
           offset.dx,
136
           offset.dy,
136
           offset.dy,
137
           offset.dx,
137
           offset.dx,
164
       final box = result.first;
164
       final box = result.first;
165
       final dx = isBaseShifted == -1 ? box.right : box.left;
165
       final dx = isBaseShifted == -1 ? box.right : box.left;
166
       result.removeAt(0);
166
       result.removeAt(0);
167
-      result.insert(0,
168
-          new ui.TextBox.fromLTRBD(dx, box.top, dx, box.bottom, box.direction));
167
+      result.insert(
168
+          0, ui.TextBox.fromLTRBD(dx, box.top, dx, box.bottom, box.direction));
169
     }
169
     }
170
     if (isExtentShifted) {
170
     if (isExtentShifted) {
171
       final box = result.last;
171
       final box = result.last;
172
       result.removeLast;
172
       result.removeLast;
173
-      result.add(new ui.TextBox.fromLTRBD(
173
+      result.add(ui.TextBox.fromLTRBD(
174
           box.left, box.top, box.left, box.bottom, box.direction));
174
           box.left, box.top, box.left, box.bottom, box.direction));
175
     }
175
     }
176
     return result;
176
     return result;
182
 
182
 
183
   @override
183
   @override
184
   void set text(InlineSpan value) {
184
   void set text(InlineSpan value) {
185
-    _prototypePainter.text = new TextSpan(text: '.', style: value.style);
185
+    _prototypePainter.text = TextSpan(text: '.', style: value.style);
186
     _selectionRects = null;
186
     _selectionRects = null;
187
     super.text = value;
187
     super.text = value;
188
   }
188
   }

+ 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
- 7
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),
29
-        isEditable = false,
28
+  ZefyrScope.view({ZefyrImageDelegate imageDelegate})
29
+      : isEditable = false,
30
+        _mode = ZefyrMode.view,
30
         _imageDelegate = imageDelegate;
31
         _imageDelegate = imageDelegate;
31
 
32
 
32
   /// Creates editable scope.
33
   /// Creates editable scope.
33
   ///
34
   ///
34
   /// Normally used in [ZefyrEditor].
35
   /// Normally used in [ZefyrEditor].
35
   ZefyrScope.editable({
36
   ZefyrScope.editable({
37
+    @required ZefyrMode mode,
36
     @required ZefyrController controller,
38
     @required ZefyrController controller,
37
-    @required ZefyrImageDelegate imageDelegate,
38
     @required FocusNode focusNode,
39
     @required FocusNode focusNode,
39
     @required FocusScopeNode focusScope,
40
     @required FocusScopeNode focusScope,
40
-  })  : assert(controller != null),
41
-        assert(imageDelegate != null),
41
+    ZefyrImageDelegate imageDelegate,
42
+  })  : assert(mode != null),
43
+        assert(controller != null),
42
         assert(focusNode != null),
44
         assert(focusNode != null),
43
         assert(focusScope != null),
45
         assert(focusScope != null),
44
         isEditable = true,
46
         isEditable = true,
47
+        _mode = mode,
45
         _controller = controller,
48
         _controller = controller,
46
         _imageDelegate = imageDelegate,
49
         _imageDelegate = imageDelegate,
47
         _focusNode = focusNode,
50
         _focusNode = focusNode,
63
   ZefyrImageDelegate _imageDelegate;
66
   ZefyrImageDelegate _imageDelegate;
64
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
67
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
65
   set imageDelegate(ZefyrImageDelegate value) {
68
   set imageDelegate(ZefyrImageDelegate value) {
66
-    assert(value != null);
67
     if (_imageDelegate != value) {
69
     if (_imageDelegate != value) {
68
       _imageDelegate = value;
70
       _imageDelegate = value;
69
       notifyListeners();
71
       notifyListeners();
70
     }
72
     }
71
   }
73
   }
72
 
74
 
75
+  ZefyrMode _mode;
76
+  ZefyrMode get mode => _mode;
77
+  set mode(ZefyrMode value) {
78
+    assert(value != null);
79
+    if (_mode != value) {
80
+      _mode = value;
81
+      notifyListeners();
82
+    }
83
+  }
84
+
73
   ZefyrController _controller;
85
   ZefyrController _controller;
74
   ZefyrController get controller => _controller;
86
   ZefyrController get controller => _controller;
75
   set controller(ZefyrController value) {
87
   set controller(ZefyrController value) {

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

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

88
     } else if (autoImplyTrailing) {
88
     } else if (autoImplyTrailing) {
89
       children.add(toolbar.buildButton(context, ZefyrToolbarAction.close));
89
       children.add(toolbar.buildButton(context, ZefyrToolbarAction.close));
90
     }
90
     }
91
-    return new Container(
91
+    return Container(
92
       constraints: constraints,
92
       constraints: constraints,
93
       child: Material(color: theme.color, child: Row(children: children)),
93
       child: Material(color: theme.color, child: Row(children: children)),
94
     );
94
     );
122
   ZefyrToolbarState createState() => ZefyrToolbarState();
122
   ZefyrToolbarState createState() => ZefyrToolbarState();
123
 
123
 
124
   @override
124
   @override
125
-  ui.Size get preferredSize => new Size.fromHeight(ZefyrToolbar.kToolbarHeight);
125
+  ui.Size get preferredSize => Size.fromHeight(ZefyrToolbar.kToolbarHeight);
126
 }
126
 }
127
 
127
 
128
 class _ZefyrToolbarScope extends InheritedWidget {
128
 class _ZefyrToolbarScope extends InheritedWidget {
165
 
165
 
166
   Future<void> showOverlay(WidgetBuilder builder) async {
166
   Future<void> showOverlay(WidgetBuilder builder) async {
167
     assert(_overlayBuilder == null);
167
     assert(_overlayBuilder == null);
168
-    final completer = new Completer<void>();
168
+    final completer = Completer<void>();
169
     setState(() {
169
     setState(() {
170
       _overlayBuilder = builder;
170
       _overlayBuilder = builder;
171
       _overlayCompleter = completer;
171
       _overlayCompleter = completer;
192
   @override
192
   @override
193
   void initState() {
193
   void initState() {
194
     super.initState();
194
     super.initState();
195
-    _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
196
-    _overlayAnimation = new AnimationController(
197
-        vsync: this, duration: Duration(milliseconds: 100));
195
+    _delegate = widget.delegate ?? _DefaultZefyrToolbarDelegate();
196
+    _overlayAnimation =
197
+        AnimationController(vsync: this, duration: Duration(milliseconds: 100));
198
     _selection = editor.selection;
198
     _selection = editor.selection;
199
   }
199
   }
200
 
200
 
202
   void didUpdateWidget(ZefyrToolbar oldWidget) {
202
   void didUpdateWidget(ZefyrToolbar oldWidget) {
203
     super.didUpdateWidget(oldWidget);
203
     super.didUpdateWidget(oldWidget);
204
     if (widget.delegate != oldWidget.delegate) {
204
     if (widget.delegate != oldWidget.delegate) {
205
-      _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
205
+      _delegate = widget.delegate ?? _DefaultZefyrToolbarDelegate();
206
     }
206
     }
207
   }
207
   }
208
 
208
 
227
     layers.add(toolbar);
227
     layers.add(toolbar);
228
 
228
 
229
     if (hasOverlay) {
229
     if (hasOverlay) {
230
-      Widget widget = new Builder(builder: _overlayBuilder);
230
+      Widget widget = Builder(builder: _overlayBuilder);
231
       assert(widget != null);
231
       assert(widget != null);
232
       final overlay = FadeTransition(
232
       final overlay = FadeTransition(
233
         key: _overlayKey,
233
         key: _overlayKey,
259
       buildButton(context, ZefyrToolbarAction.quote),
259
       buildButton(context, ZefyrToolbarAction.quote),
260
       buildButton(context, ZefyrToolbarAction.code),
260
       buildButton(context, ZefyrToolbarAction.code),
261
       buildButton(context, ZefyrToolbarAction.horizontalRule),
261
       buildButton(context, ZefyrToolbarAction.horizontalRule),
262
-      ImageButton(),
262
+      if (editor.imageDelegate != null) ImageButton(),
263
     ];
263
     ];
264
     return buttons;
264
     return buttons;
265
   }
265
   }
275
 }
275
 }
276
 
276
 
277
 class _ZefyrButtonListState extends State<ZefyrButtonList> {
277
 class _ZefyrButtonListState extends State<ZefyrButtonList> {
278
-  final ScrollController _controller = new ScrollController();
278
+  final ScrollController _controller = ScrollController();
279
   bool _showLeftArrow = false;
279
   bool _showLeftArrow = false;
280
   bool _showRightArrow = false;
280
   bool _showRightArrow = false;
281
 
281
 

+ 3
- 0
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';

+ 3
- 3
packages/zefyr/lib/util.dart View File

12
 export 'src/fast_diff.dart';
12
 export 'src/fast_diff.dart';
13
 
13
 
14
 int getPositionDelta(Delta user, Delta actual) {
14
 int getPositionDelta(Delta user, Delta actual) {
15
-  final userIter = new DeltaIterator(user);
16
-  final actualIter = new DeltaIterator(actual);
15
+  final userIter = DeltaIterator(user);
16
+  final actualIter = DeltaIterator(actual);
17
   int diff = 0;
17
   int diff = 0;
18
   while (userIter.hasNext || actualIter.hasNext) {
18
   while (userIter.hasNext || actualIter.hasNext) {
19
     num length = math.min(userIter.peekLength(), actualIter.peekLength());
19
     num length = math.min(userIter.peekLength(), actualIter.peekLength());
26
     } else if (userOp.isDelete && actualOp.isRetain) {
26
     } else if (userOp.isDelete && actualOp.isRetain) {
27
       diff += userOp.length;
27
       diff += userOp.length;
28
     } else if (userOp.isRetain && actualOp.isInsert) {
28
     } else if (userOp.isRetain && actualOp.isInsert) {
29
-      if (actualOp.data.startsWith('\n') ) {
29
+      if (actualOp.data.startsWith('\n')) {
30
         // At this point user input reached its end (retain). If a heuristic
30
         // At this point user input reached its end (retain). If a heuristic
31
         // rule inserts a new line we should keep cursor on it's original position.
31
         // rule inserts a new line we should keep cursor on it's original position.
32
         continue;
32
         continue;

+ 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';

+ 3
- 3
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.1
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
 
7
 environment:
7
 environment:
8
-  sdk: '>=2.1.0 <3.0.0'
8
+  sdk: '>=2.2.2 <3.0.0'
9
 
9
 
10
 dependencies:
10
 dependencies:
11
   flutter:
11
   flutter:
12
     sdk: flutter
12
     sdk: flutter
13
   collection: ^1.14.6
13
   collection: ^1.14.6
14
   url_launcher: ^5.0.0
14
   url_launcher: ^5.0.0
15
-  image_picker: '>=0.5.0 <0.7.0'
16
   quill_delta: ^1.0.0-dev.1.0
15
   quill_delta: ^1.0.0-dev.1.0
17
   notus: ^0.1.0
16
   notus: ^0.1.0
18
   meta: ^1.1.0
17
   meta: ^1.1.0
18
+  quiver_hashcode: ^2.0.0
19
 
19
 
20
 dev_dependencies:
20
 dev_dependencies:
21
   flutter_test:
21
   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
+}

+ 12
- 4
packages/zefyr/test/testing.dart View File

22
     NotusDocument document,
22
     NotusDocument document,
23
     ZefyrThemeData theme,
23
     ZefyrThemeData theme,
24
     bool autofocus: false,
24
     bool autofocus: false,
25
+    ZefyrImageDelegate imageDelegate,
25
   }) {
26
   }) {
26
     focusNode ??= FocusNode();
27
     focusNode ??= FocusNode();
27
     document ??= NotusDocument.fromDelta(delta);
28
     document ??= NotusDocument.fromDelta(delta);
31
       controller: controller,
32
       controller: controller,
32
       focusNode: focusNode,
33
       focusNode: focusNode,
33
       autofocus: autofocus,
34
       autofocus: autofocus,
35
+      imageDelegate: imageDelegate,
34
     );
36
     );
35
 
37
 
36
     if (theme != null) {
38
     if (theme != null) {
115
 }
117
 }
116
 
118
 
117
 class _ZefyrSandbox extends StatefulWidget {
119
 class _ZefyrSandbox extends StatefulWidget {
118
-  const _ZefyrSandbox(
119
-      {Key key, this.controller, this.focusNode, this.autofocus})
120
-      : super(key: key);
120
+  const _ZefyrSandbox({
121
+    Key key,
122
+    this.controller,
123
+    this.focusNode,
124
+    this.autofocus,
125
+    this.imageDelegate,
126
+  }) : super(key: key);
121
   final ZefyrController controller;
127
   final ZefyrController controller;
122
   final FocusNode focusNode;
128
   final FocusNode focusNode;
123
   final bool autofocus;
129
   final bool autofocus;
130
+  final ZefyrImageDelegate imageDelegate;
124
 
131
 
125
   @override
132
   @override
126
   _ZefyrSandboxState createState() => _ZefyrSandboxState();
133
   _ZefyrSandboxState createState() => _ZefyrSandboxState();
134
     return new ZefyrEditor(
141
     return new ZefyrEditor(
135
       controller: widget.controller,
142
       controller: widget.controller,
136
       focusNode: widget.focusNode,
143
       focusNode: widget.focusNode,
137
-      enabled: _enabled,
144
+      mode: _enabled ? ZefyrMode.edit : ZefyrMode.view,
138
       autofocus: widget.autofocus,
145
       autofocus: widget.autofocus,
146
+      imageDelegate: widget.imageDelegate,
139
     );
147
     );
140
   }
148
   }
141
 
149
 

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

1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
 // for details. All rights reserved. Use of this source code is governed by a
2
 // for details. All rights reserved. Use of this source code is governed by a
3
 // BSD-style license that can be found in the LICENSE file.
3
 // BSD-style license that can be found in the LICENSE file.
4
+import 'dart:io';
5
+
4
 import 'package:flutter/material.dart';
6
 import 'package:flutter/material.dart';
5
-import 'package:flutter/services.dart';
6
 import 'package:flutter_test/flutter_test.dart';
7
 import 'package:flutter_test/flutter_test.dart';
7
 import 'package:zefyr/src/widgets/buttons.dart';
8
 import 'package:zefyr/src/widgets/buttons.dart';
8
 import 'package:zefyr/zefyr.dart';
9
 import 'package:zefyr/zefyr.dart';
145
   });
146
   });
146
 
147
 
147
   group('$ImageButton', () {
148
   group('$ImageButton', () {
148
-    const MethodChannel channel =
149
-        const MethodChannel('plugins.flutter.io/image_picker');
150
-
151
-    final List<MethodCall> log = <MethodCall>[];
152
-
153
-    setUp(() {
154
-      channel.setMockMethodCallHandler((MethodCall methodCall) async {
155
-        log.add(methodCall);
156
-        return '/tmp/test.jpg';
157
-      });
158
-      log.clear();
159
-    });
160
-
161
     testWidgets('toggle overlay', (tester) async {
149
     testWidgets('toggle overlay', (tester) async {
162
-      final editor = new EditorSandBox(tester: tester);
150
+      final editor = new EditorSandBox(
151
+        tester: tester,
152
+        imageDelegate: _TestImageDelegate(),
153
+      );
163
       await editor.pumpAndTap();
154
       await editor.pumpAndTap();
164
       await editor.tapButtonWithIcon(Icons.photo);
155
       await editor.tapButtonWithIcon(Icons.photo);
165
 
156
 
169
     });
160
     });
170
 
161
 
171
     testWidgets('pick from camera', (tester) async {
162
     testWidgets('pick from camera', (tester) async {
172
-      final editor = new EditorSandBox(tester: tester);
163
+      final editor = new EditorSandBox(
164
+        tester: tester,
165
+        imageDelegate: _TestImageDelegate(),
166
+      );
173
       await editor.pumpAndTap();
167
       await editor.pumpAndTap();
174
       await editor.tapButtonWithIcon(Icons.photo);
168
       await editor.tapButtonWithIcon(Icons.photo);
175
       await editor.tapButtonWithIcon(Icons.photo_camera);
169
       await editor.tapButtonWithIcon(Icons.photo_camera);
176
-      expect(log, hasLength(1));
177
-      expect(
178
-        log.single,
179
-        isMethodCall(
180
-          'pickImage',
181
-          arguments: <String, dynamic>{
182
-            'source': 0,
183
-            'maxWidth': null,
184
-            'maxHeight': null,
185
-            'imageQuality': null,
186
-          },
187
-        ),
188
-      );
170
+      expect(find.byType(ZefyrImage), findsOneWidget);
189
     });
171
     });
190
 
172
 
191
     testWidgets('pick from gallery', (tester) async {
173
     testWidgets('pick from gallery', (tester) async {
192
-      final editor = new EditorSandBox(tester: tester);
174
+      final editor = new EditorSandBox(
175
+        tester: tester,
176
+        imageDelegate: _TestImageDelegate(),
177
+      );
193
       await editor.pumpAndTap();
178
       await editor.pumpAndTap();
194
       await editor.tapButtonWithIcon(Icons.photo);
179
       await editor.tapButtonWithIcon(Icons.photo);
195
       await editor.tapButtonWithIcon(Icons.photo_library);
180
       await editor.tapButtonWithIcon(Icons.photo_library);
196
-      expect(log, hasLength(1));
197
-      expect(
198
-        log.single,
199
-        isMethodCall(
200
-          'pickImage',
201
-          arguments: <String, dynamic>{
202
-            'source': 1,
203
-            'maxWidth': null,
204
-            'maxHeight': null,
205
-            'imageQuality': null,
206
-          },
207
-        ),
208
-      );
181
+      expect(find.byType(ZefyrImage), findsOneWidget);
209
     });
182
     });
210
   });
183
   });
211
 }
184
 }
185
+
186
+class _TestImageDelegate implements ZefyrImageDelegate<String> {
187
+  @override
188
+  Widget buildImage(BuildContext context, String key) {
189
+    return Image.file(File(key));
190
+  }
191
+
192
+  @override
193
+  String get cameraSource => "camera";
194
+
195
+  @override
196
+  String get gallerySource => "gallery";
197
+
198
+  @override
199
+  Future<String> pickImage(String source) {
200
+    return Future.value("file:///tmp/test.jpg");
201
+  }
202
+}

+ 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
 }

+ 38
- 43
packages/zefyr/test/widgets/image_test.dart View File

1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
 // for details. All rights reserved. Use of this source code is governed by a
2
 // for details. All rights reserved. Use of this source code is governed by a
3
 // BSD-style license that can be found in the LICENSE file.
3
 // BSD-style license that can be found in the LICENSE file.
4
+import 'dart:io';
5
+
4
 import 'package:flutter/material.dart';
6
 import 'package:flutter/material.dart';
5
-import 'package:flutter/services.dart';
6
 import 'package:flutter_test/flutter_test.dart';
7
 import 'package:flutter_test/flutter_test.dart';
7
-import 'package:image_picker/image_picker.dart';
8
 import 'package:zefyr/zefyr.dart';
8
 import 'package:zefyr/zefyr.dart';
9
 
9
 
10
 import '../testing.dart';
10
 import '../testing.dart';
11
 
11
 
12
 void main() {
12
 void main() {
13
-  group('$ZefyrDefaultImageDelegate', () {
14
-    const MethodChannel channel =
15
-        const MethodChannel('plugins.flutter.io/image_picker');
16
-
17
-    final List<MethodCall> log = <MethodCall>[];
18
-
19
-    setUp(() {
20
-      channel.setMockMethodCallHandler((MethodCall methodCall) async {
21
-        log.add(methodCall);
22
-        return '/tmp/test.jpg';
23
-      });
24
-      log.clear();
25
-    });
26
-
27
-    test('pick image', () async {
28
-      final delegate = new ZefyrDefaultImageDelegate();
29
-      final result = await delegate.pickImage(ImageSource.gallery);
30
-      expect(result, 'file:///tmp/test.jpg');
31
-    });
32
-  });
33
-
34
   group('$ZefyrImage', () {
13
   group('$ZefyrImage', () {
35
-    const MethodChannel channel =
36
-        const MethodChannel('plugins.flutter.io/image_picker');
37
-
38
-    final List<MethodCall> log = <MethodCall>[];
39
-
40
-    setUp(() {
41
-      channel.setMockMethodCallHandler((MethodCall methodCall) async {
42
-        log.add(methodCall);
43
-        return '/tmp/test.jpg';
44
-      });
45
-      log.clear();
46
-    });
47
-
48
     testWidgets('embed image', (tester) async {
14
     testWidgets('embed image', (tester) async {
49
-      final editor = new EditorSandBox(tester: tester);
15
+      final editor = new EditorSandBox(
16
+        tester: tester,
17
+        imageDelegate: _TestImageDelegate(),
18
+      );
50
       await editor.pumpAndTap();
19
       await editor.pumpAndTap();
51
       await editor.tapButtonWithIcon(Icons.photo);
20
       await editor.tapButtonWithIcon(Icons.photo);
52
       await editor.tapButtonWithIcon(Icons.photo_camera);
21
       await editor.tapButtonWithIcon(Icons.photo_camera);
62
 
31
 
63
     testWidgets('tap on left side of image puts caret before it',
32
     testWidgets('tap on left side of image puts caret before it',
64
         (tester) async {
33
         (tester) async {
65
-      final editor = new EditorSandBox(tester: tester);
34
+      final editor = new EditorSandBox(
35
+        tester: tester,
36
+        imageDelegate: _TestImageDelegate(),
37
+      );
66
       await editor.pumpAndTap();
38
       await editor.pumpAndTap();
67
       await editor.tapButtonWithIcon(Icons.photo);
39
       await editor.tapButtonWithIcon(Icons.photo);
68
       await editor.tapButtonWithIcon(Icons.photo_camera);
40
       await editor.tapButtonWithIcon(Icons.photo_camera);
75
       expect(editor.selection.extentOffset, embed.documentOffset);
47
       expect(editor.selection.extentOffset, embed.documentOffset);
76
     });
48
     });
77
 
49
 
78
-    testWidgets('tap right side of image puts caret after it',
79
-        (tester) async {
80
-      final editor = new EditorSandBox(tester: tester);
50
+    testWidgets('tap right side of image puts caret after it', (tester) async {
51
+      final editor = new EditorSandBox(
52
+        tester: tester,
53
+        imageDelegate: _TestImageDelegate(),
54
+      );
81
       await editor.pumpAndTap();
55
       await editor.pumpAndTap();
82
       await editor.tapButtonWithIcon(Icons.photo);
56
       await editor.tapButtonWithIcon(Icons.photo);
83
       await editor.tapButtonWithIcon(Icons.photo_camera);
57
       await editor.tapButtonWithIcon(Icons.photo_camera);
93
     });
67
     });
94
 
68
 
95
     testWidgets('selects on long press', (tester) async {
69
     testWidgets('selects on long press', (tester) async {
96
-      final editor = new EditorSandBox(tester: tester);
70
+      final editor = new EditorSandBox(
71
+        tester: tester,
72
+        imageDelegate: _TestImageDelegate(),
73
+      );
97
       await editor.pumpAndTap();
74
       await editor.pumpAndTap();
98
       await editor.tapButtonWithIcon(Icons.photo);
75
       await editor.tapButtonWithIcon(Icons.photo);
99
       await editor.tapButtonWithIcon(Icons.photo_camera);
76
       await editor.tapButtonWithIcon(Icons.photo_camera);
105
       EmbedNode embed = line.children.single;
82
       EmbedNode embed = line.children.single;
106
       expect(editor.selection.baseOffset, embed.documentOffset);
83
       expect(editor.selection.baseOffset, embed.documentOffset);
107
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
84
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
108
-      expect(find.text('Paste'), findsOneWidget);
85
+      expect(find.text('PASTE'), findsOneWidget);
109
     });
86
     });
110
   });
87
   });
111
 }
88
 }
89
+
90
+class _TestImageDelegate implements ZefyrImageDelegate<String> {
91
+  @override
92
+  Widget buildImage(BuildContext context, String key) {
93
+    return Image.file(File(key));
94
+  }
95
+
96
+  @override
97
+  String get cameraSource => "camera";
98
+
99
+  @override
100
+  String get gallerySource => "gallery";
101
+
102
+  @override
103
+  Future<String> pickImage(String source) {
104
+    return Future.value("file:///tmp/test.jpg");
105
+  }
106
+}

+ 20
- 2
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
         focusNode: FocusNode(),
19
         focusNode: FocusNode(),
20
         focusScope: FocusScopeNode(),
20
         focusScope: FocusScopeNode(),
21
       );
21
       );
26
       scope.addListener(() {
26
       scope.addListener(() {
27
         notified = true;
27
         notified = true;
28
       });
28
       });
29
-      final delegate = ZefyrDefaultImageDelegate();
29
+      final delegate = _TestImageDelegate();
30
       scope.imageDelegate = delegate;
30
       scope.imageDelegate = delegate;
31
       expect(notified, isTrue);
31
       expect(notified, isTrue);
32
       notified = false;
32
       notified = false;
73
     });
73
     });
74
   });
74
   });
75
 }
75
 }
76
+
77
+class _TestImageDelegate implements ZefyrImageDelegate<String> {
78
+  @override
79
+  Widget buildImage(BuildContext context, String key) {
80
+    return null;
81
+  }
82
+
83
+  @override
84
+  String get cameraSource => null;
85
+
86
+  @override
87
+  String get gallerySource => null;
88
+
89
+  @override
90
+  Future<String> pickImage(String source) {
91
+    return null;
92
+  }
93
+}

+ 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 {