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