Parcourir la source

merged in master

Anatoly Pulyaevskiy il y a 5 ans
Parent
révision
b40428af97
39 fichiers modifiés avec 751 ajouts et 436 suppressions
  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 Voir le fichier

@@ -0,0 +1,4 @@
1
+root: ./doc/
2
+
3
+structure:
4
+  readme: ./../README.md

+ 3
- 13
.travis.yml Voir le fichier

@@ -14,24 +14,14 @@ addons:
14 14
       - ubuntu-toolchain-r-test
15 15
     packages:
16 16
       - libstdc++6
17
-      - fonts-droid
17
+      - fonts-droid-fallback
18 18
 
19 19
 cache:
20 20
   directories:
21 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 26
 before_script:
37 27
   - pwd

+ 82
- 22
doc/images.md Voir le fichier

@@ -1,40 +1,54 @@
1 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 4
 > changed in backward incompatible ways. If this happens all changes will be
5 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 8
 your application you need to implement `ZefyrImageDelegate` interface which
9 9
 looks like this:
10 10
 
11 11
 ```dart
12 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 25
   /// Picks an image from specified [source].
17 26
   ///
18 27
   /// Returns unique string key for the selected image. Returned key is stored
19 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 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 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 49
 Let's start from the `pickImage` method:
33 50
 
34 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 52
 import 'package:image_picker/image_picker.dart';
39 53
 
40 54
 class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
@@ -53,17 +67,17 @@ camera or gallery), handling result of selection and returning a string value
53 67
 which essentially serves as an identifier for the image.
54 68
 
55 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 72
 It is up to the developer to define what this value represents.
59 73
 
60 74
 In the above example we simply return a full path to the file on user's device,
61 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 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 82
 ```dart
69 83
 class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
@@ -84,7 +98,7 @@ class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
84 98
 
85 99
 Next we need to implement `buildImage`. This method takes `imageSource` argument
86 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 102
 you would return the standard `Image` widget from this method, but it is not
89 103
 a requirement. You are free to create a custom widget which, for instance,
90 104
 shows progress of upload operation that you initiated in the `pickImage` call.
@@ -97,12 +111,58 @@ class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
97 111
   // ...
98 112
 
