Browse Source

Introduced ZefyrScaffold instead of putting toolbar in Overlay

Anatoly Pulyaevskiy 6 years ago
parent
commit
3345df5ec2

+ 35
- 92
packages/zefyr/example/lib/main.dart View File

@@ -1,125 +1,68 @@
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:convert';
5
-
6 4
 import 'package:flutter/material.dart';
7
-import 'package:quill_delta/quill_delta.dart';
8
-import 'package:zefyr/zefyr.dart';
5
+
6
+import 'src/full_page.dart';
7
+import 'src/form.dart';
9 8
 
10 9
 void main() {
11 10
   runApp(new ZefyrApp());
12 11
 }
13 12
 
14
-class ZefyrLogo extends StatelessWidget {
15
-  @override
16
-  Widget build(BuildContext context) {
17
-    return Row(
18
-      mainAxisAlignment: MainAxisAlignment.center,
19
-      children: <Widget>[
20
-        Text('Ze'),
21
-        FlutterLogo(size: 24.0),
22
-        Text('yr'),
23
-      ],
24
-    );
25
-  }
26
-}
27
-
28 13
 class ZefyrApp extends StatelessWidget {
29 14
   @override
30 15
   Widget build(BuildContext context) {
31
-    return new MaterialApp(
16
+    return MaterialApp(
32 17
       debugShowCheckedModeBanner: false,
33 18
       title: 'Zefyr Editor',
34
-      theme: new ThemeData(primarySwatch: Colors.cyan),
35
-      home: new MyHomePage(),
19
+      theme: ThemeData(primarySwatch: Colors.cyan),
20
+      home: HomePage(),
21
+      routes: {
22
+        "/fullPage": buildFullPage,
23
+        "/form": buildFormPage,
24
+      },
36 25
     );
37 26
   }
38
-}
39
-
40
-class MyHomePage extends StatefulWidget {
41
-  @override
42
-  _MyHomePageState createState() => new _MyHomePageState();
43
-}
44 27
 
45
-final doc =
46
-    r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\n"},{"insert":"​","attributes":{"embed":{"type":"image","source":"asset://images/breeze.jpg"}}},{"insert":"\n"},{"insert":"Photo by Hiroyuki Takeda.","attributes":{"i":true}},{"insert":"\nZefyr is currently in "},{"insert":"early preview","attributes":{"b":true}},{"insert":". If you have a feature request or found a bug, please file it at the "},{"insert":"issue tracker","attributes":{"a":"https://github.com/memspace/zefyr/issues"}},{"insert":'
47
-    r'".\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/quick_start.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data Format and Document Model","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/data_and_document.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style Attributes","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/attr'
48
-    r'ibutes.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic Rules","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/heuristics.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"FAQ","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/faq.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and fle'
49
-    r'xibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nMarkdown inspired semantics"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Ever needed to have a heading line inside of a quote block, like this:\nI’m a Markdown heading"},{"insert":"\n","attributes":{"block":"quote","heading":3}},{"insert":"And I’m a regular paragraph"},{"insert":"\n","attributes":{"block":"quote"}},{"insert":"Code blocks"},{"insert":"\n","attributes":{"headin'
50
-    r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
28
+  Widget buildFullPage(BuildContext context) {
29
+    return FullPageEditorScreen();
30
+  }
51 31
 
52
-Delta getDelta() {
53
-  return Delta.fromJson(json.decode(doc));
32
+  Widget buildFormPage(BuildContext context) {
33
+    return FormEmbeddedScreen();
34
+  }
54 35
 }
55 36
 
56
-class _MyHomePageState extends State<MyHomePage> {
57
-  final ZefyrController _controller =
58
-      ZefyrController(NotusDocument.fromDelta(getDelta()));
59
-  final FocusNode _focusNode = new FocusNode();
60
-  bool _editing = false;
61
-
37
+class HomePage extends StatelessWidget {
62 38
   @override
63 39
   Widget build(BuildContext context) {
64
-    final theme = new ZefyrThemeData(
65
-      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
66
-        color: Colors.grey.shade800,
67
-        toggleColor: Colors.grey.shade900,
68
-        iconColor: Colors.white,
69
-        disabledIconColor: Colors.grey.shade500,
70
-      ),
71
-    );
72
-
73
-    final done = _editing
74
-        ? [new FlatButton(onPressed: _stopEditing, child: Text('DONE'))]
75
-        : [new FlatButton(onPressed: _startEditing, child: Text('EDIT'))];
40
+    final nav = Navigator.of(context);
76 41
     return Scaffold(
77
-      resizeToAvoidBottomPadding: true,
78 42
       appBar: AppBar(
79 43
         elevation: 1.0,
80 44
         backgroundColor: Colors.grey.shade200,
81 45
         brightness: Brightness.light,
82 46
         title: ZefyrLogo(),
83
-        actions: done,
84 47
       ),
85
-      body: ZefyrTheme(
86
-        data: theme,
87
-        child: ZefyrEditor(
88
-          controller: _controller,
89
-          focusNode: _focusNode,
90
-          enabled: _editing,
91
-          imageDelegate: new CustomImageDelegate(),
92
-        ),
48
+      body: Column(
49
+        children: <Widget>[
50
+          Expanded(child: Container()),
51
+          FlatButton(
52
+            onPressed: () => nav.pushNamed('/fullPage'),
53
+            child: Text('Full page editor'),
54
+            color: Colors.lightBlue,
55
+            textColor: Colors.white,
56
+          ),
57
+          FlatButton(
58
+            onPressed: () => nav.pushNamed('/form'),
59
+            child: Text('Embedded in a form'),
60
+            color: Colors.lightBlue,
61
+            textColor: Colors.white,
62
+          ),
63
+          Expanded(child: Container()),
64
+        ],
93 65
       ),
94 66
     );
95 67
   }
96
-
97
-  void _startEditing() {
98
-    setState(() {
99
-      _editing = true;
100
-    });
101
-  }
102
-
103
-  void _stopEditing() {
104
-    setState(() {
105
-      _editing = false;
106
-    });
107
-  }
108
-}
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 68
 }

+ 78
- 0
packages/zefyr/example/lib/src/form.dart View File

@@ -0,0 +1,78 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:zefyr/zefyr.dart';
3
+
4
+import 'full_page.dart';
5
+
6
+class FormEmbeddedScreen extends StatefulWidget {
7
+  @override
8
+  _FormEmbeddedScreenState createState() => _FormEmbeddedScreenState();
9
+}
10
+
11
+class _FormEmbeddedScreenState extends State<FormEmbeddedScreen> {
12
+  final ZefyrController _controller = ZefyrController(NotusDocument());
13
+  final FocusNode _focusNode = new FocusNode();
14
+
15
+  @override
16
+  Widget build(BuildContext context) {
17
+    final form = ListView(
18
+      children: <Widget>[
19
+        TextField(decoration: InputDecoration(labelText: 'Name')),
20
+        TextField(decoration: InputDecoration(labelText: 'Email')),
21
+        Container(
22
+          padding: EdgeInsets.only(top: 16.0),
23
+          child: Text(
24
+            'Description',
25
+            style: TextStyle(color: Colors.black54, fontSize: 16.0),
26
+          ),
27
+          alignment: Alignment.centerLeft,
28
+        ),
29
+        buildEditor(),
30
+      ],
31
+    );
32
+
33
+    return Scaffold(
34
+      resizeToAvoidBottomPadding: true,
35
+      appBar: AppBar(
36
+        elevation: 1.0,
37
+        backgroundColor: Colors.grey.shade200,
38
+        brightness: Brightness.light,
39
+        title: ZefyrLogo(),
40
+      ),
41
+      body: ZefyrScaffold(
42
+        child: Padding(
43
+          padding: const EdgeInsets.all(8.0),
44
+          child: form,
45
+        ),
46
+      ),
47
+    );
48
+  }
49
+
50
+  Widget buildEditor() {
51
+    final theme = new ZefyrThemeData(
52
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
53
+        color: Colors.grey.shade800,
54
+        toggleColor: Colors.grey.shade900,
55
+        iconColor: Colors.white,
56
+        disabledIconColor: Colors.grey.shade500,
57
+      ),
58
+    );
59
+
60
+    return Container(
61
+      margin: EdgeInsets.symmetric(vertical: 8.0),
62
+      constraints: BoxConstraints.tightFor(height: 300.0),
63
+      decoration:
64
+          BoxDecoration(border: Border.all(color: Colors.grey.shade400)),
65
+      child: ZefyrTheme(
66
+        data: theme,
67
+        child: ZefyrEditor(
68
+          padding: EdgeInsets.all(8.0),
69
+          controller: _controller,
70
+          focusNode: _focusNode,
71
+          autofocus: false,
72
+          imageDelegate: new CustomImageDelegate(),
73
+          physics: ClampingScrollPhysics(),
74
+        ),
75
+      ),
76
+    );
77
+  }
78
+}

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

