Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'dart:math' as math;
8 :
9 : import 'package:flutter/foundation.dart' show ValueListenable;
10 : import 'package:flutter/gestures.dart';
11 : import 'package:flutter/material.dart' show kMinInteractiveDimension;
12 : import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase;
13 : export 'package:flutter/services.dart' show TextSelectionDelegate;
14 : import 'package:flutter/widgets.dart';
15 :
16 : import '../mongol_editable_text.dart';
17 : import '../mongol_render_editable.dart';
18 :
19 : /// The text position that a give selection handle manipulates. Dragging the
20 : /// [start] handle always moves the [start]/[baseOffset] of the selection.
21 8 : enum _TextSelectionHandlePosition { start, end }
22 :
23 : /// An object that manages a pair of text selection handles.
24 : ///
25 : /// The selection handles are displayed in the [Overlay] that most closely
26 : /// encloses the given [BuildContext].
27 : class MongolTextSelectionOverlay {
28 : /// Creates an object that manages overlay entries for selection handles.
29 : ///
30 : /// The [context] must not be null and must have an [Overlay] as an ancestor.
31 2 : MongolTextSelectionOverlay({
32 : required TextEditingValue value,
33 : required this.context,
34 : this.debugRequiredFor,
35 : required this.toolbarLayerLink,
36 : required this.startHandleLayerLink,
37 : required this.endHandleLayerLink,
38 : required this.renderObject,
39 : this.selectionControls,
40 : bool handlesVisible = false,
41 : this.selectionDelegate,
42 : this.dragStartBehavior = DragStartBehavior.start,
43 : this.onSelectionHandleTapped,
44 : this.clipboardStatus,
45 : }) : _handlesVisible = handlesVisible,
46 : _value = value {
47 4 : final overlay = Overlay.of(context, rootOverlay: true);
48 : assert(
49 0 : overlay != null,
50 0 : 'No Overlay widget exists above $context.\n'
51 : 'Usually the Navigator created by WidgetsApp provides the overlay. Perhaps your '
52 : 'app content was created above the Navigator with the WidgetsApp builder parameter.');
53 2 : _toolbarController =
54 2 : AnimationController(duration: fadeDuration, vsync: overlay!);
55 : }
56 :
57 : /// The context in which the selection handles should appear.
58 : ///
59 : /// This context must have an [Overlay] as an ancestor because this object
60 : /// will display the text selection handles in that [Overlay].
61 : final BuildContext context;
62 :
63 : /// Debugging information for explaining why the [Overlay] is required.
64 : final Widget? debugRequiredFor;
65 :
66 : /// The object supplied to the [CompositedTransformTarget] that wraps the text
67 : /// field.
68 : final LayerLink toolbarLayerLink;
69 :
70 : /// The objects supplied to the [CompositedTransformTarget] that wraps the
71 : /// location of start selection handle.
72 : final LayerLink startHandleLayerLink;
73 :
74 : /// The objects supplied to the [CompositedTransformTarget] that wraps the
75 : /// location of end selection handle.
76 : final LayerLink endHandleLayerLink;
77 :
78 : /// The editable line in which the selected text is being displayed.
79 : final MongolRenderEditable renderObject;
80 :
81 : /// Builds text selection handles and toolbar.
82 : final TextSelectionControls? selectionControls;
83 :
84 : /// The delegate for manipulating the current selection in the owning
85 : /// text field.
86 : final TextSelectionDelegate? selectionDelegate;
87 :
88 : /// Determines the way that drag start behavior is handled.
89 : ///
90 : /// If set to [DragStartBehavior.start], handle drag behavior will
91 : /// begin upon the detection of a drag gesture. If set to
92 : /// [DragStartBehavior.down] it will begin when a down event is first detected.
93 : ///
94 : /// In general, setting this to [DragStartBehavior.start] will make drag
95 : /// animation smoother and setting it to [DragStartBehavior.down] will make
96 : /// drag behavior feel slightly more reactive.
97 : ///
98 : /// By default, the drag start behavior is [DragStartBehavior.start].
99 : ///
100 : /// See also:
101 : ///
102 : /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
103 : final DragStartBehavior dragStartBehavior;
104 :
105 : /// A callback that's invoked when a selection handle is tapped.
106 : ///
107 : /// Both regular taps and long presses invoke this callback, but a drag
108 : /// gesture won't.
109 : final VoidCallback? onSelectionHandleTapped;
110 :
111 : /// Maintains the status of the clipboard for determining if its contents can
112 : /// be pasted or not.
113 : ///
114 : /// Useful because the actual value of the clipboard can only be checked
115 : /// asynchronously (see [Clipboard.getData]).
116 : final ClipboardStatusNotifier? clipboardStatus;
117 :
118 : /// Controls the fade-in and fade-out animations for the toolbar and handles.
119 : static const Duration fadeDuration = Duration(milliseconds: 150);
120 :
121 : late AnimationController _toolbarController;
122 6 : Animation<double> get _toolbarOpacity => _toolbarController.view;
123 :
124 : /// Retrieve current value.
125 1 : @visibleForTesting
126 1 : TextEditingValue get value => _value;
127 :
128 : TextEditingValue _value;
129 :
130 : /// A pair of handles. If this is non-null, there are always 2, though the
131 : /// second is hidden when the selection is collapsed.
132 : List<OverlayEntry>? _handles;
133 :
134 : /// A copy/paste toolbar.
135 : OverlayEntry? _toolbar;
136 :
137 6 : TextSelection get _selection => _value.selection;
138 :
139 : /// Whether selection handles are visible.
140 : ///
141 : /// Set to false if you want to hide the handles. Use this property to show or
142 : /// hide the handle without rebuilding them.
143 : ///
144 : /// If this method is called while the [SchedulerBinding.schedulerPhase] is
145 : /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
146 : /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
147 : /// until the post-frame callbacks phase. Otherwise the update is done
148 : /// synchronously. This means that it is safe to call during builds, but also
149 : /// that if you do call this during a build, the UI will not update until the
150 : /// next frame (i.e. many milliseconds later).
151 : ///
152 : /// Defaults to false.
153 4 : bool get handlesVisible => _handlesVisible;
154 : bool _handlesVisible = false;
155 2 : set handlesVisible(bool visible) {
156 4 : if (_handlesVisible == visible) {
157 : return;
158 : }
159 2 : _handlesVisible = visible;
160 : // If we are in build state, it will be too late to update visibility.
161 : // We will need to schedule the build in next frame.
162 6 : if (SchedulerBinding.instance!.schedulerPhase ==
163 : SchedulerPhase.persistentCallbacks) {
164 3 : SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild);
165 : } else {
166 1 : _markNeedsBuild();
167 : }
168 : }
169 :
170 : /// Builds the handles by inserting them into the [context]'s overlay.
171 2 : void showHandles() {
172 2 : if (_handles != null) {
173 : return;
174 : }
175 :
176 4 : _handles = <OverlayEntry>[
177 2 : OverlayEntry(
178 2 : builder: (BuildContext context) =>
179 2 : _buildHandle(context, _TextSelectionHandlePosition.start)),
180 2 : OverlayEntry(
181 2 : builder: (BuildContext context) =>
182 2 : _buildHandle(context, _TextSelectionHandlePosition.end)),
183 : ];
184 :
185 6 : Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
186 4 : .insertAll(_handles!);
187 : }
188 :
189 : /// Destroys the handles by removing them from overlay.
190 0 : void hideHandles() {
191 0 : if (_handles != null) {
192 0 : _handles![0].remove();
193 0 : _handles![1].remove();
194 0 : _handles = null;
195 : }
196 : }
197 :
198 : /// Shows the toolbar by inserting it into the [context]'s overlay.
199 2 : void showToolbar() {
200 2 : assert(_toolbar == null);
201 6 : _toolbar = OverlayEntry(builder: _buildToolbar);
202 6 : Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
203 4 : .insert(_toolbar!);
204 4 : _toolbarController.forward(from: 0.0);
205 : }
206 :
207 : /// Updates the overlay after the selection has changed.
208 : ///
209 : /// If this method is called while the [SchedulerBinding.schedulerPhase] is
210 : /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
211 : /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
212 : /// until the post-frame callbacks phase. Otherwise the update is done
213 : /// synchronously. This means that it is safe to call during builds, but also
214 : /// that if you do call this during a build, the UI will not update until the
215 : /// next frame (i.e. many milliseconds later).
216 2 : void update(TextEditingValue newValue) {
217 4 : if (_value == newValue) return;
218 2 : _value = newValue;
219 6 : if (SchedulerBinding.instance!.schedulerPhase ==
220 : SchedulerPhase.persistentCallbacks) {
221 6 : SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild);
222 : } else {
223 2 : _markNeedsBuild();
224 : }
225 : }
226 :
227 : /// Causes the overlay to update its rendering.
228 : ///
229 : /// This is intended to be called when the [renderObject] may have changed its
230 : /// text metrics (e.g. because the text was scrolled).
231 1 : void updateForScroll() {
232 1 : _markNeedsBuild();
233 : }
234 :
235 2 : void _markNeedsBuild([Duration? duration]) {
236 2 : if (_handles != null) {
237 6 : _handles![0].markNeedsBuild();
238 6 : _handles![1].markNeedsBuild();
239 : }
240 3 : _toolbar?.markNeedsBuild();
241 : }
242 :
243 : /// Whether the handles are currently visible.
244 3 : bool get handlesAreVisible => _handles != null && handlesVisible;
245 :
246 : /// Whether the toolbar is currently visible.
247 4 : bool get toolbarIsVisible => _toolbar != null;
248 :
249 : /// Hides the entire overlay including the toolbar and the handles.
250 2 : void hide() {
251 2 : if (_handles != null) {
252 6 : _handles![0].remove();
253 6 : _handles![1].remove();
254 2 : _handles = null;
255 : }
256 2 : if (_toolbar != null) {
257 2 : hideToolbar();
258 : }
259 : }
260 :
261 : /// Hides the toolbar part of the overlay.
262 : ///
263 : /// To hide the whole overlay, see [hide].
264 2 : void hideToolbar() {
265 2 : assert(_toolbar != null);
266 4 : _toolbarController.stop();
267 4 : _toolbar!.remove();
268 2 : _toolbar = null;
269 : }
270 :
271 : /// Final cleanup.
272 2 : void dispose() {
273 2 : hide();
274 4 : _toolbarController.dispose();
275 : }
276 :
277 2 : Widget _buildHandle(
278 : BuildContext context, _TextSelectionHandlePosition position) {
279 : Widget handle;
280 4 : if ((_selection.isCollapsed &&
281 2 : position == _TextSelectionHandlePosition.end) ||
282 2 : selectionControls == null) {
283 2 : handle = Container(); // hide the second handle when collapsed
284 : } else {
285 2 : handle = Visibility(
286 2 : visible: handlesVisible,
287 2 : child: _TextSelectionHandleOverlay(
288 0 : onSelectionHandleChanged: (TextSelection newSelection) {
289 0 : _handleSelectionHandleChanged(newSelection, position);
290 : },
291 2 : onSelectionHandleTapped: onSelectionHandleTapped,
292 2 : startHandleLayerLink: startHandleLayerLink,
293 2 : endHandleLayerLink: endHandleLayerLink,
294 2 : renderObject: renderObject,
295 2 : selection: _selection,
296 2 : selectionControls: selectionControls,
297 : position: position,
298 2 : dragStartBehavior: dragStartBehavior,
299 : ),
300 : );
301 : }
302 2 : return ExcludeSemantics(
303 : child: handle,
304 : );
305 : }
306 :
307 2 : Widget _buildToolbar(BuildContext context) {
308 2 : if (selectionControls == null) return Container();
309 :
310 : // Find the vertical midpoint, just to the left of the selected text.
311 6 : final endpoints = renderObject.getEndpointsForSelection(_selection);
312 2 : final editingRegion = Rect.fromPoints(
313 4 : renderObject.localToGlobal(Offset.zero),
314 10 : renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)),
315 : );
316 16 : final isMultiline = endpoints.last.point.dx - endpoints.first.point.dx >
317 6 : renderObject.preferredLineWidth / 2;
318 :
319 : // If the selected text spans more than 1 line, vertically center the toolbar.
320 : // Derived from both iOS and Android.
321 : final midY = (isMultiline)
322 4 : ? editingRegion.height / 2
323 16 : : (endpoints.first.point.dy + endpoints.last.point.dy) / 2;
324 :
325 2 : final midpoint = Offset(
326 : // The x-coordinate won't be made use of most likely.
327 12 : endpoints[0].point.dx - renderObject.preferredLineWidth,
328 : midY,
329 : );
330 :
331 2 : return Directionality(
332 : textDirection: TextDirection.ltr,
333 2 : child: FadeTransition(
334 2 : opacity: _toolbarOpacity,
335 2 : child: CompositedTransformFollower(
336 2 : link: toolbarLayerLink,
337 : showWhenUnlinked: false,
338 4 : offset: -editingRegion.topLeft,
339 4 : child: selectionControls!.buildToolbar(
340 : context,
341 : editingRegion,
342 4 : renderObject.preferredLineWidth,
343 : midpoint,
344 : endpoints,
345 2 : selectionDelegate!,
346 2 : clipboardStatus!,
347 4 : renderObject.lastSecondaryTapDownPosition,
348 : ),
349 : ),
350 : ),
351 : );
352 : }
353 :
354 0 : void _handleSelectionHandleChanged(
355 : TextSelection newSelection, _TextSelectionHandlePosition position) {
356 : final TextPosition textPosition;
357 : switch (position) {
358 0 : case _TextSelectionHandlePosition.start:
359 0 : textPosition = newSelection.base;
360 : break;
361 0 : case _TextSelectionHandlePosition.end:
362 0 : textPosition = newSelection.extent;
363 : break;
364 : }
365 0 : selectionDelegate!.userUpdateTextEditingValue(
366 0 : _value.copyWith(selection: newSelection, composing: TextRange.empty),
367 : SelectionChangedCause.drag,
368 : );
369 0 : selectionDelegate!.bringIntoView(textPosition);
370 : }
371 : }
372 :
373 : /// This widget represents a single draggable text selection handle.
374 : class _TextSelectionHandleOverlay extends StatefulWidget {
375 2 : const _TextSelectionHandleOverlay({
376 : Key? key,
377 : required this.selection,
378 : required this.position,
379 : required this.startHandleLayerLink,
380 : required this.endHandleLayerLink,
381 : required this.renderObject,
382 : required this.onSelectionHandleChanged,
383 : required this.onSelectionHandleTapped,
384 : required this.selectionControls,
385 : this.dragStartBehavior = DragStartBehavior.start,
386 2 : }) : super(key: key);
387 :
388 : final TextSelection selection;
389 : final _TextSelectionHandlePosition position;
390 : final LayerLink startHandleLayerLink;
391 : final LayerLink endHandleLayerLink;
392 : final MongolRenderEditable renderObject;
393 : final ValueChanged<TextSelection> onSelectionHandleChanged;
394 : final VoidCallback? onSelectionHandleTapped;
395 : final TextSelectionControls? selectionControls;
396 : final DragStartBehavior dragStartBehavior;
397 :
398 1 : @override
399 : _TextSelectionHandleOverlayState createState() =>
400 1 : _TextSelectionHandleOverlayState();
401 :
402 1 : ValueListenable<bool> get _visibility {
403 1 : switch (position) {
404 1 : case _TextSelectionHandlePosition.start:
405 2 : return renderObject.selectionStartInViewport;
406 1 : case _TextSelectionHandlePosition.end:
407 2 : return renderObject.selectionEndInViewport;
408 : }
409 : }
410 : }
411 :
412 : class _TextSelectionHandleOverlayState
413 : extends State<_TextSelectionHandleOverlay>
414 : with SingleTickerProviderStateMixin {
415 : late Offset _dragPosition;
416 :
417 : late AnimationController _controller;
418 3 : Animation<double> get _opacity => _controller.view;
419 :
420 1 : @override
421 : void initState() {
422 1 : super.initState();
423 :
424 2 : _controller = AnimationController(
425 : duration: TextSelectionOverlay.fadeDuration, vsync: this);
426 :
427 1 : _handleVisibilityChanged();
428 4 : widget._visibility.addListener(_handleVisibilityChanged);
429 : }
430 :
431 1 : void _handleVisibilityChanged() {
432 3 : if (widget._visibility.value) {
433 2 : _controller.forward();
434 : } else {
435 0 : _controller.reverse();
436 : }
437 : }
438 :
439 1 : @override
440 : void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) {
441 1 : super.didUpdateWidget(oldWidget);
442 3 : oldWidget._visibility.removeListener(_handleVisibilityChanged);
443 1 : _handleVisibilityChanged();
444 4 : widget._visibility.addListener(_handleVisibilityChanged);
445 : }
446 :
447 1 : @override
448 : void dispose() {
449 4 : widget._visibility.removeListener(_handleVisibilityChanged);
450 2 : _controller.dispose();
451 1 : super.dispose();
452 : }
453 :
454 0 : void _handleDragStart(DragStartDetails details) {
455 0 : final handleSize = widget.selectionControls!.getHandleSize(
456 0 : widget.renderObject.preferredLineWidth,
457 : );
458 0 : _dragPosition = details.globalPosition + Offset(-handleSize.width, 0.0);
459 : }
460 :
461 0 : void _handleDragUpdate(DragUpdateDetails details) {
462 0 : _dragPosition += details.delta;
463 0 : final position = widget.renderObject.getPositionForPoint(_dragPosition);
464 :
465 0 : if (widget.selection.isCollapsed) {
466 0 : widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
467 : return;
468 : }
469 :
470 : final TextSelection newSelection;
471 0 : switch (widget.position) {
472 0 : case _TextSelectionHandlePosition.start:
473 0 : newSelection = TextSelection(
474 0 : baseOffset: position.offset,
475 0 : extentOffset: widget.selection.extentOffset,
476 : );
477 : break;
478 0 : case _TextSelectionHandlePosition.end:
479 0 : newSelection = TextSelection(
480 0 : baseOffset: widget.selection.baseOffset,
481 0 : extentOffset: position.offset,
482 : );
483 : break;
484 : }
485 :
486 0 : if (newSelection.baseOffset >= newSelection.extentOffset) {
487 : return; // don't allow order swapping.
488 : }
489 :
490 0 : widget.onSelectionHandleChanged(newSelection);
491 : }
492 :
493 1 : void _handleTap() {
494 2 : if (widget.onSelectionHandleTapped != null) {
495 3 : widget.onSelectionHandleTapped!();
496 : }
497 : }
498 :
499 1 : @override
500 : Widget build(BuildContext context) {
501 : final LayerLink layerLink;
502 : final TextSelectionHandleType type;
503 :
504 2 : switch (widget.position) {
505 1 : case _TextSelectionHandlePosition.start:
506 2 : layerLink = widget.startHandleLayerLink;
507 1 : type = _chooseType(TextSelectionHandleType.left);
508 : break;
509 1 : case _TextSelectionHandlePosition.end:
510 : // For collapsed selections, we shouldn't be building the [end] handle.
511 3 : assert(!widget.selection.isCollapsed);
512 2 : layerLink = widget.endHandleLayerLink;
513 1 : type = _chooseType(TextSelectionHandleType.right);
514 : break;
515 : }
516 :
517 3 : final handleAnchor = widget.selectionControls!.getHandleAnchor(
518 : type,
519 3 : widget.renderObject.preferredLineWidth,
520 : );
521 3 : final handleSize = widget.selectionControls!.getHandleSize(
522 3 : widget.renderObject.preferredLineWidth,
523 : );
524 :
525 1 : final handleRect = Rect.fromLTWH(
526 2 : -handleAnchor.dx,
527 2 : -handleAnchor.dy,
528 1 : handleSize.width,
529 1 : handleSize.height,
530 : );
531 :
532 : // Make sure the GestureDetector is big enough to be easily interactive.
533 1 : final interactiveRect = handleRect.expandToInclude(
534 1 : Rect.fromCircle(
535 2 : center: handleRect.center, radius: kMinInteractiveDimension / 2),
536 : );
537 1 : final padding = RelativeRect.fromLTRB(
538 5 : math.max((interactiveRect.width - handleRect.width) / 2, 0),
539 5 : math.max((interactiveRect.height - handleRect.height) / 2, 0),
540 5 : math.max((interactiveRect.width - handleRect.width) / 2, 0),
541 5 : math.max((interactiveRect.height - handleRect.height) / 2, 0),
542 : );
543 :
544 1 : return CompositedTransformFollower(
545 : link: layerLink,
546 1 : offset: interactiveRect.topLeft,
547 : showWhenUnlinked: false,
548 1 : child: FadeTransition(
549 1 : opacity: _opacity,
550 1 : child: Container(
551 : alignment: Alignment.topLeft,
552 1 : width: interactiveRect.width,
553 1 : height: interactiveRect.height,
554 1 : child: GestureDetector(
555 : behavior: HitTestBehavior.translucent,
556 2 : dragStartBehavior: widget.dragStartBehavior,
557 1 : onPanStart: _handleDragStart,
558 1 : onPanUpdate: _handleDragUpdate,
559 1 : onTap: _handleTap,
560 1 : child: Padding(
561 1 : padding: EdgeInsets.only(
562 1 : left: padding.left,
563 1 : top: padding.top,
564 1 : right: padding.right,
565 1 : bottom: padding.bottom,
566 : ),
567 3 : child: widget.selectionControls!.buildHandle(
568 : context,
569 : type,
570 3 : widget.renderObject.preferredLineWidth,
571 : ),
572 : ),
573 : ),
574 : ),
575 : ),
576 : );
577 : }
578 :
579 1 : TextSelectionHandleType _chooseType(TextSelectionHandleType type) {
580 3 : if (widget.selection.isCollapsed) {
581 : return TextSelectionHandleType.collapsed;
582 : }
583 : return type;
584 : }
585 : }
586 :
587 : /// Delegate interface for the [MongolTextSelectionGestureDetectorBuilder].
588 : ///
589 : /// The interface is usually implemented by textfield implementations wrapping
590 : /// [MongolEditableText], that use a [MongolTextSelectionGestureDetectorBuilder] to build a
591 : /// [TextSelectionGestureDetector] for their [MongolEditableText]. The delegate provides
592 : /// the builder with information about the current state of the textfield.
593 : /// Based on this information, the builder adds the correct gesture handlers
594 : /// to the gesture detector.
595 : ///
596 : /// See also:
597 : ///
598 : /// * [MongolTextField], which implements this delegate for the Material textfield.
599 : abstract class MongolTextSelectionGestureDetectorBuilderDelegate {
600 : /// [GlobalKey] to the [MongolEditableText] for which the
601 : /// [MongolTextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
602 : GlobalKey<MongolEditableTextState> get editableTextKey;
603 :
604 : /// Whether the text field should respond to force presses.
605 : bool get forcePressEnabled;
606 :
607 : /// Whether the user may select text in the text field.
608 : bool get selectionEnabled;
609 : }
610 :
611 : /// Builds a [TextSelectionGestureDetector] to wrap an [MongolEditableText].
612 : ///
613 : /// The class implements sensible defaults for many user interactions
614 : /// with an [MongolEditableText] (see the documentation of the various gesture handler
615 : /// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of
616 : /// [MongolTextSelectionGestureDetectorBuilder] can change the behavior performed in
617 : /// responds to these gesture events by overriding the corresponding handler
618 : /// methods of this class.
619 : ///
620 : /// The resulting [TextSelectionGestureDetector] to wrap an [MongolEditableText] is
621 : /// obtained by calling [buildGestureDetector].
622 : ///
623 : /// See also:
624 : ///
625 : /// * [MongolTextField], which uses a subclass to implement the Material-specific
626 : /// gesture logic of an [MongolEditableText].
627 : class MongolTextSelectionGestureDetectorBuilder {
628 : /// Creates a [MongolTextSelectionGestureDetectorBuilder].
629 1 : MongolTextSelectionGestureDetectorBuilder({
630 : required this.delegate,
631 : });
632 :
633 : /// The delegate for this [MongolTextSelectionGestureDetectorBuilder].
634 : ///
635 : /// The delegate provides the builder with information about what actions can
636 : /// currently be performed on the text field. Based on this, the builder adds
637 : /// the correct gesture handlers to the gesture detector.
638 : @protected
639 : final MongolTextSelectionGestureDetectorBuilderDelegate delegate;
640 :
641 : /// Whether to show the selection toolbar.
642 : ///
643 : /// It is based on the signal source when a [onTapDown] is called. This getter
644 : /// will return true if current [onTapDown] event is triggered by a touch or
645 : /// a stylus.
646 2 : bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar;
647 : bool _shouldShowSelectionToolbar = true;
648 :
649 : /// The [State] of the [EditableText] for which the builder will provide a
650 : /// [TextSelectionGestureDetector].
651 1 : @protected
652 : MongolEditableTextState get editableText =>
653 3 : delegate.editableTextKey.currentState!;
654 :
655 : /// The [RenderObject] of the [MongolEditableText] for which the builder will
656 : /// provide a [TextSelectionGestureDetector].
657 1 : @protected
658 2 : MongolRenderEditable get renderEditable => editableText.renderEditable;
659 :
660 : /// Handler for [TextSelectionGestureDetector.onTapDown].
661 : ///
662 : /// By default, it forwards the tap to [MongolRenderEditable.handleTapDown] and sets
663 : /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus.
664 : ///
665 : /// See also:
666 : ///
667 : /// * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
668 1 : @protected
669 : void onTapDown(TapDownDetails details) {
670 2 : renderEditable.handleTapDown(details);
671 : // The selection overlay should only be shown when the user is interacting
672 : // through a touch screen (via either a finger or a stylus). A mouse shouldn't
673 : // trigger the selection overlay.
674 : // For backwards-compatibility, we treat a null kind the same as touch.
675 1 : final kind = details.kind;
676 1 : _shouldShowSelectionToolbar = kind == null ||
677 1 : kind == PointerDeviceKind.touch ||
678 1 : kind == PointerDeviceKind.stylus;
679 : }
680 :
681 : /// Handler for [TextSelectionGestureDetector.onForcePressStart].
682 : ///
683 : /// By default, it selects the word at the position of the force press,
684 : /// if selection is enabled.
685 : ///
686 : /// This callback is only applicable when force press is enabled.
687 : ///
688 : /// See also:
689 : ///
690 : /// * [TextSelectionGestureDetector.onForcePressStart], which triggers this
691 : /// callback.
692 0 : @protected
693 : void onForcePressStart(ForcePressDetails details) {
694 0 : assert(delegate.forcePressEnabled);
695 0 : _shouldShowSelectionToolbar = true;
696 0 : if (delegate.selectionEnabled) {
697 0 : renderEditable.selectWordsInRange(
698 0 : from: details.globalPosition,
699 : cause: SelectionChangedCause.forcePress,
700 : );
701 : }
702 : }
703 :
704 : /// Handler for [TextSelectionGestureDetector.onForcePressEnd].
705 : ///
706 : /// By default, it selects words in the range specified in [details] and shows
707 : /// toolbar if it is necessary.
708 : ///
709 : /// This callback is only applicable when force press is enabled.
710 : ///
711 : /// See also:
712 : ///
713 : /// * [TextSelectionGestureDetector.onForcePressEnd], which triggers this
714 : /// callback.
715 0 : @protected
716 : void onForcePressEnd(ForcePressDetails details) {
717 0 : assert(delegate.forcePressEnabled);
718 0 : renderEditable.selectWordsInRange(
719 0 : from: details.globalPosition,
720 : cause: SelectionChangedCause.forcePress,
721 : );
722 0 : if (shouldShowSelectionToolbar) editableText.showToolbar();
723 : }
724 :
725 : /// Handler for [TextSelectionGestureDetector.onSingleTapUp].
726 : ///
727 : /// By default, it selects word edge if selection is enabled.
728 : ///
729 : /// See also:
730 : ///
731 : /// * [TextSelectionGestureDetector.onSingleTapUp], which triggers
732 : /// this callback.
733 0 : @protected
734 : void onSingleTapUp(TapUpDetails details) {
735 0 : if (delegate.selectionEnabled) {
736 0 : renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
737 : }
738 : }
739 :
740 : /// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
741 : ///
742 : /// By default, it services as place holder to enable subclass override.
743 : ///
744 : /// See also:
745 : ///
746 : /// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers
747 : /// this callback.
748 1 : @protected
749 : void onSingleTapCancel() {
750 : /* Subclass should override this method if needed. */
751 : }
752 :
753 : /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart].
754 : ///
755 : /// By default, it selects text position specified in [details] if selection
756 : /// is enabled.
757 : ///
758 : /// See also:
759 : ///
760 : /// * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers
761 : /// this callback.
762 0 : @protected
763 : void onSingleLongTapStart(LongPressStartDetails details) {
764 0 : if (delegate.selectionEnabled) {
765 0 : renderEditable.selectPositionAt(
766 0 : from: details.globalPosition,
767 : cause: SelectionChangedCause.longPress,
768 : );
769 : }
770 : }
771 :
772 : /// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate].
773 : ///
774 : /// By default, it updates the selection location specified in [details] if
775 : /// selection is enabled.
776 : ///
777 : /// See also:
778 : ///
779 : /// * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which
780 : /// triggers this callback.
781 0 : @protected
782 : void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
783 0 : if (delegate.selectionEnabled) {
784 0 : renderEditable.selectPositionAt(
785 0 : from: details.globalPosition,
786 : cause: SelectionChangedCause.longPress,
787 : );
788 : }
789 : }
790 :
791 : /// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
792 : ///
793 : /// By default, it shows toolbar if necessary.
794 : ///
795 : /// See also:
796 : ///
797 : /// * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this
798 : /// callback.
799 1 : @protected
800 : void onSingleLongTapEnd(LongPressEndDetails details) {
801 3 : if (shouldShowSelectionToolbar) editableText.showToolbar();
802 : }
803 :
804 : /// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
805 : ///
806 : /// By default, it selects a word through [MongolRenderEditable.selectWord] if
807 : /// selectionEnabled and shows toolbar if necessary.
808 : ///
809 : /// See also:
810 : ///
811 : /// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
812 : /// callback.
813 1 : @protected
814 : void onDoubleTapDown(TapDownDetails details) {
815 2 : if (delegate.selectionEnabled) {
816 2 : renderEditable.selectWord(cause: SelectionChangedCause.tap);
817 3 : if (shouldShowSelectionToolbar) editableText.showToolbar();
818 : }
819 : }
820 :
821 : /// Handler for [TextSelectionGestureDetector.onDragSelectionStart].
822 : ///
823 : /// By default, it selects a text position specified in [details].
824 : ///
825 : /// See also:
826 : ///
827 : /// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers
828 : /// this callback.
829 0 : @protected
830 : void onDragSelectionStart(DragStartDetails details) {
831 0 : final kind = details.kind;
832 0 : _shouldShowSelectionToolbar = kind == null ||
833 0 : kind == PointerDeviceKind.touch ||
834 0 : kind == PointerDeviceKind.stylus;
835 :
836 0 : renderEditable.selectPositionAt(
837 0 : from: details.globalPosition,
838 : cause: SelectionChangedCause.drag,
839 : );
840 : }
841 :
842 : /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
843 : ///
844 : /// By default, it updates the selection location specified in the provided
845 : /// details objects.
846 : ///
847 : /// See also:
848 : ///
849 : /// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
850 : /// this callback./lib/src/material/text_field.dart
851 0 : @protected
852 : void onDragSelectionUpdate(
853 : DragStartDetails startDetails, DragUpdateDetails updateDetails) {
854 0 : renderEditable.selectPositionAt(
855 0 : from: startDetails.globalPosition,
856 0 : to: updateDetails.globalPosition,
857 : cause: SelectionChangedCause.drag,
858 : );
859 : }
860 :
861 : /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
862 : ///
863 : /// By default, it services as place holder to enable subclass override.
864 : ///
865 : /// See also:
866 : ///
867 : /// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
868 : /// callback.
869 0 : @protected
870 : void onDragSelectionEnd(DragEndDetails details) {
871 : /* Subclass should override this method if needed. */
872 : }
873 :
874 : /// Returns a [TextSelectionGestureDetector] configured with the handlers
875 : /// provided by this builder.
876 : ///
877 : /// The [child] or its subtree should contain [EditableText].
878 1 : Widget buildGestureDetector({
879 : Key? key,
880 : HitTestBehavior? behavior,
881 : required Widget child,
882 : }) {
883 1 : return TextSelectionGestureDetector(
884 : key: key,
885 1 : onTapDown: onTapDown,
886 3 : onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
887 3 : onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
888 1 : onSingleTapUp: onSingleTapUp,
889 1 : onSingleTapCancel: onSingleTapCancel,
890 1 : onSingleLongTapStart: onSingleLongTapStart,
891 1 : onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
892 1 : onSingleLongTapEnd: onSingleLongTapEnd,
893 1 : onDoubleTapDown: onDoubleTapDown,
894 1 : onDragSelectionStart: onDragSelectionStart,
895 1 : onDragSelectionUpdate: onDragSelectionUpdate,
896 1 : onDragSelectionEnd: onDragSelectionEnd,
897 : behavior: behavior,
898 : child: child,
899 : );
900 : }
901 : }
|