99 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 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,4 +171,4 @@ class MyAppZefyrImageDelegate implements ZefyrImageDelegate<ImageSource> {
111 171
 
112 172
 * [Heuristics][heuristics]
113 173
 
114
-[heuristics]: /doc/heuristics.md
174
+[heuristics]: /doc/heuristics.md

+ 21
- 0
packages/zefyr/CHANGELOG.md Voir le fichier

@@ -1,3 +1,24 @@
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 22
 ## 0.6.1
2 23
 
3 24
 * Relaxed dependency constraint on `image_picker` library to allow latest version. Note that

+ 5
- 0
packages/zefyr/example/lib/src/form.dart Voir le fichier

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

+ 7
- 18
packages/zefyr/example/lib/src/full_page.dart Voir le fichier

@@ -1,3 +1,7 @@
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 5
 import 'dart:async';
2 6
 import 'dart:convert';
3 7
 
@@ -5,6 +9,8 @@ import 'package:flutter/material.dart';
5 9
 import 'package:quill_delta/quill_delta.dart';
6 10
 import 'package:zefyr/zefyr.dart';
7 11
 
12
+import 'images.dart';
13
+
8 14
 class ZefyrLogo extends StatelessWidget {
9 15
   @override
10 16
   Widget build(BuildContext context) {
@@ -86,7 +92,7 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
86 92
           child: ZefyrEditor(
87 93
             controller: _controller,
88 94
             focusNode: _focusNode,
89
-            enabled: _editing,
95
+            mode: _editing ? ZefyrMode.edit : ZefyrMode.select,
90 96
             imageDelegate: new CustomImageDelegate(),
91 97
           ),
92 98
         ),
@@ -106,20 +112,3 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
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 Voir le fichier

@@ -0,0 +1,40 @@
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 Voir le fichier

@@ -1,3 +1,7 @@
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 5
 import 'dart:convert';
2 6
 
3 7
 import 'package:flutter/material.dart';
@@ -5,6 +9,7 @@ import 'package:quill_delta/quill_delta.dart';
5 9
 import 'package:zefyr/zefyr.dart';
6 10
 
7 11
 import 'full_page.dart';
12
+import 'images.dart';
8 13
 
9 14
 class ViewScreen extends StatefulWidget {
10 15
   @override
@@ -68,20 +73,3 @@ class _ViewScreen extends State<ViewScreen> {
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 Voir le fichier

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

+ 12
- 11
packages/zefyr/lib/src/widgets/buttons.dart Voir le fichier

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

+ 2
- 2
packages/zefyr/lib/src/widgets/caret.dart Voir le fichier

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

+ 4
- 4
packages/zefyr/lib/src/widgets/code.dart Voir le fichier

@@ -23,16 +23,16 @@ class ZefyrCode extends StatelessWidget {
23 23
       items.add(_buildLine(line, theme.blockTheme.code.textStyle));
24 24
     }
25 25
 
26
-    return new Padding(
26
+    return Padding(
27 27
       padding: theme.blockTheme.code.padding,
28
-      child: new Container(
28
+      child: Container(
29 29
         // TODO: make decorations configurable
30 30
         decoration: BoxDecoration(
31 31
           color: Colors.blueGrey.shade50,
32 32
           borderRadius: BorderRadius.circular(3.0),
33 33
         ),
34 34
         padding: const EdgeInsets.all(16.0),
35
-        child: new Column(
35
+        child: Column(
36 36
           crossAxisAlignment: CrossAxisAlignment.stretch,
37 37
           children: items,
38 38
         ),
@@ -42,6 +42,6 @@ class ZefyrCode extends StatelessWidget {
42 42
 
43 43
   Widget _buildLine(Node node, TextStyle style) {
44 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 Voir le fichier

@@ -35,11 +35,11 @@ class RawZefyrLine extends StatefulWidget {
35 35
   final EdgeInsets padding;
36 36
 
37 37
   @override
38
-  _RawZefyrLineState createState() => new _RawZefyrLineState();
38
+  _RawZefyrLineState createState() => _RawZefyrLineState();
39 39
 }
40 40
 
41 41
 class _RawZefyrLineState extends State<RawZefyrLine> {
42
-  final LayerLink _link = new LayerLink();
42
+  final LayerLink _link = LayerLink();
43 43
 
44 44
   @override
45 45
   Widget build(BuildContext context) {
@@ -113,21 +113,21 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
113 113
     final List<TextSpan> children = widget.node.children
114 114
         .map((node) => _segmentToTextSpan(node, theme))
115 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 119
   TextSpan _segmentToTextSpan(Node node, ZefyrThemeData theme) {
120 120
     final TextNode segment = node;
121 121
     final attrs = segment.style;
122 122
 
123
-    return new TextSpan(
123
+    return TextSpan(
124 124
       text: segment.value,
125 125
       style: _getTextStyle(attrs, theme),
126 126
     );
127 127
   }
128 128
 
129 129
   TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) {
130
-    TextStyle result = new TextStyle();
130
+    TextStyle result = TextStyle();
131 131
     if (style.containsSame(NotusAttribute.bold)) {
132 132
       result = result.merge(theme.boldStyle);
133 133
     }
@@ -149,7 +149,7 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
149 149
     } else if (embed.type == EmbedType.image) {
150 150
       return ZefyrImage(node: node, delegate: scope.imageDelegate);
151 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 Voir le fichier

@@ -98,6 +98,13 @@ class ZefyrController extends ChangeNotifier {
98 98
     notifyListeners();
99 99
   }
100 100
 
101
+  /// Replaces [length] characters in the document starting at [index] with
102
+  /// provided [text].
103
+  ///
104
+  /// Resulting change is registered as produced by user action, e.g.
105
+  /// using [ChangeSource.local].
106
+  ///
107
+  /// Optionally updates selection if provided.
101 108
   void replaceText(int index, int length, String text,
102 109
       {TextSelection selection}) {
103 110
     Delta delta;
@@ -112,7 +119,7 @@ class ZefyrController extends ChangeNotifier {
112 119
       } else {
113 120
         // need to transform selection position in case actual delta
114 121
         // is different from user's version (in deletes and inserts).
115
-        Delta user = new Delta()
122
+        Delta user = Delta()
116 123
           ..retain(index)
117 124
           ..insert(text)
118 125
           ..delete(length);
@@ -137,8 +144,7 @@ class ZefyrController extends ChangeNotifier {
137 144
     // the change. This is needed in cases when format operation actually
138 145
     // inserts data into the document (e.g. embeds).
139 146
     final base = change.transformPosition(_selection.baseOffset);
140
-    final extent =
141
-        change.transformPosition(_selection.extentOffset);
147
+    final extent = change.transformPosition(_selection.extentOffset);
142 148
     final adjustedSelection =
143 149
         _selection.copyWith(baseOffset: base, extentOffset: extent);
144 150
     if (_selection != adjustedSelection) {
@@ -161,10 +167,10 @@ class ZefyrController extends ChangeNotifier {
161 167
   }
162 168
 
163 169
   TextEditingValue get plainTextEditingValue {
164
-    return new TextEditingValue(
170
+    return TextEditingValue(
165 171
       text: document.toPlainText(),
166 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 Voir le fichier

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

+ 57
- 27
packages/zefyr/lib/src/widgets/editable_text.dart Voir le fichier

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

+ 44
- 8
packages/zefyr/lib/src/widgets/editor.dart Voir le fichier

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

+ 5
- 4
packages/zefyr/lib/src/widgets/field.dart Voir le fichier

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

+ 35
- 37
packages/zefyr/lib/src/widgets/image.dart Voir le fichier

@@ -2,44 +2,43 @@
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4 4
 import 'dart:async';
5
-import 'dart:io';
6 5
 import 'dart:math' as math;
7 6
 import 'dart:ui' as ui;
8 7
 
9 8
 import 'package:flutter/rendering.dart';
10 9
 import 'package:flutter/widgets.dart';
10
+import 'package:meta/meta.dart';
11 11
 import 'package:notus/notus.dart';
12
-import 'package:image_picker/image_picker.dart';
13 12
 
14 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 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 31
   /// Picks an image from specified [source].
21 32
   ///
22 33
   /// Returns unique string key for the selected image. Returned key is stored
23 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 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 42
 class ZefyrImage extends StatefulWidget {
44 43
   const ZefyrImage({Key key, @required this.node, @required this.delegate})
45 44
       : super(key: key);
@@ -76,7 +75,7 @@ class _EditableImage extends SingleChildRenderObjectWidget {
76 75
 
77 76
   @override
78 77
   RenderEditableImage createRenderObject(BuildContext context) {
79
-    return new RenderEditableImage(node: node);
78
+    return RenderEditableImage(node: node);
80 79
   }
81 80
 
82 81
   @override
@@ -130,16 +129,16 @@ class RenderEditableImage extends RenderBox
130 129
     if (local.isCollapsed) {
131 130
       final dx = local.extentOffset == 0 ? _childOffset.dx : size.width;
132 131
       return [
133
-        new ui.TextBox.fromLTRBD(
132
+        ui.TextBox.fromLTRBD(
134 133
             dx, 0.0, dx, size.height - kPaddingBottom, TextDirection.ltr),
135 134
       ];
136 135
     }
137 136
 
138 137
     final rect = _childRect;
139 138
     return [
140
-      new ui.TextBox.fromLTRBD(
139
+      ui.TextBox.fromLTRBD(
141 140
           rect.left, rect.top, rect.left, rect.bottom, TextDirection.ltr),
142
-      new ui.TextBox.fromLTRBD(
141
+      ui.TextBox.fromLTRBD(
143 142
           rect.right, rect.top, rect.right, rect.bottom, TextDirection.ltr),
144 143
     ];
145 144
   }
@@ -151,13 +150,13 @@ class RenderEditableImage extends RenderBox
151 150
     if (offset.dx > size.width / 2) {
152 151
       position++;
153 152
     }
154
-    return new TextPosition(offset: position);
153
+    return TextPosition(offset: position);
155 154
   }
156 155
 
157 156
   @override
158 157
   TextRange getWordBoundary(TextPosition position) {
159 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 162
   @override
@@ -170,10 +169,10 @@ class RenderEditableImage extends RenderBox
170 169
   @override
171 170
   Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
172 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 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 177
     return caretOffset;
179 178
   }
@@ -184,12 +183,12 @@ class RenderEditableImage extends RenderBox
184 183
     final localSelection = getLocalSelection(selection);
185 184
     assert(localSelection != null);
186 185
     if (!localSelection.isCollapsed) {
187
-      final Paint paint = new Paint()
186
+      final Paint paint = Paint()
188 187
         ..color = selectionColor
189 188
         ..style = PaintingStyle.stroke
190 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 192
       context.canvas.drawRect(rect.shift(offset + _childOffset), paint);
194 193
     }
195 194
   }
@@ -205,12 +204,12 @@ class RenderEditableImage extends RenderBox
205 204
   Offset get _childOffset {
206 205
     final dx = (size.width - _lastChildSize.width) / 2 + kHorizontalPadding;
207 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 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 215
   @override
@@ -227,8 +226,7 @@ class RenderEditableImage extends RenderBox
227 226
       );
228 227
       child.layout(childConstraints, parentUsesSize: true);
229 228
       _lastChildSize = child.size;
230
-      size = new Size(
231
-          constraints.maxWidth, _lastChildSize.height + kPaddingBottom);
229
+      size = Size(constraints.maxWidth, _lastChildSize.height + kPaddingBottom);
232 230
     } else {
233 231
       performResize();
234 232
     }

+ 1
- 1
packages/zefyr/lib/src/widgets/input.dart Voir le fichier

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

+ 5
- 5
packages/zefyr/lib/src/widgets/list.dart Voir le fichier

@@ -31,15 +31,15 @@ class ZefyrList extends StatelessWidget {
31 31
         : theme.blockTheme.bulletList.padding;
32 32
     padding = padding.copyWith(left: theme.indentSize);
33 33
 
34
-    return new Padding(
34
+    return Padding(
35 35
       padding: padding,
36
-      child: new Column(children: items),
36
+      child: Column(children: items),
37 37
     );
38 38
   }
39 39
 
40 40
   Widget _buildItem(Node node, int index) {
41 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,10 +66,10 @@ class ZefyrListItem extends StatelessWidget {
66 66
       final headingTheme = ZefyrHeading.themeOf(node, context);
67 67
       textStyle = headingTheme.textStyle;
68 68
       padding = headingTheme.padding;
69
-      content = new ZefyrHeading(node: node);
69
+      content = ZefyrHeading(node: node);
70 70
     } else {
71 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 75
     Widget bullet =

+ 61
- 0
packages/zefyr/lib/src/widgets/mode.dart Voir le fichier

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

+ 3
- 3
packages/zefyr/lib/src/widgets/paragraph.dart Voir le fichier

@@ -22,7 +22,7 @@ class ZefyrParagraph extends StatelessWidget {
22 22
     if (blockStyle != null) {
23 23
       style = style.merge(blockStyle);
24 24
     }
25
-    return new RawZefyrLine(
25
+    return RawZefyrLine(
26 26
       node: node,
27 27
       style: style,
28 28
       padding: theme.paragraphTheme.padding,
@@ -46,7 +46,7 @@ class ZefyrHeading extends StatelessWidget {
46 46
     if (blockStyle != null) {
47 47
       style = style.merge(blockStyle);
48 48
     }
49
-    return new RawZefyrLine(
49
+    return RawZefyrLine(
50 50
       node: node,
51 51
       style: style,
52 52
       padding: theme.padding,
@@ -63,6 +63,6 @@ class ZefyrHeading extends StatelessWidget {
63 63
     } else if (style == NotusAttribute.heading.level3) {
64 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 Voir le fichier

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

+ 3
- 0
packages/zefyr/lib/src/widgets/scaffold.dart Voir le fichier

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

+ 19
- 7
packages/zefyr/lib/src/widgets/scope.dart Voir le fichier

@@ -6,6 +6,7 @@ import 'controller.dart';
6 6
 import 'cursor_timer.dart';
7 7
 import 'editor.dart';
8 8
 import 'image.dart';
9
+import 'mode.dart';
9 10
 import 'render_context.dart';
10 11
 import 'view.dart';
11 12
 
@@ -24,24 +25,26 @@ class ZefyrScope extends ChangeNotifier {
24 25
   /// Creates a view-only scope.
25 26
   ///
26 27
   /// Normally used in [ZefyrView].
27
-  ZefyrScope.view({@required ZefyrImageDelegate imageDelegate})
28
-      : assert(imageDelegate != null),
29
-        isEditable = false,
28
+  ZefyrScope.view({ZefyrImageDelegate imageDelegate})
29
+      : isEditable = false,
30
+        _mode = ZefyrMode.view,
30 31
         _imageDelegate = imageDelegate;
31 32
 
32 33
   /// Creates editable scope.
33 34
   ///
34 35
   /// Normally used in [ZefyrEditor].
35 36
   ZefyrScope.editable({
37
+    @required ZefyrMode mode,
36 38
     @required ZefyrController controller,
37
-    @required ZefyrImageDelegate imageDelegate,
38 39
     @required FocusNode focusNode,
39 40
     @required FocusScopeNode focusScope,
40
-  })  : assert(controller != null),
41
-        assert(imageDelegate != null),
41
+    ZefyrImageDelegate imageDelegate,
42
+  })  : assert(mode != null),
43
+        assert(controller != null),
42 44
         assert(focusNode != null),
43 45
         assert(focusScope != null),
44 46
         isEditable = true,
47
+        _mode = mode,
45 48
         _controller = controller,
46 49
         _imageDelegate = imageDelegate,
47 50
         _focusNode = focusNode,
@@ -63,13 +66,22 @@ class ZefyrScope extends ChangeNotifier {
63 66
   ZefyrImageDelegate _imageDelegate;
64 67
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
65 68
   set imageDelegate(ZefyrImageDelegate value) {
66
-    assert(value != null);
67 69
     if (_imageDelegate != value) {
68 70
       _imageDelegate = value;
69 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 85
   ZefyrController _controller;
74 86
   ZefyrController get controller => _controller;
75 87
   set controller(ZefyrController value) {

+ 134
- 122
packages/zefyr/lib/src/widgets/selection.dart Voir le fichier

@@ -25,34 +25,73 @@ RenderEditableBox _getEditableBox(HitTestResult result) {
25 25
 
26 26
 /// Selection overlay controls selection handles and other gestures.
27 27
 class ZefyrSelectionOverlay extends StatefulWidget {
28
-  const ZefyrSelectionOverlay({
29
-    Key key,
30
-    @required this.controller,
31
-    @required this.controls,
32
-    @required this.overlay,
33
-  }) : super(key: key);
28
+  const ZefyrSelectionOverlay({Key key, @required this.controls})
29
+      : super(key: key);
34 30
 
35
-  final ZefyrController controller;
36 31
   final TextSelectionControls controls;
37
-  final OverlayState overlay;
38 32
 
39 33
   @override
40
-  _ZefyrSelectionOverlayState createState() =>
41
-      new _ZefyrSelectionOverlayState();
34
+  _ZefyrSelectionOverlayState createState() => _ZefyrSelectionOverlayState();
42 35
 }
43 36
 
44 37
 class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
45 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 85
   @override
47 86
   TextEditingValue get textEditingValue =>
48
-      widget.controller.plainTextEditingValue;
87
+      _scope.controller.plainTextEditingValue;
49 88
 
50 89
   set textEditingValue(TextEditingValue value) {
51 90
     final cursorPosition = value.selection.extentOffset;
52
-    final oldText = widget.controller.document.toPlainText();
91
+    final oldText = _scope.controller.document.toPlainText();
53 92
     final newText = value.text;
54 93
     final diff = fastDiff(oldText, newText, cursorPosition);
55
-    widget.controller.replaceText(
94
+    _scope.controller.replaceText(
56 95
         diff.start, diff.deleted.length, diff.inserted,
57 96
         selection: value.selection);
58 97
   }
@@ -62,73 +101,60 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
62 101
     // TODO: implement bringIntoView
63 102
   }
64 103
 
65
-  bool get isToolbarVisible => _toolbar != null;
66
-  bool get isToolbarHidden => _toolbar == null;
67
-
68 104
   @override
69 105
   void hideToolbar() {
70 106
     _didCaretTap = false; // reset double tap.
71 107
     _toolbar?.remove();
72 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 114
   @override
97 115
   void initState() {
98 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 120
   @override
106 121
   void didUpdateWidget(ZefyrSelectionOverlay oldWidget) {
107 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 126
   @override
117 127
   void didChangeDependencies() {
118 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 155
   @override
130 156
   void dispose() {
131
-    _editor.removeListener(_handleChange);
157
+    _scope.removeListener(_handleChange);
132 158
     hideToolbar();
133 159
     _toolbarController.dispose();
134 160
     _toolbarController = null;
@@ -137,50 +163,35 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
137 163
 
138 164
   @override
139 165
   Widget build(BuildContext context) {
140
-    final overlay = new GestureDetector(
166
+    final overlay = GestureDetector(
141 167
       behavior: HitTestBehavior.translucent,
142 168
       onTapDown: _handleTapDown,
143 169
       onTap: _handleTap,
144 170
       onTapCancel: _handleTapCancel,
145 171
       onLongPress: _handleLongPress,
146
-      child: new Stack(
172
+      child: Stack(
147 173
         fit: StackFit.expand,
148 174
         children: <Widget>[
149
-          new SelectionHandleDriver(
175
+          SelectionHandleDriver(
150 176
             position: _SelectionHandlePosition.base,
151
-            controls: widget.controls,
177
+            selectionOverlay: this,
152 178
           ),
153
-          new SelectionHandleDriver(
179
+          SelectionHandleDriver(
154 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 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 193
   void _handleChange() {
183
-    if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) {
194
+    if (_selection != _scope.selection || _focusOwner != _scope.focusOwner) {
184 195
       _updateToolbar();
185 196
     }
186 197
   }
@@ -190,14 +201,16 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
190 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 206
     setState(() {
196
-      if (focusOwner != FocusOwner.editor) {
207
+      if (shouldHideControls && isToolbarVisible) {
197 208
         hideToolbar();
198 209
       } else {
199 210
         if (_selection != selection) {
200
-          if (selection.isCollapsed && isToolbarVisible) hideToolbar();
211
+          if (selection.isCollapsed && isToolbarVisible) {
212
+            hideToolbar();
213
+          }
201 214
           _toolbar?.markNeedsBuild();
202 215
           if (!selection.isCollapsed && isToolbarHidden) showToolbar();
203 216
         } else {
@@ -227,18 +240,18 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
227 240
     assert(_lastTapDownPosition != null);
228 241
     final globalPoint = _lastTapDownPosition;
229 242
     _lastTapDownPosition = null;
230
-    HitTestResult result = new HitTestResult();
243
+    HitTestResult result = HitTestResult();
231 244
     WidgetsBinding.instance.hitTest(result, globalPoint);
232 245
 
233 246
     RenderEditableProxyBox box = _getEditableBox(result);
234 247
     if (box == null) {
235
-      box = _editor.renderContext.closestBoxForGlobalPoint(globalPoint);
248
+      box = _scope.renderContext.closestBoxForGlobalPoint(globalPoint);
236 249
     }
237 250
     if (box == null) return null;
238 251
 
239 252
     final localPoint = box.globalToLocal(globalPoint);
240 253
     final position = box.getPositionForOffset(localPoint);
241
-    final selection = new TextSelection.collapsed(
254
+    final selection = TextSelection.collapsed(
242 255
       offset: position.offset,
243 256
       affinity: position.affinity,
244 257
     );
@@ -252,13 +265,13 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
252 265
     } else {
253 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 271
   void _handleLongPress() {
259 272
     final Offset globalPoint = _longPressPosition;
260 273
     _longPressPosition = null;
261
-    HitTestResult result = new HitTestResult();
274
+    HitTestResult result = HitTestResult();
262 275
     WidgetsBinding.instance.hitTest(result, globalPoint);
263 276
     final box = _getEditableBox(result);
264 277
     if (box == null) {
@@ -267,25 +280,24 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
267 280
     final localPoint = box.globalToLocal(globalPoint);
268 281
     final position = box.getPositionForOffset(localPoint);
269 282
     final word = box.getWordBoundary(position);
270
-    final selection = new TextSelection(
283
+    final selection = TextSelection(
271 284
       baseOffset: word.start,
272 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 290
   @override
279
-  bool get copyEnabled => _editor.isEditable;
291
+  bool get copyEnabled => _scope.mode.canSelect && !_selection.isCollapsed;
280 292
 
281 293
   @override
282
-  bool get cutEnabled => _editor.isEditable;
294
+  bool get cutEnabled => _scope.mode.canEdit && !_selection.isCollapsed;
283 295
 
284 296
   @override
285
-  bool get pasteEnabled => _editor.isEditable;
297
+  bool get pasteEnabled => _scope.mode.canEdit;
286 298
 
287 299
   @override
288
-  bool get selectAllEnabled => _editor.isEditable;
300
+  bool get selectAllEnabled => _scope.mode.canSelect;
289 301
 }
290 302
 
291 303
 enum _SelectionHandlePosition { base, extent }
@@ -294,15 +306,15 @@ class SelectionHandleDriver extends StatefulWidget {
294 306
   const SelectionHandleDriver({
295 307
     Key key,
296 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 313
   final _SelectionHandlePosition position;
301
-  final TextSelectionControls controls;
314
+  final _ZefyrSelectionOverlayState selectionOverlay;
302 315
 
303 316
   @override
304
-  _SelectionHandleDriverState createState() =>
305
-      new _SelectionHandleDriverState();
317
+  _SelectionHandleDriverState createState() => _SelectionHandleDriverState();
306 318
 }
307 319
 
308 320
 class _SelectionHandleDriverState extends State<SelectionHandleDriver>
@@ -361,11 +373,8 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
361 373
 
362 374
   @override
363 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 379
     final block = _scope.renderContext.boxForTextOffset(documentOffset);
371 380
     if (block == null) {
@@ -404,11 +413,12 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
404 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 418
       type,
409 419
       block.preferredLineHeight,
410 420
     );
411
-    final Size handleSize = widget.controls.getHandleSize(
421
+    final Size handleSize = widget.selectionOverlay.controls.getHandleSize(
412 422
       block.preferredLineHeight,
413 423
     );
414 424
     final Rect handleRect = Rect.fromLTWH(
@@ -451,7 +461,7 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
451 461
               right: padding.right,
452 462
               bottom: padding.bottom,
453 463
             ),
454
-            child: widget.controls.buildHandle(
464
+            child: widget.selectionOverlay.controls.buildHandle(
455 465
               context,
456 466
               type,
457 467
               block.preferredLineHeight,
@@ -525,22 +535,20 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
525 535
 class _SelectionToolbar extends StatefulWidget {
526 536
   const _SelectionToolbar({
527 537
     Key key,
528
-    @required this.scope,
529
-    @required this.controls,
530
-    @required this.delegate,
538
+    @required this.selectionOverlay,
531 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 543
   @override
538
-  _SelectionToolbarState createState() => new _SelectionToolbarState();
544
+  _SelectionToolbarState createState() => _SelectionToolbarState();
539 545
 }
540 546
 
541 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 553
   @override
546 554
   Widget build(BuildContext context) {
@@ -549,14 +557,13 @@ class _SelectionToolbarState extends State<_SelectionToolbar> {
549 557
 
550 558
   Widget _buildToolbar(BuildContext context) {
551 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 561
     if (block == null) {
555 562
       return Container();
556 563
     }
557 564
     final boxes = block.getEndpointsForSelection(selection);
558 565
     // Find the horizontal midpoint, just above the selected text.
559
-    Offset midpoint = new Offset(
566
+    Offset midpoint = Offset(
560 567
       (boxes.length == 1)
561 568
           ? (boxes[0].start + boxes[0].end) / 2.0
562 569
           : (boxes[0].start + boxes[1].start) / 2.0,
@@ -579,14 +586,19 @@ class _SelectionToolbarState extends State<_SelectionToolbar> {
579 586
       ];
580 587
     }
581 588
 
582
-    final Rect editingRegion = new Rect.fromPoints(
589
+    final Rect editingRegion = Rect.fromPoints(
583 590
       block.localToGlobal(Offset.zero),
584 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 602
       link: block.layerLink,
591 603
       showWhenUnlinked: false,
592 604
       offset: -editingRegion.topLeft,

+ 10
- 10
packages/zefyr/lib/src/widgets/toolbar.dart Voir le fichier

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

+ 3
- 0
packages/zefyr/lib/src/widgets/view.dart Voir le fichier

@@ -1,3 +1,6 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
1 4
 import 'package:flutter/material.dart';
2 5
 import 'package:meta/meta.dart';
3 6
 import 'package:notus/notus.dart';

+ 3
- 3
packages/zefyr/lib/util.dart Voir le fichier

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

+ 1
- 0
packages/zefyr/lib/zefyr.dart Voir le fichier

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

+ 3
- 3
packages/zefyr/pubspec.yaml Voir le fichier

@@ -1,21 +1,21 @@
1 1
 name: zefyr
2 2
 description: Clean, minimalistic and collaboration-ready rich text editor for Flutter.
3
-version: 0.6.1
3
+version: 0.7.0
4 4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
5 5
 homepage: https://github.com/memspace/zefyr
6 6
 
7 7
 environment:
8
-  sdk: '>=2.1.0 <3.0.0'
8
+  sdk: '>=2.2.2 <3.0.0'
9 9
 
10 10
 dependencies:
11 11
   flutter:
12 12
     sdk: flutter
13 13
   collection: ^1.14.6
14 14
   url_launcher: ^5.0.0
15
-  image_picker: '>=0.5.0 <0.7.0'
16 15
   quill_delta: ^1.0.0-dev.1.0
17 16
   notus: ^0.1.0
18 17
   meta: ^1.1.0
18
+  quiver_hashcode: ^2.0.0
19 19
 
20 20
 dev_dependencies:
21 21
   flutter_test:

+ 41
- 0
packages/zefyr/test/rendering/render_zefyr_paragraph_test.dart Voir le fichier

@@ -0,0 +1,41 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:ui';
5
+
6
+import 'package:flutter/material.dart';
7
+import 'package:flutter/rendering.dart';
8
+import 'package:flutter_test/flutter_test.dart';
9
+import 'package:zefyr/src/widgets/render_context.dart';
10
+import 'package:zefyr/src/widgets/rich_text.dart';
11
+import 'package:zefyr/zefyr.dart';
12
+
13
+void main() {
14
+  group('$RenderZefyrParagraph', () {
15
+    final doc = new NotusDocument();
16
+    doc.insert(0, 'This House Is A Circus');
17
+    final text = new TextSpan(text: 'This House Is A Circus');
18
+
19
+    ZefyrRenderContext renderContext;
20
+    RenderZefyrParagraph p;
21
+
22
+    setUp(() {
23
+      WidgetsFlutterBinding.ensureInitialized();
24
+      renderContext = new ZefyrRenderContext();
25
+      p = new RenderZefyrParagraph(
26
+        text,
27
+        node: doc.root.children.first as LineNode,
28
+        textDirection: TextDirection.ltr,
29
+      );
30
+    });
31
+
32
+    test('it registers with viewport', () {
33
+      var owner = new PipelineOwner();
34
+      expect(renderContext.active, isNot(contains(p)));
35
+      p.attach(owner);
36
+      expect(renderContext.dirty, contains(p));
37
+      p.layout(new BoxConstraints());
38
+      expect(renderContext.active, contains(p));
39
+    }, skip: 'TODO: move to RenderEditableProxyBox');
40
+  });
41
+}

+ 12
- 4
packages/zefyr/test/testing.dart Voir le fichier

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

+ 34
- 43
packages/zefyr/test/widgets/buttons_test.dart Voir le fichier

@@ -1,8 +1,9 @@
1 1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4
+import 'dart:io';
5
+
4 6
 import 'package:flutter/material.dart';
5
-import 'package:flutter/services.dart';
6 7
 import 'package:flutter_test/flutter_test.dart';
7 8
 import 'package:zefyr/src/widgets/buttons.dart';
8 9
 import 'package:zefyr/zefyr.dart';
@@ -145,21 +146,11 @@ void main() {
145 146
   });
146 147
 
147 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 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 154
       await editor.pumpAndTap();
164 155
       await editor.tapButtonWithIcon(Icons.photo);
165 156
 
@@ -169,43 +160,43 @@ void main() {
169 160
     });
170 161
 
171 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 167
       await editor.pumpAndTap();
174 168
       await editor.tapButtonWithIcon(Icons.photo);
175 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 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 178
       await editor.pumpAndTap();
194 179
       await editor.tapButtonWithIcon(Icons.photo);
195 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 Voir le fichier

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

+ 38
- 43
packages/zefyr/test/widgets/image_test.dart Voir le fichier

@@ -1,52 +1,21 @@
1 1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4
+import 'dart:io';
5
+
4 6
 import 'package:flutter/material.dart';
5
-import 'package:flutter/services.dart';
6 7
 import 'package:flutter_test/flutter_test.dart';
7
-import 'package:image_picker/image_picker.dart';
8 8
 import 'package:zefyr/zefyr.dart';
9 9
 
10 10
 import '../testing.dart';
11 11
 
12 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 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 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 19
       await editor.pumpAndTap();
51 20
       await editor.tapButtonWithIcon(Icons.photo);
52 21
       await editor.tapButtonWithIcon(Icons.photo_camera);
@@ -62,7 +31,10 @@ void main() {
62 31
 
63 32
     testWidgets('tap on left side of image puts caret before it',
64 33
         (tester) async {
65
-      final editor = new EditorSandBox(tester: tester);
34
+      final editor = new EditorSandBox(
35
+        tester: tester,
36
+        imageDelegate: _TestImageDelegate(),
37
+      );
66 38
       await editor.pumpAndTap();
67 39
       await editor.tapButtonWithIcon(Icons.photo);
68 40
       await editor.tapButtonWithIcon(Icons.photo_camera);
@@ -75,9 +47,11 @@ void main() {
75 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 55
       await editor.pumpAndTap();
82 56
       await editor.tapButtonWithIcon(Icons.photo);
83 57
       await editor.tapButtonWithIcon(Icons.photo_camera);
@@ -93,7 +67,10 @@ void main() {
93 67
     });
94 68
 
95 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 74
       await editor.pumpAndTap();
98 75
       await editor.tapButtonWithIcon(Icons.photo);
99 76
       await editor.tapButtonWithIcon(Icons.photo_camera);
@@ -105,7 +82,25 @@ void main() {
105 82
       EmbedNode embed = line.children.single;
106 83
       expect(editor.selection.baseOffset, embed.documentOffset);
107 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 Voir le fichier

@@ -14,8 +14,8 @@ void main() {
14 14
       WidgetsFlutterBinding.ensureInitialized();
15 15
       final doc = NotusDocument();
16 16
       scope = ZefyrScope.editable(
17
+        mode: ZefyrMode.edit,
17 18
         controller: ZefyrController(doc),
18
-        imageDelegate: ZefyrDefaultImageDelegate(),
19 19
         focusNode: FocusNode(),
20 20
         focusScope: FocusScopeNode(),
21 21
       );
@@ -26,7 +26,7 @@ void main() {
26 26
       scope.addListener(() {
27 27
         notified = true;
28 28
       });
29
-      final delegate = ZefyrDefaultImageDelegate();
29
+      final delegate = _TestImageDelegate();
30 30
       scope.imageDelegate = delegate;
31 31
       expect(notified, isTrue);
32 32
       notified = false;
@@ -73,3 +73,21 @@ void main() {
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 Voir le fichier

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