@@ -0,0 +1,108 @@
1
+import 'dart:convert';
2
+
3
+import 'package:flutter/material.dart';
4
+import 'package:quill_delta/quill_delta.dart';
5
+import 'package:zefyr/zefyr.dart';
6
+
7
+class ZefyrLogo extends StatelessWidget {
8
+  @override
9
+  Widget build(BuildContext context) {
10
+    return Row(
11
+      mainAxisAlignment: MainAxisAlignment.center,
12
+      children: <Widget>[
13
+        Text('Ze'),
14
+        FlutterLogo(size: 24.0),
15
+        Text('yr'),
16
+      ],
17
+    );
18
+  }
19
+}
20
+
21
+class FullPageEditorScreen extends StatefulWidget {
22
+  @override
23
+  _FullPageEditorScreenState createState() => new _FullPageEditorScreenState();
24
+}
25
+
26
+final doc =
27
+    r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\n"},{"insert":"​","attributes":{"embed":{"type":"image","source":"asset://images/breeze.jpg"}}},{"insert":"\n"},{"insert":"Photo by Hiroyuki Takeda.","attributes":{"i":true}},{"insert":"\nZefyr is currently in "},{"insert":"early preview","attributes":{"b":true}},{"insert":". If you have a feature request or found a bug, please file it at the "},{"insert":"issue tracker","attributes":{"a":"https://github.com/memspace/zefyr/issues"}},{"insert":'
28
+    r'".\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/quick_start.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data Format and Document Model","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/data_and_document.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style Attributes","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/attr'
29
+    r'ibutes.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic Rules","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/heuristics.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"FAQ","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/faq.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and fle'
30
+    r'xibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nMarkdown inspired semantics"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Ever needed to have a heading line inside of a quote block, like this:\nI’m a Markdown heading"},{"insert":"\n","attributes":{"block":"quote","heading":3}},{"insert":"And I’m a regular paragraph"},{"insert":"\n","attributes":{"block":"quote"}},{"insert":"Code blocks"},{"insert":"\n","attributes":{"headin'
31
+    r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
32
+
33
+Delta getDelta() {
34
+  return Delta.fromJson(json.decode(doc));
35
+}
36
+
37
+class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
38
+  final ZefyrController _controller =
39
+      ZefyrController(NotusDocument.fromDelta(getDelta()));
40
+  final FocusNode _focusNode = new FocusNode();
41
+  bool _editing = false;
42
+
43
+  @override
44
+  Widget build(BuildContext context) {
45
+    final theme = new ZefyrThemeData(
46
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
47
+        color: Colors.grey.shade800,
48
+        toggleColor: Colors.grey.shade900,
49
+        iconColor: Colors.white,
50
+        disabledIconColor: Colors.grey.shade500,
51
+      ),
52
+    );
53
+
54
+    final done = _editing
55
+        ? [new FlatButton(onPressed: _stopEditing, child: Text('DONE'))]
56
+        : [new FlatButton(onPressed: _startEditing, child: Text('EDIT'))];
57
+    return Scaffold(
58
+      resizeToAvoidBottomPadding: true,
59
+      appBar: AppBar(
60
+        elevation: 1.0,
61
+        backgroundColor: Colors.grey.shade200,
62
+        brightness: Brightness.light,
63
+        title: ZefyrLogo(),
64
+        actions: done,
65
+      ),
66
+      body: ZefyrScaffold(
67
+        child: ZefyrTheme(
68
+          data: theme,
69
+          child: ZefyrEditor(
70
+            controller: _controller,
71
+            focusNode: _focusNode,
72
+            enabled: _editing,
73
+            imageDelegate: new CustomImageDelegate(),
74
+          ),
75
+        ),
76
+      ),
77
+    );
78
+  }
79
+
80
+  void _startEditing() {
81
+    setState(() {
82
+      _editing = true;
83
+    });
84
+  }
85
+
86
+  void _stopEditing() {
87
+    setState(() {
88
+      _editing = false;
89
+    });
90
+  }
91
+}
92
+
93
+/// Custom image delegate used by this example to load image from application
94
+/// assets.
95
+///
96
+/// Default image delegate only supports [FileImage]s.
97
+class CustomImageDelegate extends ZefyrDefaultImageDelegate {
98
+  @override
99
+  Widget buildImage(BuildContext context, String imageSource) {
100
+    // We use custom "asset" scheme to distinguish asset images from other files.
101
+    if (imageSource.startsWith('asset://')) {
102
+      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
103
+      return new Image(image: asset);
104
+    } else {
105
+      return super.buildImage(context, imageSource);
106
+    }
107
+  }
108
+}

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

