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
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
 // for details. All rights reserved. Use of this source code is governed by a
2
 // for details. All rights reserved. Use of this source code is governed by a
3
 // BSD-style license that can be found in the LICENSE file.
3
 // BSD-style license that can be found in the LICENSE file.
4
-import 'dart:convert';
5
-
6
 import 'package:flutter/material.dart';
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
 void main() {
9
 void main() {
11
   runApp(new ZefyrApp());
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
 class ZefyrApp extends StatelessWidget {
13
 class ZefyrApp extends StatelessWidget {
29
   @override
14
   @override
30
   Widget build(BuildContext context) {
15
   Widget build(BuildContext context) {
31
-    return new MaterialApp(
16
+    return MaterialApp(
32
       debugShowCheckedModeBanner: false,
17
       debugShowCheckedModeBanner: false,
33
       title: 'Zefyr Editor',
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
   @override
38
   @override
63
   Widget build(BuildContext context) {
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
     return Scaffold(
41
     return Scaffold(
77
-      resizeToAvoidBottomPadding: true,
78
       appBar: AppBar(
42
       appBar: AppBar(
79
         elevation: 1.0,
43
         elevation: 1.0,
80
         backgroundColor: Colors.grey.shade200,
44
         backgroundColor: Colors.grey.shade200,
81
         brightness: Brightness.light,
45
         brightness: Brightness.light,
82
         title: ZefyrLogo(),
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

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

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

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

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

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

7
 import 'controller.dart';
7
 import 'controller.dart';
8
 import 'editable_text.dart';
8
 import 'editable_text.dart';
9
 import 'image.dart';
9
 import 'image.dart';
10
+import 'scaffold.dart';
10
 import 'theme.dart';
11
 import 'theme.dart';
11
 import 'toolbar.dart';
12
 import 'toolbar.dart';
12
 
13
 
31
   ZefyrImageDelegate _imageDelegate;
32
   ZefyrImageDelegate _imageDelegate;
32
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
33
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
33
 
34
 
34
-  FocusScopeNode get focusScope => _focusScope;
35
   FocusScopeNode _focusScope;
35
   FocusScopeNode _focusScope;
36
   FocusNode _focusNode;
36
   FocusNode _focusNode;
37
 
37
 
103
   FocusNode _toolbarFocusNode;
103
   FocusNode _toolbarFocusNode;
104
 
104
 
105
   void setToolbarFocusNode(FocusNode node) {
105
   void setToolbarFocusNode(FocusNode node) {
106
-    assert(!_disposed);
106
+    assert(!_disposed || node == null);
107
     if (_toolbarFocusNode != node) {
107
     if (_toolbarFocusNode != node) {
108
       _toolbarFocusNode?.removeListener(_handleFocusChange);
108
       _toolbarFocusNode?.removeListener(_handleFocusChange);
109
       _toolbarFocusNode = node;
109
       _toolbarFocusNode = node;
110
-      _toolbarFocusNode.addListener(_handleFocusChange);
110
+      _toolbarFocusNode?.addListener(_handleFocusChange);
111
       // We do not notify listeners here because it will happen when
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
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
169
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
170
     this.toolbarDelegate,
170
     this.toolbarDelegate,
171
     this.imageDelegate,
171
     this.imageDelegate,
172
+    this.physics,
172
   }) : super(key: key);
173
   }) : super(key: key);
173
 
174
 
174
   final ZefyrController controller;
175
   final ZefyrController controller;
177
   final bool enabled;
178
   final bool enabled;
178
   final ZefyrToolbarDelegate toolbarDelegate;
179
   final ZefyrToolbarDelegate toolbarDelegate;
179
   final ZefyrImageDelegate imageDelegate;
180
   final ZefyrImageDelegate imageDelegate;
181
+  final ScrollPhysics physics;
180
 
182
 
181
   /// Padding around editable area.
183
   /// Padding around editable area.
182
   final EdgeInsets padding;
184
   final EdgeInsets padding;
195
   ZefyrImageDelegate _imageDelegate;
197
   ZefyrImageDelegate _imageDelegate;
196
   ZefyrEditorScope _scope;
198
   ZefyrEditorScope _scope;
197
   ZefyrThemeData _themeData;
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
   void showToolbar() {
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
   void hideToolbar() {
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
   void _handleChange() {
228
   void _handleChange() {
221
     if (_scope.focusOwner == FocusOwner.none) {
229
     if (_scope.focusOwner == FocusOwner.none) {
222
       hideToolbar();
230
       hideToolbar();
223
-    } else if (_toolbar == null) {
231
+    } else if (!hasToolbar) {
224
       showToolbar();
232
       showToolbar();
225
     } else {
233
     } else {
226
       WidgetsBinding.instance.addPostFrameCallback((_) {
234
       WidgetsBinding.instance.addPostFrameCallback((_) {
227
-        _toolbar?.markNeedsBuild();
235
+        _toolbarKey?.currentState?.markNeedsRebuild();
228
       });
236
       });
229
     }
237
     }
230
   }
238
   }
249
   @override
257
   @override
250
   void didChangeDependencies() {
258
   void didChangeDependencies() {
251
     super.didChangeDependencies();
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
     if (_scope == null) {
266
     if (_scope == null) {
254
       _scope = ZefyrEditorScope(
267
       _scope = ZefyrEditorScope(
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
       hideToolbar();
284
       hideToolbar();
277
-      _overlay = overlay;
278
-      // TODO: update toolbar.
285
+      _scaffold = scaffold;
286
+      if (didHaveToolbar) showToolbar();
279
     }
287
     }
280
   }
288
   }
281
 
289
 
296
       autofocus: widget.autofocus,
304
       autofocus: widget.autofocus,
297
       enabled: widget.enabled,
305
       enabled: widget.enabled,
298
       padding: widget.padding,
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
     return ZefyrTheme(
310
     return ZefyrTheme(
314
       data: _themeData,
311
       data: _themeData,
315
       child: _ZefyrEditorScope(
312
       child: _ZefyrEditorScope(
316
         scope: _scope,
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

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
 
66
 
67
   @override
67
   @override
68
   void hideToolbar() {
68
   void hideToolbar() {
69
+    _didCaretTap = false; // reset double tap.
69
     _toolbar?.remove();
70
     _toolbar?.remove();
70
     _toolbar = null;
71
     _toolbar = null;
71
     _toolbarController.stop();
72
     _toolbarController.stop();
98
     super.initState();
99
     super.initState();
99
     _toolbarController = new AnimationController(
100
     _toolbarController = new AnimationController(
100
         duration: _kFadeDuration, vsync: widget.overlay);
101
         duration: _kFadeDuration, vsync: widget.overlay);
101
-    _selection = widget.controller.selection;
102
-    widget.controller.addListener(_handleChange);
103
   }
102
   }
104
 
103
 
105
   static const Duration _kFadeDuration = const Duration(milliseconds: 150);
104
   static const Duration _kFadeDuration = const Duration(milliseconds: 150);
113
       _toolbarController = new AnimationController(
112
       _toolbarController = new AnimationController(
114
           duration: _kFadeDuration, vsync: widget.overlay);
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
   @override
117
   @override
123
   void didChangeDependencies() {
118
   void didChangeDependencies() {
124
     super.didChangeDependencies();
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
   @override
130
   @override
131
   void dispose() {
131
   void dispose() {
132
-    widget.controller.removeListener(_handleChange);
132
+    _editor.removeListener(_handleChange);
133
     hideToolbar();
133
     hideToolbar();
134
     _toolbarController.dispose();
134
     _toolbarController.dispose();
135
     _toolbarController = null;
135
     _toolbarController = null;
174
   OverlayEntry _toolbar;
174
   OverlayEntry _toolbar;
175
   AnimationController _toolbarController;
175
   AnimationController _toolbarController;
176
 
176
 
177
+  ZefyrEditorScope _editor;
177
   TextSelection _selection;
178
   TextSelection _selection;
179
+  FocusOwner _focusOwner;
178
 
180
 
179
   bool _didCaretTap = false;
181
   bool _didCaretTap = false;
180
 
182
 
181
   void _handleChange() {
183
   void _handleChange() {
182
-    if (_selection != widget.controller.selection) {
184
+    if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) {
183
       _updateToolbar();
185
       _updateToolbar();
184
     }
186
     }
185
   }
187
   }
208
         }
210
         }
209
       }
211
       }
210
       _selection = selection;
212
       _selection = selection;
213
+      _focusOwner = focusOwner;
211
     });
214
     });
212
   }
215
   }
213
 
216
 

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

151
   TextSelection _selection;
151
   TextSelection _selection;
152
 
152
 
153
   void markNeedsRebuild() {
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
   Widget buildButton(BuildContext context, ZefyrToolbarAction action,
162
   Widget buildButton(BuildContext context, ZefyrToolbarAction action,
200
     if (widget.delegate != oldWidget.delegate) {
205
     if (widget.delegate != oldWidget.delegate) {
201
       _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
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
   @override
210
   @override
214
 
215
 
215
   @override
216
   @override
216
   Widget build(BuildContext context) {
217
   Widget build(BuildContext context) {
217
-    if (editor.focusOwner == FocusOwner.none) {
218
-      return new Container();
219
-    }
220
-
221
     final layers = <Widget>[];
218
     final layers = <Widget>[];
222
 
219
 
223
     // Must set unique key for the toolbar to prevent it from reconstructing
220
     // Must set unique key for the toolbar to prevent it from reconstructing
243
 
240
 
244
     final constraints =
241
     final constraints =
245
         BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight);
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
 export 'src/widgets/list.dart';
20
 export 'src/widgets/list.dart';
21
 export 'src/widgets/paragraph.dart';
21
 export 'src/widgets/paragraph.dart';
22
 export 'src/widgets/quote.dart';
22
 export 'src/widgets/quote.dart';
23
+export 'src/widgets/scaffold.dart';
23
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
24
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
24
 export 'src/widgets/theme.dart';
25
 export 'src/widgets/theme.dart';
25
 export 'src/widgets/toolbar.dart';
26
 export 'src/widgets/toolbar.dart';

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

30
     if (theme != null) {
30
     if (theme != null) {
31
       widget = ZefyrTheme(data: theme, child: widget);
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
     return EditorSandBox._(tester, focusNode, document, controller, widget);
37
     return EditorSandBox._(tester, focusNode, document, controller, widget);
36
   }
38
   }

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

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

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

52
     });
52
     });
53
 
53
 
54
     testWidgets('notifyListeners is delayed to next frame', (tester) async {
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
       await tester.pumpWidget(widget);
65
       await tester.pumpWidget(widget);