@@ -491,18 +491,19 @@ class _LinkInput extends StatefulWidget {
491 491
 class _LinkInputState extends State<_LinkInput> {
492 492
   final FocusNode _focusNode = FocusNode();
493 493
 
494
-  bool _didAutoFocus = false;
495 494
   ZefyrEditorScope _editor;
495
+  bool _didAutoFocus = false;
496 496
 
497 497
   @override
498 498
   void didChangeDependencies() {
499 499
     super.didChangeDependencies();
500
-    final toolbar = ZefyrToolbar.of(context);
501 500
     if (!_didAutoFocus) {
502 501
       FocusScope.of(context).requestFocus(_focusNode);
503 502
       _didAutoFocus = true;
504 503
     }
505 504
 
505
+    final toolbar = ZefyrToolbar.of(context);
506
+
506 507
     if (_editor != toolbar.editor) {
507 508
       _editor?.setToolbarFocusNode(null);
508 509
       _editor = toolbar.editor;
@@ -512,6 +513,7 @@ class _LinkInputState extends State<_LinkInput> {
512 513
 
513 514
   @override
514 515
   void dispose() {
516
+    _editor?.setToolbarFocusNode(null);
515 517
     _focusNode.dispose();
516 518
     _editor = null;
517 519
     super.dispose();
@@ -519,8 +521,6 @@ class _LinkInputState extends State<_LinkInput> {
519 521
 
520 522
   @override
521 523
   Widget build(BuildContext context) {
522
-    FocusScope.of(context).reparentIfNeeded(_focusNode);
523
-
524 524
     final theme = Theme.of(context);
525 525
     final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
526 526
     final color =
@@ -531,7 +531,7 @@ class _LinkInputState extends State<_LinkInput> {
531 531
       keyboardType: TextInputType.url,
532 532
       focusNode: _focusNode,
533 533
       controller: widget.controller,
534
-//      autofocus: true,
534
+      autofocus: true,
535 535
       decoration: new InputDecoration(
536 536
         hintText: 'https://',
537 537
         filled: true,

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

@@ -36,6 +36,7 @@ class ZefyrEditableText extends StatefulWidget {
36 36
     this.autofocus: true,
37 37
     this.enabled: true,
38 38
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
39
+    this.physics,
39 40
   }) : super(key: key);
40 41
 
41 42
   final ZefyrController controller;
@@ -43,6 +44,7 @@ class ZefyrEditableText extends StatefulWidget {
43 44
   final ZefyrImageDelegate imageDelegate;
44 45
   final bool autofocus;
45 46
   final bool enabled;
47
+  final ScrollPhysics physics;
46 48
 
47 49
   /// Padding around editable area.
48 50
   final EdgeInsets padding;
@@ -132,8 +134,7 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
132 134
       body = new Padding(padding: widget.padding, child: body);
133 135
     }
134 136
     final scrollable = SingleChildScrollView(
135
-      padding: EdgeInsets.only(top: 16.0),
136
-      physics: AlwaysScrollableScrollPhysics(),
137
+      physics: widget.physics,
137 138
       controller: _scrollController,
138 139
       child: body,
139 140
     );

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

@@ -7,6 +7,7 @@ import 'package:notus/notus.dart';
7 7
 import 'controller.dart';
8 8
 import 'editable_text.dart';
9 9
 import 'image.dart';
10
+import 'scaffold.dart';
10 11
 import 'theme.dart';
11 12
 import 'toolbar.dart';
12 13
 
@@ -31,7 +32,6 @@ class ZefyrEditorScope extends ChangeNotifier {
31 32
   ZefyrImageDelegate _imageDelegate;
32 33
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
33 34
 
34
-  FocusScopeNode get focusScope => _focusScope;
35 35
   FocusScopeNode _focusScope;
36 36
   FocusNode _focusNode;
37 37
 
@@ -103,13 +103,13 @@ class ZefyrEditorScope extends ChangeNotifier {
103 103
   FocusNode _toolbarFocusNode;
104 104
 
105 105
   void setToolbarFocusNode(FocusNode node) {
106
-    assert(!_disposed);
106
+    assert(!_disposed || node == null);
107 107
     if (_toolbarFocusNode != node) {
108 108
       _toolbarFocusNode?.removeListener(_handleFocusChange);
109 109
       _toolbarFocusNode = node;
110
-      _toolbarFocusNode.addListener(_handleFocusChange);
110
+      _toolbarFocusNode?.addListener(_handleFocusChange);
111 111
       // We do not notify listeners here because it will happen when
112
-      // focus state changes.
112
+      // focus state changes, see [_handleFocusChange].
113 113
     }
114 114
   }
115 115
 
@@ -169,6 +169,7 @@ class ZefyrEditor extends StatefulWidget {
169 169
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
170 170
     this.toolbarDelegate,
171 171
     this.imageDelegate,
172
+    this.physics,
172 173
   }) : super(key: key);
173 174
 
174 175
   final ZefyrController controller;
@@ -177,6 +178,7 @@ class ZefyrEditor extends StatefulWidget {
177 178
   final bool enabled;
178 179
   final ZefyrToolbarDelegate toolbarDelegate;
179 180
   final ZefyrImageDelegate imageDelegate;
181
+  final ScrollPhysics physics;
180 182
 
181 183
   /// Padding around editable area.
182 184
   final EdgeInsets padding;
@@ -195,36 +197,42 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
195 197
   ZefyrImageDelegate _imageDelegate;
196 198
   ZefyrEditorScope _scope;
197 199
   ZefyrThemeData _themeData;
200
+  GlobalKey<ZefyrToolbarState> _toolbarKey;
201
+  ZefyrScaffoldState _scaffold;
198 202
 
199
-  OverlayEntry _toolbar;
200
-  OverlayState _overlay;
203
+  bool get hasToolbar => _toolbarKey != null;
201 204
 
202 205
   void showToolbar() {
203
-    _toolbar = new OverlayEntry(
204
-      builder: (context) => _ZefyrToolbarContainer(
205
-            theme: _themeData,
206
-            toolbar: ZefyrToolbar(
207
-              editor: _scope,
208
-              delegate: widget.toolbarDelegate,
209
-            ),
210
-          ),
211
-    );
212
-    _overlay.insert(_toolbar);
206
+    assert(_toolbarKey == null);
207
+    _toolbarKey = GlobalKey();
208
+    _scaffold.showToolbar(buildToolbar);
213 209
   }
214 210
 
215 211
   void hideToolbar() {
216
-    _toolbar?.remove();
217
-    _toolbar = null;
212
+    if (_toolbarKey == null) return;
213
+    _scaffold.hideToolbar();
214
+    _toolbarKey = null;
215
+  }
216
+
217
+  Widget buildToolbar(BuildContext) {
218
+    return ZefyrTheme(
219
+      data: _themeData,
220
+      child: ZefyrToolbar(
221
+        key: _toolbarKey,
222
+        editor: _scope,
223
+        delegate: widget.toolbarDelegate,
224
+      ),
225
+    );
218 226
   }
219 227
 
220 228
   void _handleChange() {
221 229
     if (_scope.focusOwner == FocusOwner.none) {
222 230
       hideToolbar();
223
-    } else if (_toolbar == null) {
231
+    } else if (!hasToolbar) {
224 232
       showToolbar();
225 233
     } else {
226 234
       WidgetsBinding.instance.addPostFrameCallback((_) {
227
-        _toolbar?.markNeedsBuild();
235
+        _toolbarKey?.currentState?.markNeedsRebuild();
228 236
       });
229 237
     }
230 238
   }
@@ -249,6 +257,11 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
249 257
   @override
250 258
   void didChangeDependencies() {
251 259
     super.didChangeDependencies();
260
+    final parentTheme = ZefyrTheme.of(context, nullOk: true);
261
+    final fallbackTheme = ZefyrThemeData.fallback(context);
262
+    _themeData = (parentTheme != null)
263
+        ? fallbackTheme.merge(parentTheme)
264
+        : fallbackTheme;
252 265
 
253 266
     if (_scope == null) {
254 267
       _scope = ZefyrEditorScope(
@@ -265,17 +278,12 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
265 278
       }
266 279
     }
267 280
 
268
-    final parentTheme = ZefyrTheme.of(context, nullOk: true);
269
-    final fallbackTheme = ZefyrThemeData.fallback(context);
270
-    _themeData = (parentTheme != null)
271
-        ? fallbackTheme.merge(parentTheme)
272
-        : fallbackTheme;
273
-
274
-    final overlay = Overlay.of(context, debugRequiredFor: widget);
275
-    if (_overlay != overlay) {
281
+    final scaffold = ZefyrScaffold.of(context);
282
+    if (_scaffold != scaffold) {
283
+      bool didHaveToolbar = hasToolbar;
276 284
       hideToolbar();
277
-      _overlay = overlay;
278
-      // TODO: update toolbar.
285
+      _scaffold = scaffold;
286
+      if (didHaveToolbar) showToolbar();
279 287
     }
280 288
   }
281 289
 
@@ -296,45 +304,15 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
296 304
       autofocus: widget.autofocus,
297 305
       enabled: widget.enabled,
298 306
       padding: widget.padding,
307
+      physics: widget.physics,
299 308
     );
300 309
 
301
-    final children = <Widget>[];
302
-    children.add(Expanded(child: editable));
303
-    if (_toolbar != null) {
304
-      children.add(SizedBox(height: ZefyrToolbar.kToolbarHeight));
305
-    }
306
-//    final toolbar = ZefyrToolbar(
307
-//      editor: _scope,
308
-//      focusNode: _toolbarFocusNode,
309
-//      delegate: widget.toolbarDelegate,
310
-//    );
311
-//    children.add(toolbar);
312
-
313 310
     return ZefyrTheme(
314 311
       data: _themeData,
315 312
       child: _ZefyrEditorScope(
316 313
         scope: _scope,
317
-        child: Column(children: children),
314
+        child: editable,
318 315
       ),
319 316
     );
320 317
   }
321 318
 }
322
-
323
-class _ZefyrToolbarContainer extends StatelessWidget {
324
-  final ZefyrThemeData theme;
325
-  final Widget toolbar;
326
-
327
-  const _ZefyrToolbarContainer({Key key, this.theme, this.toolbar})
328
-      : super(key: key);
329
-
330
-  @override
331
-  Widget build(BuildContext context) {
332
-    final media = MediaQuery.of(context);
333
-    return Positioned(
334
-      bottom: media.viewInsets.bottom,
335
-      left: 0.0,
336
-      right: 0.0,
337
-      child: ZefyrTheme(data: theme, child: toolbar),
338
-    );
339
-  }
340
-}

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

@@ -0,0 +1,60 @@
1
+import 'package:flutter/material.dart';
2
+
3
+class ZefyrScaffold extends StatefulWidget {
4
+  final Widget child;
5
+
6
+  const ZefyrScaffold({Key key, this.child}) : super(key: key);
7
+
8
+  static ZefyrScaffoldState of(BuildContext context) {
9
+    final _ZefyrScaffoldAccess widget =
10
+        context.inheritFromWidgetOfExactType(_ZefyrScaffoldAccess);
11
+    return widget.scaffold;
12
+  }
13
+
14
+  @override
15
+  ZefyrScaffoldState createState() => ZefyrScaffoldState();
16
+}
17
+
18
+class ZefyrScaffoldState extends State<ZefyrScaffold> {
19
+  WidgetBuilder _toolbarBuilder;
20
+
21
+  void showToolbar(WidgetBuilder builder) {
22
+    setState(() {
23
+      _toolbarBuilder = builder;
24
+    });
25
+  }
26
+
27
+  void hideToolbar() {
28
+    if (_toolbarBuilder != null) {
29
+      setState(() {
30
+        _toolbarBuilder = null;
31
+      });
32
+    }
33
+  }
34
+
35
+  @override
36
+  Widget build(BuildContext context) {
37
+    final toolbar =
38
+        (_toolbarBuilder == null) ? Container() : _toolbarBuilder(context);
39
+    return _ZefyrScaffoldAccess(
40
+      scaffold: this,
41
+      child: Column(
42
+        children: <Widget>[
43
+          Expanded(child: widget.child),
44
+          toolbar,
45
+        ],
46
+      ),
47
+    );
48
+  }
49
+}
50
+
51
+class _ZefyrScaffoldAccess extends InheritedWidget {
52
+  final ZefyrScaffoldState scaffold;
53
+
54
+  _ZefyrScaffoldAccess({Widget child, this.scaffold}) : super(child: child);
55
+
56
+  @override
57
+  bool updateShouldNotify(_ZefyrScaffoldAccess oldWidget) {
58
+    return oldWidget.scaffold != scaffold;
59
+  }
60
+}

+ 14
- 11
packages/zefyr/lib/src/widgets/selection.dart View File

@@ -66,6 +66,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
66 66
 
67 67
   @override
68 68
   void hideToolbar() {
69
+    _didCaretTap = false; // reset double tap.
69 70
     _toolbar?.remove();
70 71
     _toolbar = null;
71 72
     _toolbarController.stop();
@@ -98,8 +99,6 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
98 99
     super.initState();
99 100
     _toolbarController = new AnimationController(
100 101
         duration: _kFadeDuration, vsync: widget.overlay);
101
-    _selection = widget.controller.selection;
102
-    widget.controller.addListener(_handleChange);
103 102
   }
104 103
 
105 104
   static const Duration _kFadeDuration = const Duration(milliseconds: 150);
@@ -113,23 +112,24 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
113 112
       _toolbarController = new AnimationController(
114 113
           duration: _kFadeDuration, vsync: widget.overlay);
115 114
     }
116
-    if (oldWidget.controller != widget.controller) {
117
-      oldWidget.controller.removeListener(_handleChange);
118
-      widget.controller.addListener(_handleChange);
119
-    }
120 115
   }
121 116
 
122 117
   @override
123 118
   void didChangeDependencies() {
124 119
     super.didChangeDependencies();
125
-    WidgetsBinding.instance.addPostFrameCallback((_) {
126
-      _updateToolbar();
127
-    });
120
+    final editor = ZefyrEditor.of(context);
121
+    if (_editor != editor) {
122
+      _editor?.removeListener(_handleChange);
123
+      _editor = editor;
124
+      _editor.addListener(_handleChange);
125
+      _selection = _editor.selection;
126
+      _focusOwner = _editor.focusOwner;
127
+    }
128 128
   }
129 129
 
130 130
   @override
131 131
   void dispose() {
132
-    widget.controller.removeListener(_handleChange);
132
+    _editor.removeListener(_handleChange);
133 133
     hideToolbar();
134 134
     _toolbarController.dispose();
135 135
     _toolbarController = null;
@@ -174,12 +174,14 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
174 174
   OverlayEntry _toolbar;
175 175
   AnimationController _toolbarController;
176 176
 
177
+  ZefyrEditorScope _editor;
177 178
   TextSelection _selection;
179
+  FocusOwner _focusOwner;
178 180
 
179 181
   bool _didCaretTap = false;
180 182
 
181 183
   void _handleChange() {
182
-    if (_selection != widget.controller.selection) {
184
+    if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) {
183 185
       _updateToolbar();
184 186
     }
185 187
   }
@@ -208,6 +210,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
208 210
         }
209 211
       }
210 212
       _selection = selection;
213
+      _focusOwner = focusOwner;
211 214
     });
212 215
   }
213 216
 

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

@@ -151,7 +151,12 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
151 151
   TextSelection _selection;
152 152
 
153 153
   void markNeedsRebuild() {
154
-    setState(() {});
154
+    setState(() {
155
+      if (_selection != editor.selection) {
156
+        _selection = editor.selection;
157
+        closeOverlay();
158
+      }
159
+    });
155 160
   }
156 161
 
157 162
   Widget buildButton(BuildContext context, ZefyrToolbarAction action,
@@ -200,10 +205,6 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
200 205
     if (widget.delegate != oldWidget.delegate) {
201 206
       _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
202 207
     }
203
-    if (_selection != editor.selection) {
204
-      _selection = editor.selection;
205
-      closeOverlay();
206
-    }
207 208
   }
208 209
 
209 210
   @override
@@ -214,10 +215,6 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
214 215
 
215 216
   @override
216 217
   Widget build(BuildContext context) {
217
-    if (editor.focusOwner == FocusOwner.none) {
218
-      return new Container();
219
-    }
220
-
221 218
     final layers = <Widget>[];
222 219
 
223 220
     // Must set unique key for the toolbar to prevent it from reconstructing
@@ -243,14 +240,11 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
243 240
 
244 241
     final constraints =
245 242
         BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight);
246
-    return FocusScope(
247
-      node: editor.focusScope,
248
-      child: _ZefyrToolbarScope(
249
-        toolbar: this,
250
-        child: Container(
251
-          constraints: constraints,
252
-          child: Stack(children: layers),
253
-        ),
243
+    return _ZefyrToolbarScope(
244
+      toolbar: this,
245
+      child: Container(
246
+        constraints: constraints,
247
+        child: Stack(children: layers),
254 248
       ),
255 249
     );
256 250
   }

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

@@ -20,6 +20,7 @@ export 'src/widgets/image.dart';
20 20
 export 'src/widgets/list.dart';
21 21
 export 'src/widgets/paragraph.dart';
22 22
 export 'src/widgets/quote.dart';
23
+export 'src/widgets/scaffold.dart';
23 24
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
24 25
 export 'src/widgets/theme.dart';
25 26
 export 'src/widgets/toolbar.dart';

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

@@ -30,7 +30,9 @@ class EditorSandBox {
30 30
     if (theme != null) {
31 31
       widget = ZefyrTheme(data: theme, child: widget);
32 32
     }
33
-    widget = MaterialApp(home: widget);
33
+    widget = MaterialApp(
34
+      home: ZefyrScaffold(child: widget),
35
+    );
34 36
 
35 37
     return EditorSandBox._(tester, focusNode, document, controller, widget);
36 38
   }

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

@@ -75,7 +75,7 @@ void main() {
75 75
       expect(editor.selection.extentOffset, embed.documentOffset);
76 76
     });
77 77
 
78
-    testWidgets('tap right side of horizontal rule puts caret after it',
78
+    testWidgets('tap right side of image puts caret after it',
79 79
         (tester) async {
80 80
       final editor = new EditorSandBox(tester: tester);
81 81
       await editor.tapEditor();

+ 8
- 6
packages/zefyr/test/widgets/render_context_test.dart View File

@@ -52,12 +52,14 @@ void main() {
52 52
     });
53 53
 
54 54
     testWidgets('notifyListeners is delayed to next frame', (tester) async {
55
-      var focusNode = new FocusNode();
56
-      var controller = new ZefyrController(new NotusDocument());
57
-      var widget = new MaterialApp(
58
-        home: new ZefyrEditor(
59
-          controller: controller,
60
-          focusNode: focusNode,
55
+      var focusNode = FocusNode();
56
+      var controller = ZefyrController(new NotusDocument());
57
+      var widget = MaterialApp(
58
+        home: ZefyrScaffold(
59
+          child: ZefyrEditor(
60
+            controller: controller,
61
+            focusNode: focusNode,
62
+          ),
61 63
         ),
62 64
       );
63 65
       await tester.pumpWidget(widget);