1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6/// @docImport 'package:flutter_test/flutter_test.dart';
7///
8/// @docImport 'editable_text.dart';
9/// @docImport 'text.dart';
10library;
11
12import 'dart:async';
13import 'dart:math';
14
15import 'package:flutter/foundation.dart';
16import 'package:flutter/gestures.dart';
17import 'package:flutter/rendering.dart';
18import 'package:flutter/scheduler.dart';
19import 'package:flutter/services.dart';
20import 'package:vector_math/vector_math_64.dart';
21
22import 'actions.dart';
23import 'basic.dart';
24import 'context_menu_button_item.dart';
25import 'debug.dart';
26import 'focus_manager.dart';
27import 'focus_scope.dart';
28import 'framework.dart';
29import 'gesture_detector.dart';
30import 'magnifier.dart';
31import 'media_query.dart';
32import 'overlay.dart';
33import 'platform_selectable_region_context_menu.dart';
34import 'selection_container.dart';
35import 'text_editing_intents.dart';
36import 'text_selection.dart';
37import 'text_selection_toolbar_anchors.dart';
38
39// Examples can assume:
40// late GlobalKey key;
41
42const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
43 PointerDeviceKind.touch,
44 PointerDeviceKind.stylus,
45 PointerDeviceKind.invertedStylus,
46};
47
48// In practice some selectables like widgetspan shift several pixels. So when
49// the vertical position diff is within the threshold, compare the horizontal
50// position to make the compareScreenOrder function more robust.
51const double _kSelectableVerticalComparingThreshold = 3.0;
52
53/// A widget that introduces an area for user selections.
54///
55/// Flutter widgets are not selectable by default. Wrapping a widget subtree
56/// with a [SelectableRegion] widget enables selection within that subtree (for
57/// example, [Text] widgets automatically look for selectable regions to enable
58/// selection). The wrapped subtree can be selected by users using mouse or
59/// touch gestures, e.g. users can select widgets by holding the mouse
60/// left-click and dragging across widgets, or they can use long press gestures
61/// to select words on touch devices.
62///
63/// A [SelectableRegion] widget requires configuration; in particular specific
64/// [selectionControls] must be provided.
65///
66/// The [SelectionArea] widget from the [material] library configures a
67/// [SelectableRegion] in a platform-specific manner (e.g. using a Material
68/// toolbar on Android, a Cupertino toolbar on iOS), and it may therefore be
69/// simpler to use that widget rather than using [SelectableRegion] directly.
70///
71/// ## An overview of the selection system.
72///
73/// Every [Selectable] under the [SelectableRegion] can be selected. They form a
74/// selection tree structure to handle the selection.
75///
76/// The [SelectableRegion] is a wrapper over [SelectionContainer]. It listens to
77/// user gestures and sends corresponding [SelectionEvent]s to the
78/// [SelectionContainer] it creates.
79///
80/// A [SelectionContainer] is a single [Selectable] that handles
81/// [SelectionEvent]s on behalf of child [Selectable]s in the subtree. It
82/// creates a [SelectionRegistrarScope] with its [SelectionContainer.delegate]
83/// to collect child [Selectable]s and sends the [SelectionEvent]s it receives
84/// from the parent [SelectionRegistrar] to the appropriate child [Selectable]s.
85/// It creates an abstraction for the parent [SelectionRegistrar] as if it is
86/// interacting with a single [Selectable].
87///
88/// The [SelectionContainer] created by [SelectableRegion] is the root node of a
89/// selection tree. Each non-leaf node in the tree is a [SelectionContainer],
90/// and the leaf node is a leaf widget whose render object implements
91/// [Selectable]. They are connected through [SelectionRegistrarScope]s created
92/// by [SelectionContainer]s.
93///
94/// Both [SelectionContainer]s and the leaf [Selectable]s need to register
95/// themselves to the [SelectionRegistrar] from the
96/// [SelectionContainer.maybeOf] if they want to participate in the
97/// selection.
98///
99/// An example selection tree will look like:
100///
101/// {@tool snippet}
102///
103/// ```dart
104/// MaterialApp(
105/// home: SelectableRegion(
106/// selectionControls: materialTextSelectionControls,
107/// child: Scaffold(
108/// appBar: AppBar(title: const Text('Flutter Code Sample')),
109/// body: ListView(
110/// children: const <Widget>[
111/// Text('Item 0', style: TextStyle(fontSize: 50.0)),
112/// Text('Item 1', style: TextStyle(fontSize: 50.0)),
113/// ],
114/// ),
115/// ),
116/// ),
117/// )
118/// ```
119/// {@end-tool}
120///
121///
122/// SelectionContainer
123/// (SelectableRegion)
124/// / \
125/// / \
126/// / \
127/// Selectable \
128/// ("Flutter Code Sample") \
129/// \
130/// SelectionContainer
131/// (ListView)
132/// / \
133/// / \
134/// / \
135/// Selectable Selectable
136/// ("Item 0") ("Item 1")
137///
138///
139/// ## Making a widget selectable
140///
141/// Some leaf widgets, such as [Text], have all of the selection logic wired up
142/// automatically and can be selected as long as they are under a
143/// [SelectableRegion].
144///
145/// To make a custom selectable widget, its render object needs to mix in
146/// [Selectable] and implement the required APIs to handle [SelectionEvent]s
147/// as well as paint appropriate selection highlights.
148///
149/// The render object also needs to register itself to a [SelectionRegistrar].
150/// For the most cases, one can use [SelectionRegistrant] to auto-register
151/// itself with the register returned from [SelectionContainer.maybeOf] as
152/// seen in the example below.
153///
154/// {@tool dartpad}
155/// This sample demonstrates how to create an adapter widget that makes any
156/// child widget selectable.
157///
158/// ** See code in examples/api/lib/material/selectable_region/selectable_region.0.dart **
159/// {@end-tool}
160///
161/// ## Complex layout
162///
163/// By default, the screen order is used as the selection order. If a group of
164/// [Selectable]s needs to select differently, consider wrapping them with a
165/// [SelectionContainer] to customize its selection behavior.
166///
167/// {@tool dartpad}
168/// This sample demonstrates how to create a [SelectionContainer] that only
169/// allows selecting everything or nothing with no partial selection.
170///
171/// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart **
172/// {@end-tool}
173///
174/// In the case where a group of widgets should be excluded from selection under
175/// a [SelectableRegion], consider wrapping that group of widgets using
176/// [SelectionContainer.disabled].
177///
178/// {@tool dartpad}
179/// This sample demonstrates how to disable selection for a Text in a Column.
180///
181/// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart **
182/// {@end-tool}
183///
184/// To create a separate selection system from its parent selection area,
185/// wrap part of the subtree with another [SelectableRegion]. The selection of the
186/// child selection area can not extend past its subtree, and the selection of
187/// the parent selection area can not extend inside the child selection area.
188///
189/// ## Selection status
190///
191/// A [SelectableRegion]s [SelectableRegionSelectionStatus] is used to indicate whether
192/// the [SelectableRegion] is actively changing the selection, or has finalized it. For
193/// example, during a mouse click + drag, the [SelectableRegionSelectionStatus] will be
194/// set to [SelectableRegionSelectionStatus.changing], and when the mouse click is released
195/// the status will be set to [SelectableRegionSelectionStatus.finalized].
196///
197/// The default value of [SelectableRegion]s selection status
198/// is [SelectableRegionSelectionStatus.finalized].
199///
200/// To access the [SelectableRegionSelectionStatus] of a parent [SelectableRegion]
201/// use [SelectableRegionSelectionStatusScope.maybeOf] and retrieve the value from
202/// the [ValueListenable].
203///
204/// One can also listen for changes to the [SelectableRegionSelectionStatus] by
205/// adding a listener to the [ValueListenable] retrieved from [SelectableRegionSelectionStatusScope.maybeOf]
206/// through [ValueListenable.addListener]. In Stateful widgets this is typically
207/// done in [State.didChangeDependencies]. Remove the listener when no longer
208/// needed, typically in your Stateful widgets [State.dispose] method through
209/// [ValueListenable.removeListener].
210///
211/// ## Tests
212///
213/// In a test, a region can be selected either by faking drag events (e.g. using
214/// [WidgetTester.dragFrom]) or by sending intents to a widget inside the region
215/// that has been given a [GlobalKey], e.g.:
216///
217/// ```dart
218/// Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard));
219/// ```
220///
221/// See also:
222///
223/// * [SelectionArea], which creates a [SelectableRegion] with
224/// platform-adaptive selection controls.
225/// * [SelectableText], which enables selection on a single run of text.
226/// * [SelectionHandler], which contains APIs to handle selection events from the
227/// [SelectableRegion].
228/// * [Selectable], which provides API to participate in the selection system.
229/// * [SelectionRegistrar], which [Selectable] needs to subscribe to receive
230/// selection events.
231/// * [SelectionContainer], which collects selectable widgets in the subtree
232/// and provides api to dispatch selection event to the collected widget.
233/// * [SelectionListener], which enables accessing the [SelectionDetails] of
234/// the selectable subtree it wraps.
235class SelectableRegion extends StatefulWidget {
236 /// Create a new [SelectableRegion] widget.
237 ///
238 /// The [selectionControls] are used for building the selection handles and
239 /// toolbar for mobile devices.
240 const SelectableRegion({
241 super.key,
242 this.contextMenuBuilder,
243 this.focusNode,
244 this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
245 this.onSelectionChanged,
246 required this.selectionControls,
247 required this.child,
248 });
249
250 /// The configuration for the magnifier used with selections in this region.
251 ///
252 /// By default, [SelectableRegion]'s [TextMagnifierConfiguration] is disabled.
253 /// For a version of [SelectableRegion] that adapts automatically to the
254 /// current platform, consider [SelectionArea].
255 ///
256 /// {@macro flutter.widgets.magnifier.intro}
257 final TextMagnifierConfiguration magnifierConfiguration;
258
259 /// {@macro flutter.widgets.Focus.focusNode}
260 final FocusNode? focusNode;
261
262 /// The child widget this selection area applies to.
263 ///
264 /// {@macro flutter.widgets.ProxyWidget.child}
265 final Widget child;
266
267 /// {@macro flutter.widgets.EditableText.contextMenuBuilder}
268 final SelectableRegionContextMenuBuilder? contextMenuBuilder;
269
270 /// The delegate to build the selection handles and toolbar for mobile
271 /// devices.
272 ///
273 /// The [emptyTextSelectionControls] global variable provides a default
274 /// [TextSelectionControls] implementation with no controls.
275 final TextSelectionControls selectionControls;
276
277 /// Called when the selected content changes.
278 final ValueChanged<SelectedContent?>? onSelectionChanged;
279
280 /// Returns the [ContextMenuButtonItem]s representing the buttons in this
281 /// platform's default selection menu.
282 ///
283 /// For example, [SelectableRegion] uses this to generate the default buttons
284 /// for its context menu.
285 ///
286 /// See also:
287 ///
288 /// * [SelectableRegionState.contextMenuButtonItems], which gives the
289 /// [ContextMenuButtonItem]s for a specific SelectableRegion.
290 /// * [EditableText.getEditableButtonItems], which performs a similar role but
291 /// for content that is both selectable and editable.
292 /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
293 /// take a list of [ContextMenuButtonItem]s with
294 /// [AdaptiveTextSelectionToolbar.buttonItems].
295 /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
296 /// Widgets for the current platform given [ContextMenuButtonItem]s.
297 static List<ContextMenuButtonItem> getSelectableButtonItems({
298 required final SelectionGeometry selectionGeometry,
299 required final VoidCallback onCopy,
300 required final VoidCallback onSelectAll,
301 required final VoidCallback? onShare,
302 }) {
303 final bool canCopy = selectionGeometry.status == SelectionStatus.uncollapsed;
304 final bool canSelectAll = selectionGeometry.hasContent;
305 final bool platformCanShare = switch (defaultTargetPlatform) {
306 TargetPlatform.android => selectionGeometry.status == SelectionStatus.uncollapsed,
307 TargetPlatform.macOS ||
308 TargetPlatform.fuchsia ||
309 TargetPlatform.linux ||
310 TargetPlatform.windows => false,
311 // TODO(bleroux): the share button should be shown on iOS but the share
312 // functionality requires some changes on the engine side because, on iPad,
313 // it needs an anchor for the popup.
314 // See: https://github.com/flutter/flutter/issues/141775.
315 TargetPlatform.iOS => false,
316 };
317 final bool canShare = onShare != null && platformCanShare;
318
319 // On Android, the share button is before the select all button.
320 final bool showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android;
321
322 // Determine which buttons will appear so that the order and total number is
323 // known. A button's position in the menu can slightly affect its
324 // appearance.
325 return <ContextMenuButtonItem>[
326 if (canCopy) ContextMenuButtonItem(onPressed: onCopy, type: ContextMenuButtonType.copy),
327 if (canShare && showShareBeforeSelectAll)
328 ContextMenuButtonItem(onPressed: onShare, type: ContextMenuButtonType.share),
329 if (canSelectAll)
330 ContextMenuButtonItem(onPressed: onSelectAll, type: ContextMenuButtonType.selectAll),
331 if (canShare && !showShareBeforeSelectAll)
332 ContextMenuButtonItem(onPressed: onShare, type: ContextMenuButtonType.share),
333 ];
334 }
335
336 @override
337 State<StatefulWidget> createState() => SelectableRegionState();
338}
339
340/// State for a [SelectableRegion].
341class SelectableRegionState extends State<SelectableRegion>
342 with TextSelectionDelegate
343 implements SelectionRegistrar {
344 late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
345 SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
346 CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
347 ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(
348 _GranularlyExtendSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(
349 this,
350 granularity: TextGranularity.word,
351 ),
352 ),
353 ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(
354 _GranularlyExtendSelectionAction<ExpandSelectionToDocumentBoundaryIntent>(
355 this,
356 granularity: TextGranularity.document,
357 ),
358 ),
359 ExpandSelectionToLineBreakIntent: _makeOverridable(
360 _GranularlyExtendSelectionAction<ExpandSelectionToLineBreakIntent>(
361 this,
362 granularity: TextGranularity.line,
363 ),
364 ),
365 ExtendSelectionByCharacterIntent: _makeOverridable(
366 _GranularlyExtendCaretSelectionAction<ExtendSelectionByCharacterIntent>(
367 this,
368 granularity: TextGranularity.character,
369 ),
370 ),
371 ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(
372 _GranularlyExtendCaretSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(
373 this,
374 granularity: TextGranularity.word,
375 ),
376 ),
377 ExtendSelectionToLineBreakIntent: _makeOverridable(
378 _GranularlyExtendCaretSelectionAction<ExtendSelectionToLineBreakIntent>(
379 this,
380 granularity: TextGranularity.line,
381 ),
382 ),
383 ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(
384 _DirectionallyExtendCaretSelectionAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this),
385 ),
386 ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(
387 _GranularlyExtendCaretSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(
388 this,
389 granularity: TextGranularity.document,
390 ),
391 ),
392 };
393
394 final Map<Type, GestureRecognizerFactory> _gestureRecognizers =
395 <Type, GestureRecognizerFactory>{};
396 SelectionOverlay? _selectionOverlay;
397 final LayerLink _startHandleLayerLink = LayerLink();
398 final LayerLink _endHandleLayerLink = LayerLink();
399 final LayerLink _toolbarLayerLink = LayerLink();
400 final StaticSelectionContainerDelegate _selectionDelegate = StaticSelectionContainerDelegate();
401 // there should only ever be one selectable, which is the SelectionContainer.
402 Selectable? _selectable;
403
404 bool get _hasSelectionOverlayGeometry =>
405 _selectionDelegate.value.startSelectionPoint != null ||
406 _selectionDelegate.value.endSelectionPoint != null;
407
408 Orientation? _lastOrientation;
409 SelectedContent? _lastSelectedContent;
410
411 /// The [SelectionOverlay] that is currently visible on the screen.
412 ///
413 /// Can be null if there is no visible [SelectionOverlay].
414 @visibleForTesting
415 SelectionOverlay? get selectionOverlay => _selectionOverlay;
416
417 /// The text processing service used to retrieve the native text processing actions.
418 final ProcessTextService _processTextService = DefaultProcessTextService();
419
420 /// The list of native text processing actions provided by the engine.
421 final List<ProcessTextAction> _processTextActions = <ProcessTextAction>[];
422
423 // The focus node to use if the widget didn't supply one.
424 FocusNode? _localFocusNode;
425 FocusNode get _focusNode =>
426 widget.focusNode ?? (_localFocusNode ??= FocusNode(debugLabel: 'SelectableRegion'));
427
428 /// Notifies its listeners when the selection state in this [SelectableRegion] changes.
429 final _SelectableRegionSelectionStatusNotifier _selectionStatusNotifier =
430 _SelectableRegionSelectionStatusNotifier._();
431
432 @protected
433 @override
434 void initState() {
435 super.initState();
436 _focusNode.addListener(_handleFocusChanged);
437 _initMouseGestureRecognizer();
438 _initTouchGestureRecognizer();
439 // Right clicks.
440 _gestureRecognizers[TapGestureRecognizer] =
441 GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
442 () => TapGestureRecognizer(debugOwner: this),
443 (TapGestureRecognizer instance) {
444 instance.onSecondaryTapDown = _handleRightClickDown;
445 },
446 );
447 _initProcessTextActions();
448 }
449
450 /// Query the engine to initialize the list of text processing actions to show
451 /// in the text selection toolbar.
452 Future<void> _initProcessTextActions() async {
453 _processTextActions.clear();
454 _processTextActions.addAll(await _processTextService.queryTextActions());
455 }
456
457 @protected
458 @override
459 void didChangeDependencies() {
460 super.didChangeDependencies();
461 switch (defaultTargetPlatform) {
462 case TargetPlatform.android:
463 case TargetPlatform.iOS:
464 break;
465 case TargetPlatform.fuchsia:
466 case TargetPlatform.linux:
467 case TargetPlatform.macOS:
468 case TargetPlatform.windows:
469 return;
470 }
471
472 // Hide the text selection toolbar on mobile when orientation changes.
473 final Orientation orientation = MediaQuery.orientationOf(context);
474 if (_lastOrientation == null) {
475 _lastOrientation = orientation;
476 return;
477 }
478 if (orientation != _lastOrientation) {
479 _lastOrientation = orientation;
480 hideToolbar(defaultTargetPlatform == TargetPlatform.android);
481 }
482 }
483
484 @protected
485 @override
486 void didUpdateWidget(SelectableRegion oldWidget) {
487 super.didUpdateWidget(oldWidget);
488 if (widget.focusNode != oldWidget.focusNode) {
489 if (oldWidget.focusNode == null && widget.focusNode != null) {
490 _localFocusNode?.removeListener(_handleFocusChanged);
491 _localFocusNode?.dispose();
492 _localFocusNode = null;
493 } else if (widget.focusNode == null && oldWidget.focusNode != null) {
494 oldWidget.focusNode!.removeListener(_handleFocusChanged);
495 }
496 _focusNode.addListener(_handleFocusChanged);
497 if (_focusNode.hasFocus != oldWidget.focusNode?.hasFocus) {
498 _handleFocusChanged();
499 }
500 }
501 }
502
503 Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) {
504 return Action<T>.overridable(context: context, defaultAction: defaultAction);
505 }
506
507 void _handleFocusChanged() {
508 if (!_focusNode.hasFocus) {
509 if (kIsWeb) {
510 PlatformSelectableRegionContextMenu.detach(_selectionDelegate);
511 }
512 if (SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) {
513 // We should only clear the selection when this SelectableRegion loses
514 // focus while the application is currently running. It is possible
515 // that the application is not currently running, for example on desktop
516 // platforms, clicking on a different window switches the focus to
517 // the new window causing the Flutter application to go inactive. In this
518 // case we want to retain the selection so it remains when we return to
519 // the Flutter application.
520 clearSelection();
521 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
522 _finalizeSelectableRegionStatus();
523 }
524 }
525 if (kIsWeb) {
526 PlatformSelectableRegionContextMenu.attach(_selectionDelegate);
527 }
528 }
529
530 void _updateSelectionStatus() {
531 final SelectionGeometry geometry = _selectionDelegate.value;
532 final TextSelection selection = switch (geometry.status) {
533 SelectionStatus.uncollapsed ||
534 SelectionStatus.collapsed => const TextSelection(baseOffset: 0, extentOffset: 1),
535 SelectionStatus.none => const TextSelection.collapsed(offset: 1),
536 };
537 textEditingValue = TextEditingValue(text: '__', selection: selection);
538 if (_hasSelectionOverlayGeometry) {
539 _updateSelectionOverlay();
540 } else {
541 _selectionOverlay?.dispose();
542 _selectionOverlay = null;
543 }
544 }
545
546 // gestures.
547
548 /// Whether the Shift key was pressed when the most recent [PointerDownEvent]
549 /// was tracked by the [BaseTapAndDragGestureRecognizer].
550 bool _isShiftPressed = false;
551
552 // The position of the most recent secondary tap down event on this
553 // SelectableRegion.
554 Offset? _lastSecondaryTapDownPosition;
555
556 // The device kind for the pointer of the most recent tap down event on this
557 // SelectableRegion.
558 PointerDeviceKind? _lastPointerDeviceKind;
559
560 static bool _isPrecisePointerDevice(PointerDeviceKind pointerDeviceKind) {
561 switch (pointerDeviceKind) {
562 case PointerDeviceKind.mouse:
563 return true;
564 case PointerDeviceKind.trackpad:
565 case PointerDeviceKind.stylus:
566 case PointerDeviceKind.invertedStylus:
567 case PointerDeviceKind.touch:
568 case PointerDeviceKind.unknown:
569 return false;
570 }
571 }
572
573 void _finalizeSelectableRegionStatus() {
574 if (_selectionStatusNotifier.value != SelectableRegionSelectionStatus.changing) {
575 // Don't finalize the selection again if it is not currently changing.
576 return;
577 }
578 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.finalized;
579 }
580
581 // Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
582 // which can grow to be infinitely large, to a value between 1 and the supported
583 // max consecutive tap count. The value that the raw count is converted to is
584 // based on the default observed behavior on the native platforms.
585 //
586 // This method should be used in all instances when details.consecutiveTapCount
587 // would be used.
588 int _getEffectiveConsecutiveTapCount(int rawCount) {
589 int maxConsecutiveTap = 3;
590 switch (defaultTargetPlatform) {
591 case TargetPlatform.android:
592 case TargetPlatform.fuchsia:
593 if (_lastPointerDeviceKind != null && _lastPointerDeviceKind != PointerDeviceKind.mouse) {
594 // When the pointer device kind is not precise like a mouse, native
595 // Android resets the tap count at 2. For example, this is so the
596 // selection can collapse on the third tap.
597 maxConsecutiveTap = 2;
598 }
599 // From observation, these platforms reset their tap count to 0 when
600 // the number of consecutive taps exceeds the max consecutive tap supported.
601 // For example on native Android, when going past a triple click,
602 // on the fourth click the selection is moved to the precise click
603 // position, on the fifth click the word at the position is selected, and
604 // on the sixth click the paragraph at the position is selected.
605 return rawCount <= maxConsecutiveTap
606 ? rawCount
607 : (rawCount % maxConsecutiveTap == 0
608 ? maxConsecutiveTap
609 : rawCount % maxConsecutiveTap);
610 case TargetPlatform.linux:
611 // From observation, these platforms reset their tap count to 0 when
612 // the number of consecutive taps exceeds the max consecutive tap supported.
613 // For example on Debian Linux with GTK, when going past a triple click,
614 // on the fourth click the selection is moved to the precise click
615 // position, on the fifth click the word at the position is selected, and
616 // on the sixth click the paragraph at the position is selected.
617 return rawCount <= maxConsecutiveTap
618 ? rawCount
619 : (rawCount % maxConsecutiveTap == 0
620 ? maxConsecutiveTap
621 : rawCount % maxConsecutiveTap);
622 case TargetPlatform.iOS:
623 case TargetPlatform.macOS:
624 case TargetPlatform.windows:
625 // From observation, these platforms hold their tap count at the max
626 // consecutive tap supported. For example on macOS, when going past a triple
627 // click, the selection should be retained at the paragraph that was first
628 // selected on triple click.
629 return min(rawCount, maxConsecutiveTap);
630 }
631 }
632
633 void _initMouseGestureRecognizer() {
634 _gestureRecognizers[TapAndPanGestureRecognizer] =
635 GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
636 () => TapAndPanGestureRecognizer(
637 debugOwner: this,
638 supportedDevices: <PointerDeviceKind>{PointerDeviceKind.mouse},
639 ),
640 (TapAndPanGestureRecognizer instance) {
641 instance
642 ..onTapTrackStart = _onTapTrackStart
643 ..onTapTrackReset = _onTapTrackReset
644 ..onTapDown = _startNewMouseSelectionGesture
645 ..onTapUp = _handleMouseTapUp
646 ..onDragStart = _handleMouseDragStart
647 ..onDragUpdate = _handleMouseDragUpdate
648 ..onDragEnd = _handleMouseDragEnd
649 ..onCancel = clearSelection
650 ..dragStartBehavior = DragStartBehavior.down;
651 },
652 );
653 }
654
655 void _onTapTrackStart() {
656 _isShiftPressed =
657 HardwareKeyboard.instance.logicalKeysPressed.intersection(<LogicalKeyboardKey>{
658 LogicalKeyboardKey.shiftLeft,
659 LogicalKeyboardKey.shiftRight,
660 }).isNotEmpty;
661 }
662
663 void _onTapTrackReset() {
664 _isShiftPressed = false;
665 }
666
667 void _initTouchGestureRecognizer() {
668 // A [TapAndHorizontalDragGestureRecognizer] is used on non-precise pointer devices
669 // like PointerDeviceKind.touch so [SelectableRegion] gestures do not conflict with
670 // ancestor Scrollable gestures in common scenarios like a vertically scrolling list view.
671 _gestureRecognizers[TapAndHorizontalDragGestureRecognizer] =
672 GestureRecognizerFactoryWithHandlers<TapAndHorizontalDragGestureRecognizer>(
673 () => TapAndHorizontalDragGestureRecognizer(
674 debugOwner: this,
675 supportedDevices:
676 PointerDeviceKind.values.where((PointerDeviceKind device) {
677 return device != PointerDeviceKind.mouse;
678 }).toSet(),
679 ),
680 (TapAndHorizontalDragGestureRecognizer instance) {
681 instance
682 // iOS does not provide a device specific touch slop
683 // unlike Android (~8.0), so the touch slop for a [Scrollable]
684 // always default to kTouchSlop which is 18.0. When
685 // [SelectableRegion] is the child of a horizontal
686 // scrollable that means the [SelectableRegion] will
687 // always win the gesture arena when competing with
688 // the ancestor scrollable because they both have
689 // the same touch slop threshold and the child receives
690 // the [PointerEvent] first. To avoid this conflict
691 // and ensure a smooth scrolling experience, on
692 // iOS the [TapAndHorizontalDragGestureRecognizer]
693 // will wait for all other gestures to lose before
694 // declaring victory.
695 ..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS
696 ..onTapDown = _startNewMouseSelectionGesture
697 ..onTapUp = _handleMouseTapUp
698 ..onDragStart = _handleMouseDragStart
699 ..onDragUpdate = _handleMouseDragUpdate
700 ..onDragEnd = _handleMouseDragEnd
701 ..onCancel = clearSelection
702 ..dragStartBehavior = DragStartBehavior.down;
703 },
704 );
705 _gestureRecognizers[LongPressGestureRecognizer] =
706 GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
707 () => LongPressGestureRecognizer(
708 debugOwner: this,
709 supportedDevices: _kLongPressSelectionDevices,
710 ),
711 (LongPressGestureRecognizer instance) {
712 instance
713 ..onLongPressStart = _handleTouchLongPressStart
714 ..onLongPressMoveUpdate = _handleTouchLongPressMoveUpdate
715 ..onLongPressEnd = _handleTouchLongPressEnd;
716 },
717 );
718 }
719
720 Offset? _doubleTapOffset;
721 void _startNewMouseSelectionGesture(TapDragDownDetails details) {
722 _lastPointerDeviceKind = details.kind;
723 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
724 case 1:
725 _focusNode.requestFocus();
726 switch (defaultTargetPlatform) {
727 case TargetPlatform.android:
728 case TargetPlatform.fuchsia:
729 case TargetPlatform.iOS:
730 // On mobile platforms the selection is set on tap up for the first
731 // tap.
732 break;
733 case TargetPlatform.macOS:
734 case TargetPlatform.linux:
735 case TargetPlatform.windows:
736 hideToolbar();
737 // It is impossible to extend the selection when the shift key is
738 // pressed and the start of the selection has not been initialized.
739 // In this case we fallback on collapsing the selection to first
740 // initialize the selection.
741 final bool isShiftPressedValid =
742 _isShiftPressed && _selectionDelegate.value.startSelectionPoint != null;
743 if (isShiftPressedValid) {
744 _selectEndTo(offset: details.globalPosition);
745 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
746 break;
747 }
748 clearSelection();
749 _collapseSelectionAt(offset: details.globalPosition);
750 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
751 }
752 case 2:
753 switch (defaultTargetPlatform) {
754 case TargetPlatform.iOS:
755 if (kIsWeb && details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
756 // Double tap on iOS web triggers when a drag begins after the double tap.
757 _doubleTapOffset = details.globalPosition;
758 break;
759 }
760 _selectWordAt(offset: details.globalPosition);
761 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
762 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
763 _showHandles();
764 }
765 case TargetPlatform.android:
766 case TargetPlatform.fuchsia:
767 case TargetPlatform.macOS:
768 case TargetPlatform.linux:
769 case TargetPlatform.windows:
770 _selectWordAt(offset: details.globalPosition);
771 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
772 }
773 case 3:
774 switch (defaultTargetPlatform) {
775 case TargetPlatform.android:
776 case TargetPlatform.fuchsia:
777 case TargetPlatform.iOS:
778 if (details.kind != null && _isPrecisePointerDevice(details.kind!)) {
779 // Triple tap on static text is only supported on mobile
780 // platforms using a precise pointer device.
781 _selectParagraphAt(offset: details.globalPosition);
782 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
783 }
784 case TargetPlatform.macOS:
785 case TargetPlatform.linux:
786 case TargetPlatform.windows:
787 _selectParagraphAt(offset: details.globalPosition);
788 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
789 }
790 }
791 _updateSelectedContentIfNeeded();
792 }
793
794 void _handleMouseDragStart(TapDragStartDetails details) {
795 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
796 case 1:
797 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
798 // Drag to select is only enabled with a precise pointer device.
799 return;
800 }
801 _selectStartTo(offset: details.globalPosition);
802 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
803 }
804 _updateSelectedContentIfNeeded();
805 }
806
807 void _handleMouseDragUpdate(TapDragUpdateDetails details) {
808 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
809 case 1:
810 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
811 // Drag to select is only enabled with a precise pointer device.
812 return;
813 }
814 _selectEndTo(offset: details.globalPosition, continuous: true);
815 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
816 case 2:
817 switch (defaultTargetPlatform) {
818 case TargetPlatform.android:
819 case TargetPlatform.fuchsia:
820 // Double tap + drag is only supported on Android when using a precise
821 // pointer device or when not on the web.
822 if (!kIsWeb || details.kind != null && _isPrecisePointerDevice(details.kind!)) {
823 _selectEndTo(
824 offset: details.globalPosition,
825 continuous: true,
826 textGranularity: TextGranularity.word,
827 );
828 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
829 }
830 case TargetPlatform.iOS:
831 if (kIsWeb &&
832 details.kind != null &&
833 !_isPrecisePointerDevice(details.kind!) &&
834 _doubleTapOffset != null) {
835 // On iOS web a double tap does not select the word at the position,
836 // until the drag has begun.
837 _selectWordAt(offset: _doubleTapOffset!);
838 _doubleTapOffset = null;
839 }
840 _selectEndTo(
841 offset: details.globalPosition,
842 continuous: true,
843 textGranularity: TextGranularity.word,
844 );
845 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
846 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
847 _showHandles();
848 }
849 case TargetPlatform.macOS:
850 case TargetPlatform.linux:
851 case TargetPlatform.windows:
852 _selectEndTo(
853 offset: details.globalPosition,
854 continuous: true,
855 textGranularity: TextGranularity.word,
856 );
857 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
858 }
859 case 3:
860 switch (defaultTargetPlatform) {
861 case TargetPlatform.android:
862 case TargetPlatform.fuchsia:
863 case TargetPlatform.iOS:
864 // Triple tap + drag is only supported on mobile devices when using
865 // a precise pointer device.
866 if (details.kind != null && _isPrecisePointerDevice(details.kind!)) {
867 _selectEndTo(
868 offset: details.globalPosition,
869 continuous: true,
870 textGranularity: TextGranularity.paragraph,
871 );
872 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
873 }
874 case TargetPlatform.macOS:
875 case TargetPlatform.linux:
876 case TargetPlatform.windows:
877 _selectEndTo(
878 offset: details.globalPosition,
879 continuous: true,
880 textGranularity: TextGranularity.paragraph,
881 );
882 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
883 }
884 }
885 _updateSelectedContentIfNeeded();
886 }
887
888 void _handleMouseDragEnd(TapDragEndDetails details) {
889 assert(_lastPointerDeviceKind != null);
890 final bool isPointerPrecise = _isPrecisePointerDevice(_lastPointerDeviceKind!);
891 // On mobile platforms like android, fuchsia, and iOS, a drag gesture will
892 // only show the selection overlay when the drag has finished and the pointer
893 // device kind is not precise, for example at the end of a double tap + drag
894 // to select on native iOS.
895 final bool shouldShowSelectionOverlayOnMobile = !isPointerPrecise;
896 switch (defaultTargetPlatform) {
897 case TargetPlatform.android:
898 case TargetPlatform.fuchsia:
899 if (shouldShowSelectionOverlayOnMobile) {
900 _showHandles();
901 _showToolbar();
902 }
903 case TargetPlatform.iOS:
904 if (shouldShowSelectionOverlayOnMobile) {
905 _showToolbar();
906 }
907 case TargetPlatform.macOS:
908 case TargetPlatform.linux:
909 case TargetPlatform.windows:
910 // The selection overlay is not shown on desktop platforms after a drag.
911 break;
912 }
913 _finalizeSelection();
914 _updateSelectedContentIfNeeded();
915 _finalizeSelectableRegionStatus();
916 }
917
918 void _handleMouseTapUp(TapDragUpDetails details) {
919 if (defaultTargetPlatform == TargetPlatform.iOS &&
920 _positionIsOnActiveSelection(globalPosition: details.globalPosition)) {
921 // On iOS when the tap occurs on the previous selection, instead of
922 // moving the selection, the context menu will be toggled.
923 final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
924 if (toolbarIsVisible) {
925 hideToolbar(false);
926 } else {
927 _showToolbar();
928 }
929 return;
930 }
931 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
932 case 1:
933 switch (defaultTargetPlatform) {
934 case TargetPlatform.android:
935 case TargetPlatform.fuchsia:
936 case TargetPlatform.iOS:
937 hideToolbar();
938 _collapseSelectionAt(offset: details.globalPosition);
939 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
940 case TargetPlatform.macOS:
941 case TargetPlatform.linux:
942 case TargetPlatform.windows:
943 // On desktop platforms the selection is set on tap down.
944 }
945 case 2:
946 final bool isPointerPrecise = _isPrecisePointerDevice(details.kind);
947 switch (defaultTargetPlatform) {
948 case TargetPlatform.android:
949 case TargetPlatform.fuchsia:
950 if (!isPointerPrecise) {
951 // On Android, a double tap will only show the selection overlay after
952 // the following tap up when the pointer device kind is not precise.
953 _showHandles();
954 _showToolbar();
955 }
956 case TargetPlatform.iOS:
957 if (!isPointerPrecise) {
958 if (kIsWeb) {
959 // Double tap on iOS web only triggers when a drag begins after the double tap.
960 break;
961 }
962 // On iOS, a double tap will only show the selection toolbar after
963 // the following tap up when the pointer device kind is not precise.
964 _showToolbar();
965 }
966 case TargetPlatform.macOS:
967 case TargetPlatform.linux:
968 case TargetPlatform.windows:
969 // The selection overlay is not shown on desktop platforms
970 // on a double click.
971 break;
972 }
973 }
974 _finalizeSelectableRegionStatus();
975 _updateSelectedContentIfNeeded();
976 }
977
978 void _updateSelectedContentIfNeeded() {
979 if (widget.onSelectionChanged == null) {
980 return;
981 }
982 final SelectedContent? content = _selectable?.getSelectedContent();
983 if (_lastSelectedContent?.plainText != content?.plainText) {
984 _lastSelectedContent = content;
985 widget.onSelectionChanged!.call(_lastSelectedContent);
986 }
987 }
988
989 void _handleTouchLongPressStart(LongPressStartDetails details) {
990 HapticFeedback.selectionClick();
991 _focusNode.requestFocus();
992 _selectWordAt(offset: details.globalPosition);
993 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
994 // Platforms besides Android will show the text selection handles when
995 // the long press is initiated. Android shows the text selection handles when
996 // the long press has ended, usually after a pointer up event is received.
997 if (defaultTargetPlatform != TargetPlatform.android) {
998 _showHandles();
999 }
1000 _updateSelectedContentIfNeeded();
1001 }
1002
1003 void _handleTouchLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
1004 _selectEndTo(offset: details.globalPosition, textGranularity: TextGranularity.word);
1005 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1006 _updateSelectedContentIfNeeded();
1007 }
1008
1009 void _handleTouchLongPressEnd(LongPressEndDetails details) {
1010 _finalizeSelection();
1011 _updateSelectedContentIfNeeded();
1012 _finalizeSelectableRegionStatus();
1013 _showToolbar();
1014 if (defaultTargetPlatform == TargetPlatform.android) {
1015 _showHandles();
1016 }
1017 }
1018
1019 bool _positionIsOnActiveSelection({required Offset globalPosition}) {
1020 for (final Rect selectionRect in _selectionDelegate.value.selectionRects) {
1021 final Matrix4 transform = _selectable!.getTransformTo(null);
1022 final Rect globalRect = MatrixUtils.transformRect(transform, selectionRect);
1023 if (globalRect.contains(globalPosition)) {
1024 return true;
1025 }
1026 }
1027 return false;
1028 }
1029
1030 void _handleRightClickDown(TapDownDetails details) {
1031 final Offset? previousSecondaryTapDownPosition = _lastSecondaryTapDownPosition;
1032 final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
1033 _lastSecondaryTapDownPosition = details.globalPosition;
1034 _focusNode.requestFocus();
1035 switch (defaultTargetPlatform) {
1036 case TargetPlatform.android:
1037 case TargetPlatform.fuchsia:
1038 case TargetPlatform.windows:
1039 // If _lastSecondaryTapDownPosition is within the current selection then
1040 // keep the current selection, if not then collapse it.
1041 final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(
1042 globalPosition: details.globalPosition,
1043 );
1044 if (lastSecondaryTapDownPositionWasOnActiveSelection) {
1045 // Restore _lastSecondaryTapDownPosition since it may be cleared if a user
1046 // accesses contextMenuAnchors.
1047 _lastSecondaryTapDownPosition = details.globalPosition;
1048 _showHandles();
1049 _showToolbar(location: _lastSecondaryTapDownPosition);
1050 _updateSelectedContentIfNeeded();
1051 return;
1052 }
1053 _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!);
1054 case TargetPlatform.iOS:
1055 _selectWordAt(offset: _lastSecondaryTapDownPosition!);
1056 case TargetPlatform.macOS:
1057 if (previousSecondaryTapDownPosition == _lastSecondaryTapDownPosition && toolbarIsVisible) {
1058 hideToolbar();
1059 return;
1060 }
1061 _selectWordAt(offset: _lastSecondaryTapDownPosition!);
1062 case TargetPlatform.linux:
1063 if (toolbarIsVisible) {
1064 hideToolbar();
1065 return;
1066 }
1067 // If _lastSecondaryTapDownPosition is within the current selection then
1068 // keep the current selection, if not then collapse it.
1069 final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(
1070 globalPosition: details.globalPosition,
1071 );
1072 if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
1073 _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!);
1074 }
1075 }
1076 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1077 _finalizeSelectableRegionStatus();
1078 // Restore _lastSecondaryTapDownPosition since it may be cleared if a user
1079 // accesses contextMenuAnchors.
1080 _lastSecondaryTapDownPosition = details.globalPosition;
1081 _showHandles();
1082 _showToolbar(location: _lastSecondaryTapDownPosition);
1083 _updateSelectedContentIfNeeded();
1084 }
1085
1086 // Selection update helper methods.
1087
1088 Offset? _selectionEndPosition;
1089 bool get _userDraggingSelectionEnd => _selectionEndPosition != null;
1090 bool _scheduledSelectionEndEdgeUpdate = false;
1091
1092 /// Sends end [SelectionEdgeUpdateEvent] to the selectable subtree.
1093 ///
1094 /// If the selectable subtree returns a [SelectionResult.pending], this method
1095 /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result
1096 /// is not pending or users end their gestures.
1097 void _triggerSelectionEndEdgeUpdate({TextGranularity? textGranularity}) {
1098 // This method can be called when the drag is not in progress. This can
1099 // happen if the child scrollable returns SelectionResult.pending, and
1100 // the selection area scheduled a selection update for the next frame, but
1101 // the drag is lifted before the scheduled selection update is run.
1102 if (_scheduledSelectionEndEdgeUpdate || !_userDraggingSelectionEnd) {
1103 return;
1104 }
1105 if (_selectable?.dispatchSelectionEvent(
1106 SelectionEdgeUpdateEvent.forEnd(
1107 globalPosition: _selectionEndPosition!,
1108 granularity: textGranularity,
1109 ),
1110 ) ==
1111 SelectionResult.pending) {
1112 _scheduledSelectionEndEdgeUpdate = true;
1113 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
1114 if (!_scheduledSelectionEndEdgeUpdate) {
1115 return;
1116 }
1117 _scheduledSelectionEndEdgeUpdate = false;
1118 _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity);
1119 }, debugLabel: 'SelectableRegion.endEdgeUpdate');
1120 return;
1121 }
1122 }
1123
1124 void _onAnyDragEnd(DragEndDetails details) {
1125 if (widget.selectionControls is! TextSelectionHandleControls) {
1126 _selectionOverlay!.hideMagnifier();
1127 _selectionOverlay!.showToolbar();
1128 } else {
1129 _selectionOverlay!.hideMagnifier();
1130 _selectionOverlay!.showToolbar(
1131 context: context,
1132 contextMenuBuilder: (BuildContext context) {
1133 return widget.contextMenuBuilder!(context, this);
1134 },
1135 );
1136 }
1137 _finalizeSelection();
1138 _updateSelectedContentIfNeeded();
1139 _finalizeSelectableRegionStatus();
1140 }
1141
1142 void _stopSelectionEndEdgeUpdate() {
1143 _scheduledSelectionEndEdgeUpdate = false;
1144 _selectionEndPosition = null;
1145 }
1146
1147 Offset? _selectionStartPosition;
1148 bool get _userDraggingSelectionStart => _selectionStartPosition != null;
1149 bool _scheduledSelectionStartEdgeUpdate = false;
1150
1151 /// Sends start [SelectionEdgeUpdateEvent] to the selectable subtree.
1152 ///
1153 /// If the selectable subtree returns a [SelectionResult.pending], this method
1154 /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result
1155 /// is not pending or users end their gestures.
1156 void _triggerSelectionStartEdgeUpdate({TextGranularity? textGranularity}) {
1157 // This method can be called when the drag is not in progress. This can
1158 // happen if the child scrollable returns SelectionResult.pending, and
1159 // the selection area scheduled a selection update for the next frame, but
1160 // the drag is lifted before the scheduled selection update is run.
1161 if (_scheduledSelectionStartEdgeUpdate || !_userDraggingSelectionStart) {
1162 return;
1163 }
1164 if (_selectable?.dispatchSelectionEvent(
1165 SelectionEdgeUpdateEvent.forStart(
1166 globalPosition: _selectionStartPosition!,
1167 granularity: textGranularity,
1168 ),
1169 ) ==
1170 SelectionResult.pending) {
1171 _scheduledSelectionStartEdgeUpdate = true;
1172 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
1173 if (!_scheduledSelectionStartEdgeUpdate) {
1174 return;
1175 }
1176 _scheduledSelectionStartEdgeUpdate = false;
1177 _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity);
1178 }, debugLabel: 'SelectableRegion.startEdgeUpdate');
1179 return;
1180 }
1181 }
1182
1183 void _stopSelectionStartEdgeUpdate() {
1184 _scheduledSelectionStartEdgeUpdate = false;
1185 _selectionEndPosition = null;
1186 }
1187
1188 // SelectionOverlay helper methods.
1189
1190 late Offset _selectionStartHandleDragPosition;
1191 late Offset _selectionEndHandleDragPosition;
1192
1193 void _handleSelectionStartHandleDragStart(DragStartDetails details) {
1194 assert(_selectionDelegate.value.startSelectionPoint != null);
1195
1196 final Offset localPosition = _selectionDelegate.value.startSelectionPoint!.localPosition;
1197 final Matrix4 globalTransform = _selectable!.getTransformTo(null);
1198 _selectionStartHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition);
1199
1200 _selectionOverlay!.showMagnifier(
1201 _buildInfoForMagnifier(details.globalPosition, _selectionDelegate.value.startSelectionPoint!),
1202 );
1203 _updateSelectedContentIfNeeded();
1204 }
1205
1206 void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
1207 _selectionStartHandleDragPosition = _selectionStartHandleDragPosition + details.delta;
1208 // The value corresponds to the paint origin of the selection handle.
1209 // Offset it to the center of the line to make it feel more natural.
1210 _selectionStartPosition =
1211 _selectionStartHandleDragPosition -
1212 Offset(0, _selectionDelegate.value.startSelectionPoint!.lineHeight / 2);
1213 _triggerSelectionStartEdgeUpdate();
1214
1215 _selectionOverlay!.updateMagnifier(
1216 _buildInfoForMagnifier(details.globalPosition, _selectionDelegate.value.startSelectionPoint!),
1217 );
1218 _updateSelectedContentIfNeeded();
1219 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1220 }
1221
1222 void _handleSelectionEndHandleDragStart(DragStartDetails details) {
1223 assert(_selectionDelegate.value.endSelectionPoint != null);
1224 final Offset localPosition = _selectionDelegate.value.endSelectionPoint!.localPosition;
1225 final Matrix4 globalTransform = _selectable!.getTransformTo(null);
1226 _selectionEndHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition);
1227
1228 _selectionOverlay!.showMagnifier(
1229 _buildInfoForMagnifier(details.globalPosition, _selectionDelegate.value.endSelectionPoint!),
1230 );
1231 _updateSelectedContentIfNeeded();
1232 }
1233
1234 void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
1235 _selectionEndHandleDragPosition = _selectionEndHandleDragPosition + details.delta;
1236 // The value corresponds to the paint origin of the selection handle.
1237 // Offset it to the center of the line to make it feel more natural.
1238 _selectionEndPosition =
1239 _selectionEndHandleDragPosition -
1240 Offset(0, _selectionDelegate.value.endSelectionPoint!.lineHeight / 2);
1241 _triggerSelectionEndEdgeUpdate();
1242
1243 _selectionOverlay!.updateMagnifier(
1244 _buildInfoForMagnifier(details.globalPosition, _selectionDelegate.value.endSelectionPoint!),
1245 );
1246 _updateSelectedContentIfNeeded();
1247 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1248 }
1249
1250 MagnifierInfo _buildInfoForMagnifier(
1251 Offset globalGesturePosition,
1252 SelectionPoint selectionPoint,
1253 ) {
1254 final Vector3 globalTransform = _selectable!.getTransformTo(null).getTranslation();
1255 final Offset globalTransformAsOffset = Offset(globalTransform.x, globalTransform.y);
1256 final Offset globalSelectionPointPosition =
1257 selectionPoint.localPosition + globalTransformAsOffset;
1258 final Rect caretRect = Rect.fromLTWH(
1259 globalSelectionPointPosition.dx,
1260 globalSelectionPointPosition.dy - selectionPoint.lineHeight,
1261 0,
1262 selectionPoint.lineHeight,
1263 );
1264
1265 return MagnifierInfo(
1266 globalGesturePosition: globalGesturePosition,
1267 caretRect: caretRect,
1268 fieldBounds: globalTransformAsOffset & _selectable!.size,
1269 currentLineBoundaries: globalTransformAsOffset & _selectable!.size,
1270 );
1271 }
1272
1273 void _createSelectionOverlay() {
1274 assert(_hasSelectionOverlayGeometry);
1275 if (_selectionOverlay != null) {
1276 return;
1277 }
1278 final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
1279 final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
1280 _selectionOverlay = SelectionOverlay(
1281 context: context,
1282 debugRequiredFor: widget,
1283 startHandleType: start?.handleType ?? TextSelectionHandleType.collapsed,
1284 lineHeightAtStart: start?.lineHeight ?? end!.lineHeight,
1285 onStartHandleDragStart: _handleSelectionStartHandleDragStart,
1286 onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
1287 onStartHandleDragEnd: _onAnyDragEnd,
1288 endHandleType: end?.handleType ?? TextSelectionHandleType.collapsed,
1289 lineHeightAtEnd: end?.lineHeight ?? start!.lineHeight,
1290 onEndHandleDragStart: _handleSelectionEndHandleDragStart,
1291 onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
1292 onEndHandleDragEnd: _onAnyDragEnd,
1293 selectionEndpoints: selectionEndpoints,
1294 selectionControls: widget.selectionControls,
1295 selectionDelegate: this,
1296 clipboardStatus: null,
1297 startHandleLayerLink: _startHandleLayerLink,
1298 endHandleLayerLink: _endHandleLayerLink,
1299 toolbarLayerLink: _toolbarLayerLink,
1300 magnifierConfiguration: widget.magnifierConfiguration,
1301 );
1302 }
1303
1304 void _updateSelectionOverlay() {
1305 if (_selectionOverlay == null) {
1306 return;
1307 }
1308 assert(_hasSelectionOverlayGeometry);
1309 final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
1310 final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
1311 _selectionOverlay!
1312 ..startHandleType = start?.handleType ?? TextSelectionHandleType.left
1313 ..lineHeightAtStart = start?.lineHeight ?? end!.lineHeight
1314 ..endHandleType = end?.handleType ?? TextSelectionHandleType.right
1315 ..lineHeightAtEnd = end?.lineHeight ?? start!.lineHeight
1316 ..selectionEndpoints = selectionEndpoints;
1317 }
1318
1319 /// Shows the selection handles.
1320 ///
1321 /// Returns true if the handles are shown, false if the handles can't be
1322 /// shown.
1323 bool _showHandles() {
1324 if (_selectionOverlay != null) {
1325 _selectionOverlay!.showHandles();
1326 return true;
1327 }
1328
1329 if (!_hasSelectionOverlayGeometry) {
1330 return false;
1331 }
1332
1333 _createSelectionOverlay();
1334 _selectionOverlay!.showHandles();
1335 return true;
1336 }
1337
1338 /// Shows the text selection toolbar.
1339 ///
1340 /// If the parameter `location` is set, the toolbar will be shown at the
1341 /// location. Otherwise, the toolbar location will be calculated based on the
1342 /// handles' locations. The `location` is in the coordinates system of the
1343 /// [Overlay].
1344 ///
1345 /// Returns true if the toolbar is shown, false if the toolbar can't be shown.
1346 bool _showToolbar({Offset? location}) {
1347 if (!_hasSelectionOverlayGeometry && _selectionOverlay == null) {
1348 return false;
1349 }
1350
1351 // Web is using native dom elements to enable clipboard functionality of the
1352 // context menu: copy, paste, select, cut. It might also provide additional
1353 // functionality depending on the browser (such as translate). Due to this,
1354 // we should not show a Flutter toolbar for the editable text elements
1355 // unless the browser's context menu is explicitly disabled.
1356 if (kIsWeb && BrowserContextMenu.enabled) {
1357 return false;
1358 }
1359
1360 if (_selectionOverlay == null) {
1361 _createSelectionOverlay();
1362 }
1363
1364 _selectionOverlay!.toolbarLocation = location;
1365 if (widget.selectionControls is! TextSelectionHandleControls) {
1366 _selectionOverlay!.showToolbar();
1367 return true;
1368 }
1369
1370 _selectionOverlay!.hideToolbar();
1371
1372 _selectionOverlay!.showToolbar(
1373 context: context,
1374 contextMenuBuilder: (BuildContext context) {
1375 return widget.contextMenuBuilder!(context, this);
1376 },
1377 );
1378 return true;
1379 }
1380
1381 /// Sets or updates selection end edge to the `offset` location.
1382 ///
1383 /// A selection always contains a select start edge and selection end edge.
1384 /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or
1385 /// use other selection APIs, such as [_selectWordAt] or [selectAll].
1386 ///
1387 /// This method sets or updates the selection end edge by sending
1388 /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s.
1389 ///
1390 /// If `continuous` is set to true and the update causes scrolling, the
1391 /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the
1392 /// child [Selectable]s every frame until the scrolling finishes or a
1393 /// [_finalizeSelection] is called.
1394 ///
1395 /// The `continuous` argument defaults to false.
1396 ///
1397 /// The `offset` is in global coordinates.
1398 ///
1399 /// Provide the `textGranularity` if the selection should not move by the default
1400 /// [TextGranularity.character]. Only [TextGranularity.character] and
1401 /// [TextGranularity.word] are currently supported.
1402 ///
1403 /// See also:
1404 /// * [_selectStartTo], which sets or updates selection start edge.
1405 /// * [_finalizeSelection], which stops the `continuous` updates.
1406 /// * [clearSelection], which clears the ongoing selection.
1407 /// * [_selectWordAt], which selects a whole word at the location.
1408 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1409 /// * [_collapseSelectionAt], which collapses the selection at the location.
1410 /// * [selectAll], which selects the entire content.
1411 void _selectEndTo({
1412 required Offset offset,
1413 bool continuous = false,
1414 TextGranularity? textGranularity,
1415 }) {
1416 if (!continuous) {
1417 _selectable?.dispatchSelectionEvent(
1418 SelectionEdgeUpdateEvent.forEnd(globalPosition: offset, granularity: textGranularity),
1419 );
1420 return;
1421 }
1422 if (_selectionEndPosition != offset) {
1423 _selectionEndPosition = offset;
1424 _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity);
1425 }
1426 }
1427
1428 /// Sets or updates selection start edge to the `offset` location.
1429 ///
1430 /// A selection always contains a select start edge and selection end edge.
1431 /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or
1432 /// use other selection APIs, such as [_selectWordAt] or [selectAll].
1433 ///
1434 /// This method sets or updates the selection start edge by sending
1435 /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s.
1436 ///
1437 /// If `continuous` is set to true and the update causes scrolling, the
1438 /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the
1439 /// child [Selectable]s every frame until the scrolling finishes or a
1440 /// [_finalizeSelection] is called.
1441 ///
1442 /// The `continuous` argument defaults to false.
1443 ///
1444 /// The `offset` is in global coordinates.
1445 ///
1446 /// Provide the `textGranularity` if the selection should not move by the default
1447 /// [TextGranularity.character]. Only [TextGranularity.character] and
1448 /// [TextGranularity.word] are currently supported.
1449 ///
1450 /// See also:
1451 /// * [_selectEndTo], which sets or updates selection end edge.
1452 /// * [_finalizeSelection], which stops the `continuous` updates.
1453 /// * [clearSelection], which clears the ongoing selection.
1454 /// * [_selectWordAt], which selects a whole word at the location.
1455 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1456 /// * [_collapseSelectionAt], which collapses the selection at the location.
1457 /// * [selectAll], which selects the entire content.
1458 void _selectStartTo({
1459 required Offset offset,
1460 bool continuous = false,
1461 TextGranularity? textGranularity,
1462 }) {
1463 if (!continuous) {
1464 _selectable?.dispatchSelectionEvent(
1465 SelectionEdgeUpdateEvent.forStart(globalPosition: offset, granularity: textGranularity),
1466 );
1467 return;
1468 }
1469 if (_selectionStartPosition != offset) {
1470 _selectionStartPosition = offset;
1471 _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity);
1472 }
1473 }
1474
1475 /// Collapses the selection at the given `offset` location.
1476 ///
1477 /// The `offset` is in global coordinates.
1478 ///
1479 /// See also:
1480 /// * [_selectStartTo], which sets or updates selection start edge.
1481 /// * [_selectEndTo], which sets or updates selection end edge.
1482 /// * [_finalizeSelection], which stops the `continuous` updates.
1483 /// * [clearSelection], which clears the ongoing selection.
1484 /// * [_selectWordAt], which selects a whole word at the location.
1485 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1486 /// * [selectAll], which selects the entire content.
1487 void _collapseSelectionAt({required Offset offset}) {
1488 // There may be other selection ongoing.
1489 _finalizeSelection();
1490 _selectStartTo(offset: offset);
1491 _selectEndTo(offset: offset);
1492 }
1493
1494 /// Selects a whole word at the `offset` location.
1495 ///
1496 /// The `offset` is in global coordinates.
1497 ///
1498 /// If the whole word is already in the current selection, selection won't
1499 /// change. One call [clearSelection] first if the selection needs to be
1500 /// updated even if the word is already covered by the current selection.
1501 ///
1502 /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
1503 /// edges after calling this method.
1504 ///
1505 /// See also:
1506 /// * [_selectStartTo], which sets or updates selection start edge.
1507 /// * [_selectEndTo], which sets or updates selection end edge.
1508 /// * [_finalizeSelection], which stops the `continuous` updates.
1509 /// * [clearSelection], which clears the ongoing selection.
1510 /// * [_collapseSelectionAt], which collapses the selection at the location.
1511 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1512 /// * [selectAll], which selects the entire content.
1513 void _selectWordAt({required Offset offset}) {
1514 // There may be other selection ongoing.
1515 _finalizeSelection();
1516 _selectable?.dispatchSelectionEvent(SelectWordSelectionEvent(globalPosition: offset));
1517 }
1518
1519 /// Selects the entire paragraph at the `offset` location.
1520 ///
1521 /// The `offset` is in global coordinates.
1522 ///
1523 /// If the paragraph is already in the current selection, selection won't
1524 /// change. One call [clearSelection] first if the selection needs to be
1525 /// updated even if the paragraph is already covered by the current selection.
1526 ///
1527 /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
1528 /// edges after calling this method.
1529 ///
1530 /// See also:
1531 /// * [_selectStartTo], which sets or updates selection start edge.
1532 /// * [_selectEndTo], which sets or updates selection end edge.
1533 /// * [_finalizeSelection], which stops the `continuous` updates.
1534 /// * [clearSelection], which clear the ongoing selection.
1535 /// * [_selectWordAt], which selects a whole word at the location.
1536 /// * [selectAll], which selects the entire content.
1537 void _selectParagraphAt({required Offset offset}) {
1538 // There may be other selection ongoing.
1539 _finalizeSelection();
1540 _selectable?.dispatchSelectionEvent(SelectParagraphSelectionEvent(globalPosition: offset));
1541 }
1542
1543 /// Stops any ongoing selection updates.
1544 ///
1545 /// This method is different from [clearSelection] that it does not remove
1546 /// the current selection. It only stops the continuous updates.
1547 ///
1548 /// A continuous update can happen as result of calling [_selectStartTo] or
1549 /// [_selectEndTo] with `continuous` sets to true which causes a [Selectable]
1550 /// to scroll. Calling this method will stop the update as well as the
1551 /// scrolling.
1552 void _finalizeSelection() {
1553 _stopSelectionEndEdgeUpdate();
1554 _stopSelectionStartEdgeUpdate();
1555 }
1556
1557 /// Removes the ongoing selection for this [SelectableRegion].
1558 void clearSelection() {
1559 _finalizeSelection();
1560 _directionalHorizontalBaseline = null;
1561 _adjustingSelectionEnd = null;
1562 _selectable?.dispatchSelectionEvent(const ClearSelectionEvent());
1563 _updateSelectedContentIfNeeded();
1564 }
1565
1566 Future<void> _copy() async {
1567 final SelectedContent? data = _selectable?.getSelectedContent();
1568 if (data == null) {
1569 return;
1570 }
1571 await Clipboard.setData(ClipboardData(text: data.plainText));
1572 }
1573
1574 Future<void> _share() async {
1575 final SelectedContent? data = _selectable?.getSelectedContent();
1576 if (data == null) {
1577 return;
1578 }
1579 await SystemChannels.platform.invokeMethod('Share.invoke', data.plainText);
1580 }
1581
1582 /// {@macro flutter.widgets.EditableText.getAnchors}
1583 ///
1584 /// See also:
1585 ///
1586 /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s
1587 /// for the default context menu buttons.
1588 TextSelectionToolbarAnchors get contextMenuAnchors {
1589 if (_lastSecondaryTapDownPosition != null) {
1590 final TextSelectionToolbarAnchors anchors = TextSelectionToolbarAnchors(
1591 primaryAnchor: _lastSecondaryTapDownPosition!,
1592 );
1593 // Clear the state of _lastSecondaryTapDownPosition after use since a user may
1594 // access contextMenuAnchors and receive invalid anchors for their context menu.
1595 _lastSecondaryTapDownPosition = null;
1596 return anchors;
1597 }
1598 final RenderBox renderBox = context.findRenderObject()! as RenderBox;
1599 return TextSelectionToolbarAnchors.fromSelection(
1600 renderBox: renderBox,
1601 startGlyphHeight: startGlyphHeight,
1602 endGlyphHeight: endGlyphHeight,
1603 selectionEndpoints: selectionEndpoints,
1604 );
1605 }
1606
1607 bool? _adjustingSelectionEnd;
1608 bool _determineIsAdjustingSelectionEnd(bool forward) {
1609 if (_adjustingSelectionEnd != null) {
1610 return _adjustingSelectionEnd!;
1611 }
1612 final bool isReversed;
1613 final SelectionPoint start = _selectionDelegate.value.startSelectionPoint!;
1614 final SelectionPoint end = _selectionDelegate.value.endSelectionPoint!;
1615 if (start.localPosition.dy > end.localPosition.dy) {
1616 isReversed = true;
1617 } else if (start.localPosition.dy < end.localPosition.dy) {
1618 isReversed = false;
1619 } else {
1620 isReversed = start.localPosition.dx > end.localPosition.dx;
1621 }
1622 // Always move the selection edge that increases the selection range.
1623 return _adjustingSelectionEnd = forward != isReversed;
1624 }
1625
1626 void _granularlyExtendSelection(TextGranularity granularity, bool forward) {
1627 _directionalHorizontalBaseline = null;
1628 if (!_selectionDelegate.value.hasSelection) {
1629 return;
1630 }
1631 _selectable?.dispatchSelectionEvent(
1632 GranularlyExtendSelectionEvent(
1633 forward: forward,
1634 isEnd: _determineIsAdjustingSelectionEnd(forward),
1635 granularity: granularity,
1636 ),
1637 );
1638 _updateSelectedContentIfNeeded();
1639 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1640 _finalizeSelectableRegionStatus();
1641 }
1642
1643 double? _directionalHorizontalBaseline;
1644
1645 void _directionallyExtendSelection(bool forward) {
1646 if (!_selectionDelegate.value.hasSelection) {
1647 return;
1648 }
1649 final bool adjustingSelectionExtend = _determineIsAdjustingSelectionEnd(forward);
1650 final SelectionPoint baseLinePoint =
1651 adjustingSelectionExtend
1652 ? _selectionDelegate.value.endSelectionPoint!
1653 : _selectionDelegate.value.startSelectionPoint!;
1654 _directionalHorizontalBaseline ??= baseLinePoint.localPosition.dx;
1655 final Offset globalSelectionPointOffset = MatrixUtils.transformPoint(
1656 context.findRenderObject()!.getTransformTo(null),
1657 Offset(_directionalHorizontalBaseline!, 0),
1658 );
1659 _selectable?.dispatchSelectionEvent(
1660 DirectionallyExtendSelectionEvent(
1661 isEnd: _adjustingSelectionEnd!,
1662 direction:
1663 forward ? SelectionExtendDirection.nextLine : SelectionExtendDirection.previousLine,
1664 dx: globalSelectionPointOffset.dx,
1665 ),
1666 );
1667 _updateSelectedContentIfNeeded();
1668 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1669 _finalizeSelectableRegionStatus();
1670 }
1671
1672 // [TextSelectionDelegate] overrides.
1673
1674 /// Returns the [ContextMenuButtonItem]s representing the buttons in this
1675 /// platform's default selection menu.
1676 ///
1677 /// See also:
1678 ///
1679 /// * [SelectableRegion.getSelectableButtonItems], which performs a similar role,
1680 /// but for any selectable text, not just specifically SelectableRegion.
1681 /// * [EditableTextState.contextMenuButtonItems], which performs a similar role
1682 /// but for content that is not just selectable but also editable.
1683 /// * [contextMenuAnchors], which provides the anchor points for the default
1684 /// context menu.
1685 /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
1686 /// take a list of [ContextMenuButtonItem]s with
1687 /// [AdaptiveTextSelectionToolbar.buttonItems].
1688 /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
1689 /// button Widgets for the current platform given [ContextMenuButtonItem]s.
1690 List<ContextMenuButtonItem> get contextMenuButtonItems {
1691 return SelectableRegion.getSelectableButtonItems(
1692 selectionGeometry: _selectionDelegate.value,
1693 onCopy: () {
1694 _copy();
1695
1696 // On Android copy should clear the selection.
1697 switch (defaultTargetPlatform) {
1698 case TargetPlatform.android:
1699 case TargetPlatform.fuchsia:
1700 clearSelection();
1701 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1702 _finalizeSelectableRegionStatus();
1703 case TargetPlatform.iOS:
1704 hideToolbar(false);
1705 case TargetPlatform.linux:
1706 case TargetPlatform.macOS:
1707 case TargetPlatform.windows:
1708 hideToolbar();
1709 }
1710 },
1711 onSelectAll: () {
1712 switch (defaultTargetPlatform) {
1713 case TargetPlatform.android:
1714 case TargetPlatform.iOS:
1715 case TargetPlatform.fuchsia:
1716 selectAll(SelectionChangedCause.toolbar);
1717 case TargetPlatform.linux:
1718 case TargetPlatform.macOS:
1719 case TargetPlatform.windows:
1720 selectAll();
1721 hideToolbar();
1722 }
1723 },
1724 onShare: () {
1725 _share();
1726
1727 // On Android, share should clear the selection.
1728 switch (defaultTargetPlatform) {
1729 case TargetPlatform.android:
1730 case TargetPlatform.fuchsia:
1731 clearSelection();
1732 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1733 _finalizeSelectableRegionStatus();
1734 case TargetPlatform.iOS:
1735 hideToolbar(false);
1736 case TargetPlatform.linux:
1737 case TargetPlatform.macOS:
1738 case TargetPlatform.windows:
1739 hideToolbar();
1740 }
1741 },
1742 )..addAll(_textProcessingActionButtonItems);
1743 }
1744
1745 List<ContextMenuButtonItem> get _textProcessingActionButtonItems {
1746 final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
1747 final SelectedContent? data = _selectable?.getSelectedContent();
1748 if (data == null) {
1749 return buttonItems;
1750 }
1751
1752 for (final ProcessTextAction action in _processTextActions) {
1753 buttonItems.add(
1754 ContextMenuButtonItem(
1755 label: action.label,
1756 onPressed: () async {
1757 final String selectedText = data.plainText;
1758 if (selectedText.isNotEmpty) {
1759 await _processTextService.processTextAction(action.id, selectedText, true);
1760 hideToolbar();
1761 }
1762 },
1763 ),
1764 );
1765 }
1766 return buttonItems;
1767 }
1768
1769 /// The line height at the start of the current selection.
1770 double get startGlyphHeight {
1771 return _selectionDelegate.value.startSelectionPoint!.lineHeight;
1772 }
1773
1774 /// The line height at the end of the current selection.
1775 double get endGlyphHeight {
1776 return _selectionDelegate.value.endSelectionPoint!.lineHeight;
1777 }
1778
1779 /// Returns the local coordinates of the endpoints of the current selection.
1780 List<TextSelectionPoint> get selectionEndpoints {
1781 final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
1782 final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
1783 late List<TextSelectionPoint> points;
1784 final Offset startLocalPosition = start?.localPosition ?? end!.localPosition;
1785 final Offset endLocalPosition = end?.localPosition ?? start!.localPosition;
1786 if (startLocalPosition.dy > endLocalPosition.dy) {
1787 points = <TextSelectionPoint>[
1788 TextSelectionPoint(endLocalPosition, TextDirection.ltr),
1789 TextSelectionPoint(startLocalPosition, TextDirection.ltr),
1790 ];
1791 } else {
1792 points = <TextSelectionPoint>[
1793 TextSelectionPoint(startLocalPosition, TextDirection.ltr),
1794 TextSelectionPoint(endLocalPosition, TextDirection.ltr),
1795 ];
1796 }
1797 return points;
1798 }
1799
1800 // [TextSelectionDelegate] overrides.
1801 // TODO(justinmc): After deprecations have been removed, remove
1802 // TextSelectionDelegate from this class.
1803 // https://github.com/flutter/flutter/issues/111213
1804
1805 @Deprecated(
1806 'Use `contextMenuBuilder` instead. '
1807 'This feature was deprecated after v3.3.0-0.5.pre.',
1808 )
1809 @override
1810 bool get cutEnabled => false;
1811
1812 @Deprecated(
1813 'Use `contextMenuBuilder` instead. '
1814 'This feature was deprecated after v3.3.0-0.5.pre.',
1815 )
1816 @override
1817 bool get pasteEnabled => false;
1818
1819 @override
1820 void hideToolbar([bool hideHandles = true]) {
1821 _selectionOverlay?.hideToolbar();
1822 if (hideHandles) {
1823 _selectionOverlay?.hideHandles();
1824 }
1825 }
1826
1827 @override
1828 void selectAll([SelectionChangedCause? cause]) {
1829 clearSelection();
1830 _selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent());
1831 if (cause == SelectionChangedCause.toolbar) {
1832 _showToolbar();
1833 _showHandles();
1834 }
1835 _updateSelectedContentIfNeeded();
1836 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1837 _finalizeSelectableRegionStatus();
1838 }
1839
1840 @Deprecated(
1841 'Use `contextMenuBuilder` instead. '
1842 'This feature was deprecated after v3.3.0-0.5.pre.',
1843 )
1844 @override
1845 void copySelection(SelectionChangedCause cause) {
1846 _copy();
1847 clearSelection();
1848 _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
1849 _finalizeSelectableRegionStatus();
1850 }
1851
1852 @Deprecated(
1853 'Use `contextMenuBuilder` instead. '
1854 'This feature was deprecated after v3.3.0-0.5.pre.',
1855 )
1856 @override
1857 TextEditingValue textEditingValue = const TextEditingValue(text: '_');
1858
1859 @Deprecated(
1860 'Use `contextMenuBuilder` instead. '
1861 'This feature was deprecated after v3.3.0-0.5.pre.',
1862 )
1863 @override
1864 void bringIntoView(TextPosition position) {
1865 /* SelectableRegion must be in view at this point. */
1866 }
1867
1868 @Deprecated(
1869 'Use `contextMenuBuilder` instead. '
1870 'This feature was deprecated after v3.3.0-0.5.pre.',
1871 )
1872 @override
1873 void cutSelection(SelectionChangedCause cause) {
1874 assert(false);
1875 }
1876
1877 @Deprecated(
1878 'Use `contextMenuBuilder` instead. '
1879 'This feature was deprecated after v3.3.0-0.5.pre.',
1880 )
1881 @override
1882 void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {
1883 /* SelectableRegion maintains its own state */
1884 }
1885
1886 @Deprecated(
1887 'Use `contextMenuBuilder` instead. '
1888 'This feature was deprecated after v3.3.0-0.5.pre.',
1889 )
1890 @override
1891 Future<void> pasteText(SelectionChangedCause cause) async {
1892 assert(false);
1893 }
1894
1895 // [SelectionRegistrar] override.
1896
1897 @override
1898 void add(Selectable selectable) {
1899 assert(_selectable == null);
1900 _selectable = selectable;
1901 _selectable!.addListener(_updateSelectionStatus);
1902 _selectable!.pushHandleLayers(_startHandleLayerLink, _endHandleLayerLink);
1903 }
1904
1905 @override
1906 void remove(Selectable selectable) {
1907 assert(_selectable == selectable);
1908 _selectable!.removeListener(_updateSelectionStatus);
1909 _selectable!.pushHandleLayers(null, null);
1910 _selectable = null;
1911 }
1912
1913 @protected
1914 @override
1915 void dispose() {
1916 _selectable?.removeListener(_updateSelectionStatus);
1917 _selectable?.pushHandleLayers(null, null);
1918 _selectionDelegate.dispose();
1919 _selectionStatusNotifier.dispose();
1920 // In case dispose was triggered before gesture end, remove the magnifier
1921 // so it doesn't remain stuck in the overlay forever.
1922 _selectionOverlay?.hideMagnifier();
1923 _selectionOverlay?.dispose();
1924 _selectionOverlay = null;
1925 widget.focusNode?.removeListener(_handleFocusChanged);
1926 _localFocusNode?.removeListener(_handleFocusChanged);
1927 _localFocusNode?.dispose();
1928 super.dispose();
1929 }
1930
1931 @protected
1932 @override
1933 Widget build(BuildContext context) {
1934 assert(debugCheckHasOverlay(context));
1935 Widget result = SelectableRegionSelectionStatusScope._(
1936 selectionStatusNotifier: _selectionStatusNotifier,
1937 child: SelectionContainer(registrar: this, delegate: _selectionDelegate, child: widget.child),
1938 );
1939 if (kIsWeb) {
1940 result = PlatformSelectableRegionContextMenu(child: result);
1941 }
1942 return CompositedTransformTarget(
1943 link: _toolbarLayerLink,
1944 child: RawGestureDetector(
1945 gestures: _gestureRecognizers,
1946 behavior: HitTestBehavior.translucent,
1947 excludeFromSemantics: true,
1948 child: Actions(
1949 actions: _actions,
1950 child: Focus.withExternalFocusNode(
1951 includeSemantics: false,
1952 focusNode: _focusNode,
1953 child: result,
1954 ),
1955 ),
1956 ),
1957 );
1958 }
1959}
1960
1961/// An action that does not override any [Action.overridable] in the subtree.
1962///
1963/// If this action is invoked by an [Action.overridable], it will immediately
1964/// invoke the [Action.overridable] and do nothing else. Otherwise, it will call
1965/// [invokeAction].
1966abstract class _NonOverrideAction<T extends Intent> extends ContextAction<T> {
1967 Object? invokeAction(T intent, [BuildContext? context]);
1968
1969 @override
1970 Object? invoke(T intent, [BuildContext? context]) {
1971 if (callingAction != null) {
1972 return callingAction!.invoke(intent);
1973 }
1974 return invokeAction(intent, context);
1975 }
1976}
1977
1978class _SelectAllAction extends _NonOverrideAction<SelectAllTextIntent> {
1979 _SelectAllAction(this.state);
1980
1981 final SelectableRegionState state;
1982
1983 @override
1984 void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) {
1985 state.selectAll(SelectionChangedCause.keyboard);
1986 }
1987}
1988
1989class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> {
1990 _CopySelectionAction(this.state);
1991
1992 final SelectableRegionState state;
1993
1994 @override
1995 void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) {
1996 state._copy();
1997 }
1998}
1999
2000class _GranularlyExtendSelectionAction<T extends DirectionalTextEditingIntent>
2001 extends _NonOverrideAction<T> {
2002 _GranularlyExtendSelectionAction(this.state, {required this.granularity});
2003
2004 final SelectableRegionState state;
2005 final TextGranularity granularity;
2006
2007 @override
2008 void invokeAction(T intent, [BuildContext? context]) {
2009 state._granularlyExtendSelection(granularity, intent.forward);
2010 }
2011}
2012
2013class _GranularlyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent>
2014 extends _NonOverrideAction<T> {
2015 _GranularlyExtendCaretSelectionAction(this.state, {required this.granularity});
2016
2017 final SelectableRegionState state;
2018 final TextGranularity granularity;
2019
2020 @override
2021 void invokeAction(T intent, [BuildContext? context]) {
2022 if (intent.collapseSelection) {
2023 // Selectable region never collapses selection.
2024 return;
2025 }
2026 state._granularlyExtendSelection(granularity, intent.forward);
2027 }
2028}
2029
2030class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent>
2031 extends _NonOverrideAction<T> {
2032 _DirectionallyExtendCaretSelectionAction(this.state);
2033
2034 final SelectableRegionState state;
2035
2036 @override
2037 void invokeAction(T intent, [BuildContext? context]) {
2038 if (intent.collapseSelection) {
2039 // Selectable region never collapses selection.
2040 return;
2041 }
2042 state._directionallyExtendSelection(intent.forward);
2043 }
2044}
2045
2046/// A delegate that manages updating multiple [Selectable] children where the
2047/// [Selectable]s do not change or move around frequently.
2048///
2049/// This delegate keeps track of the [Selectable]s that received start or end
2050/// [SelectionEvent]s and the global locations of those events to accurately
2051/// synthesize [SelectionEvent]s for children [Selectable]s when needed.
2052///
2053/// When a new [SelectionEdgeUpdateEvent] is dispatched to a [Selectable], this
2054/// delegate checks whether the [Selectable] has already received a selection
2055/// update for each edge that currently exists, and synthesizes an event for the
2056/// edges that have not yet received an update. This synthesized event is dispatched
2057/// before dispatching the new event.
2058///
2059/// For example, if we have an existing start edge for this delegate and a [Selectable]
2060/// child receives an end [SelectionEdgeUpdateEvent] and the child hasn't received a start
2061/// [SelectionEdgeUpdateEvent], we synthesize a start [SelectionEdgeUpdateEvent] for the
2062/// child [Selectable] and dispatch it before dispatching the original end [SelectionEdgeUpdateEvent].
2063///
2064/// See also:
2065///
2066/// * [MultiSelectableSelectionContainerDelegate], for the class that provides
2067/// the main implementation details of this [SelectionContainerDelegate].
2068class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
2069 /// The set of [Selectable]s that have received start events.
2070 final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};
2071
2072 /// The set of [Selectable]s that have received end events.
2073 final Set<Selectable> _hasReceivedEndEvent = <Selectable>{};
2074
2075 /// The global position of the last selection start edge update.
2076 Offset? _lastStartEdgeUpdateGlobalPosition;
2077
2078 /// The global position of the last selection end edge update.
2079 Offset? _lastEndEdgeUpdateGlobalPosition;
2080
2081 /// Tracks whether a selection edge update event for a given [Selectable] was received.
2082 ///
2083 /// When `forEnd` is true, the [Selectable] will be registered as having received
2084 /// an end event. When false, the [Selectable] is registered as having received
2085 /// a start event.
2086 ///
2087 /// When `forEnd` is null, the [Selectable] will be registered as having received both
2088 /// start and end events.
2089 ///
2090 /// Call this method when a [SelectionEvent] is dispatched to a child selectable managed
2091 /// by this delegate.
2092 ///
2093 /// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
2094 /// added by this method, for example when removing a [Selectable] from this delegate.
2095 @protected
2096 void didReceiveSelectionEventFor({required Selectable selectable, bool? forEnd}) {
2097 switch (forEnd) {
2098 case true:
2099 _hasReceivedEndEvent.add(selectable);
2100 case false:
2101 _hasReceivedStartEvent.add(selectable);
2102 case null:
2103 _hasReceivedStartEvent.add(selectable);
2104 _hasReceivedEndEvent.add(selectable);
2105 }
2106 }
2107
2108 /// Updates the internal selection state after a [SelectionEvent] that
2109 /// selects a boundary such as: [SelectWordSelectionEvent],
2110 /// [SelectParagraphSelectionEvent], and [SelectAllSelectionEvent].
2111 ///
2112 /// Call this method after determining the new selection as a result of
2113 /// a [SelectionEvent] that selects a boundary. The [currentSelectionStartIndex]
2114 /// and [currentSelectionEndIndex] should be set to valid values at the time
2115 /// this method is called.
2116 ///
2117 /// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
2118 /// added by this method, for example when removing a [Selectable] from this delegate.
2119 @protected
2120 void didReceiveSelectionBoundaryEvents() {
2121 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
2122 return;
2123 }
2124 final int start = min(currentSelectionStartIndex, currentSelectionEndIndex);
2125 final int end = max(currentSelectionStartIndex, currentSelectionEndIndex);
2126 for (int index = start; index <= end; index += 1) {
2127 didReceiveSelectionEventFor(selectable: selectables[index]);
2128 }
2129 _updateLastSelectionEdgeLocationsFromGeometries();
2130 }
2131
2132 /// Updates the last selection edge location of the edge specified by `forEnd`
2133 /// to the provided `globalSelectionEdgeLocation`.
2134 @protected
2135 void updateLastSelectionEdgeLocation({
2136 required Offset globalSelectionEdgeLocation,
2137 required bool forEnd,
2138 }) {
2139 if (forEnd) {
2140 _lastEndEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
2141 } else {
2142 _lastStartEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
2143 }
2144 }
2145
2146 /// Updates the last selection edge locations of both start and end selection
2147 /// edges based on their [SelectionGeometry].
2148 void _updateLastSelectionEdgeLocationsFromGeometries() {
2149 if (currentSelectionStartIndex != -1 &&
2150 selectables[currentSelectionStartIndex].value.hasSelection) {
2151 final Selectable start = selectables[currentSelectionStartIndex];
2152 final Offset localStartEdge =
2153 start.value.startSelectionPoint!.localPosition +
2154 Offset(0, -start.value.startSelectionPoint!.lineHeight / 2);
2155 updateLastSelectionEdgeLocation(
2156 globalSelectionEdgeLocation: MatrixUtils.transformPoint(
2157 start.getTransformTo(null),
2158 localStartEdge,
2159 ),
2160 forEnd: false,
2161 );
2162 }
2163 if (currentSelectionEndIndex != -1 &&
2164 selectables[currentSelectionEndIndex].value.hasSelection) {
2165 final Selectable end = selectables[currentSelectionEndIndex];
2166 final Offset localEndEdge =
2167 end.value.endSelectionPoint!.localPosition +
2168 Offset(0, -end.value.endSelectionPoint!.lineHeight / 2);
2169 updateLastSelectionEdgeLocation(
2170 globalSelectionEdgeLocation: MatrixUtils.transformPoint(
2171 end.getTransformTo(null),
2172 localEndEdge,
2173 ),
2174 forEnd: true,
2175 );
2176 }
2177 }
2178
2179 /// Clears the internal selection state.
2180 ///
2181 /// This indicates that no [Selectable] child under this delegate
2182 /// has received start or end events, and resets any tracked global
2183 /// locations for start and end [SelectionEdgeUpdateEvent]s.
2184 @protected
2185 void clearInternalSelectionState() {
2186 selectables.forEach(clearInternalSelectionStateForSelectable);
2187 _lastStartEdgeUpdateGlobalPosition = null;
2188 _lastEndEdgeUpdateGlobalPosition = null;
2189 }
2190
2191 /// Clears the internal selection state for a given [Selectable].
2192 ///
2193 /// This indicates that the given `selectable` has neither received a
2194 /// start or end [SelectionEdgeUpdateEvent]s.
2195 ///
2196 /// Subclasses should call this method to clean up state added in
2197 /// [didReceiveSelectionEventFor] and [didReceiveSelectionBoundaryEvents].
2198 @protected
2199 void clearInternalSelectionStateForSelectable(Selectable selectable) {
2200 _hasReceivedStartEvent.remove(selectable);
2201 _hasReceivedEndEvent.remove(selectable);
2202 }
2203
2204 @override
2205 void remove(Selectable selectable) {
2206 clearInternalSelectionStateForSelectable(selectable);
2207 super.remove(selectable);
2208 }
2209
2210 @override
2211 SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
2212 final SelectionResult result = super.handleSelectAll(event);
2213 didReceiveSelectionBoundaryEvents();
2214 return result;
2215 }
2216
2217 @override
2218 SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
2219 final SelectionResult result = super.handleSelectWord(event);
2220 didReceiveSelectionBoundaryEvents();
2221 return result;
2222 }
2223
2224 @override
2225 SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
2226 final SelectionResult result = super.handleSelectParagraph(event);
2227 didReceiveSelectionBoundaryEvents();
2228 return result;
2229 }
2230
2231 @override
2232 SelectionResult handleClearSelection(ClearSelectionEvent event) {
2233 final SelectionResult result = super.handleClearSelection(event);
2234 clearInternalSelectionState();
2235 return result;
2236 }
2237
2238 @override
2239 SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
2240 updateLastSelectionEdgeLocation(
2241 globalSelectionEdgeLocation: event.globalPosition,
2242 forEnd: event.type == SelectionEventType.endEdgeUpdate,
2243 );
2244 return super.handleSelectionEdgeUpdate(event);
2245 }
2246
2247 @override
2248 void dispose() {
2249 clearInternalSelectionState();
2250 super.dispose();
2251 }
2252
2253 @override
2254 SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
2255 switch (event.type) {
2256 case SelectionEventType.startEdgeUpdate:
2257 didReceiveSelectionEventFor(selectable: selectable, forEnd: false);
2258 ensureChildUpdated(selectable);
2259 case SelectionEventType.endEdgeUpdate:
2260 didReceiveSelectionEventFor(selectable: selectable, forEnd: true);
2261 ensureChildUpdated(selectable);
2262 case SelectionEventType.clear:
2263 clearInternalSelectionStateForSelectable(selectable);
2264 case SelectionEventType.selectAll:
2265 case SelectionEventType.selectWord:
2266 case SelectionEventType.selectParagraph:
2267 break;
2268 case SelectionEventType.granularlyExtendSelection:
2269 case SelectionEventType.directionallyExtendSelection:
2270 didReceiveSelectionEventFor(selectable: selectable);
2271 ensureChildUpdated(selectable);
2272 }
2273 return super.dispatchSelectionEventToChild(selectable, event);
2274 }
2275
2276 /// Ensures the `selectable` child has received the most up to date selection events.
2277 ///
2278 /// This method is called when:
2279 /// 1. A new [Selectable] is added to the delegate, and its screen location
2280 /// falls into the previous selection.
2281 /// 2. Before a [SelectionEvent] of type
2282 /// [SelectionEventType.startEdgeUpdate], [SelectionEventType.endEdgeUpdate],
2283 /// [SelectionEventType.granularlyExtendSelection], or
2284 /// [SelectionEventType.directionallyExtendSelection] is dispatched
2285 /// to a [Selectable] child.
2286 @override
2287 void ensureChildUpdated(Selectable selectable) {
2288 if (_lastEndEdgeUpdateGlobalPosition != null && _hasReceivedEndEvent.add(selectable)) {
2289 final SelectionEdgeUpdateEvent synthesizedEvent = SelectionEdgeUpdateEvent.forEnd(
2290 globalPosition: _lastEndEdgeUpdateGlobalPosition!,
2291 );
2292 if (currentSelectionEndIndex == -1) {
2293 handleSelectionEdgeUpdate(synthesizedEvent);
2294 }
2295 selectable.dispatchSelectionEvent(synthesizedEvent);
2296 }
2297 if (_lastStartEdgeUpdateGlobalPosition != null && _hasReceivedStartEvent.add(selectable)) {
2298 final SelectionEdgeUpdateEvent synthesizedEvent = SelectionEdgeUpdateEvent.forStart(
2299 globalPosition: _lastStartEdgeUpdateGlobalPosition!,
2300 );
2301 if (currentSelectionStartIndex == -1) {
2302 handleSelectionEdgeUpdate(synthesizedEvent);
2303 }
2304 selectable.dispatchSelectionEvent(synthesizedEvent);
2305 }
2306 }
2307
2308 @override
2309 void didChangeSelectables() {
2310 if (_lastEndEdgeUpdateGlobalPosition != null) {
2311 handleSelectionEdgeUpdate(
2312 SelectionEdgeUpdateEvent.forEnd(globalPosition: _lastEndEdgeUpdateGlobalPosition!),
2313 );
2314 }
2315 if (_lastStartEdgeUpdateGlobalPosition != null) {
2316 handleSelectionEdgeUpdate(
2317 SelectionEdgeUpdateEvent.forStart(globalPosition: _lastStartEdgeUpdateGlobalPosition!),
2318 );
2319 }
2320 final Set<Selectable> selectableSet = selectables.toSet();
2321 _hasReceivedEndEvent.removeWhere(
2322 (Selectable selectable) => !selectableSet.contains(selectable),
2323 );
2324 _hasReceivedStartEvent.removeWhere(
2325 (Selectable selectable) => !selectableSet.contains(selectable),
2326 );
2327 super.didChangeSelectables();
2328 }
2329}
2330
2331/// A delegate that handles events and updates for multiple [Selectable]
2332/// children.
2333///
2334/// Updates are optimized by tracking which [Selectable]s reside on the edges of
2335/// a selection. Subclasses should implement [ensureChildUpdated] to describe
2336/// how a [Selectable] should behave when added to a selection.
2337abstract class MultiSelectableSelectionContainerDelegate extends SelectionContainerDelegate
2338 with ChangeNotifier {
2339 /// Creates an instance of [MultiSelectableSelectionContainerDelegate].
2340 MultiSelectableSelectionContainerDelegate() {
2341 if (kFlutterMemoryAllocationsEnabled) {
2342 ChangeNotifier.maybeDispatchObjectCreation(this);
2343 }
2344 }
2345
2346 /// Gets the list of [Selectable]s this delegate is managing.
2347 List<Selectable> selectables = <Selectable>[];
2348
2349 /// The number of additional pixels added to the selection handle drawable
2350 /// area.
2351 ///
2352 /// Selection handles that are outside of the drawable area will be hidden.
2353 /// That logic prevents handles that get scrolled off the viewport from being
2354 /// drawn on the screen.
2355 ///
2356 /// The drawable area = current rectangle of [SelectionContainer] +
2357 /// _kSelectionHandleDrawableAreaPadding on each side.
2358 ///
2359 /// This was an eyeballed value to create smooth user experiences.
2360 static const double _kSelectionHandleDrawableAreaPadding = 5.0;
2361
2362 /// The current [Selectable] that contains the selection end edge.
2363 @protected
2364 int currentSelectionEndIndex = -1;
2365
2366 /// The current [Selectable] that contains the selection start edge.
2367 @protected
2368 int currentSelectionStartIndex = -1;
2369
2370 LayerLink? _startHandleLayer;
2371 Selectable? _startHandleLayerOwner;
2372 LayerLink? _endHandleLayer;
2373 Selectable? _endHandleLayerOwner;
2374
2375 bool _isHandlingSelectionEvent = false;
2376 bool _scheduledSelectableUpdate = false;
2377 bool _selectionInProgress = false;
2378 Set<Selectable> _additions = <Selectable>{};
2379
2380 bool _extendSelectionInProgress = false;
2381
2382 @override
2383 void add(Selectable selectable) {
2384 assert(!selectables.contains(selectable));
2385 _additions.add(selectable);
2386 _scheduleSelectableUpdate();
2387 }
2388
2389 @override
2390 void remove(Selectable selectable) {
2391 if (_additions.remove(selectable)) {
2392 // The same selectable was added in the same frame and is not yet
2393 // incorporated into the selectables.
2394 //
2395 // Removing such selectable doesn't require selection geometry update.
2396 return;
2397 }
2398 _removeSelectable(selectable);
2399 _scheduleSelectableUpdate();
2400 }
2401
2402 /// Notifies this delegate that layout of the container has changed.
2403 void layoutDidChange() {
2404 _updateSelectionGeometry();
2405 }
2406
2407 void _scheduleSelectableUpdate() {
2408 if (!_scheduledSelectableUpdate) {
2409 _scheduledSelectableUpdate = true;
2410 void runScheduledTask([Duration? duration]) {
2411 if (!_scheduledSelectableUpdate) {
2412 return;
2413 }
2414 _scheduledSelectableUpdate = false;
2415 _updateSelectables();
2416 }
2417
2418 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.postFrameCallbacks) {
2419 // A new task can be scheduled as a result of running the scheduled task
2420 // from another MultiSelectableSelectionContainerDelegate. This can
2421 // happen if nesting two SelectionContainers. The selectable can be
2422 // safely updated in the same frame in this case.
2423 scheduleMicrotask(runScheduledTask);
2424 } else {
2425 SchedulerBinding.instance.addPostFrameCallback(
2426 runScheduledTask,
2427 debugLabel: 'SelectionContainer.runScheduledTask',
2428 );
2429 }
2430 }
2431 }
2432
2433 void _updateSelectables() {
2434 // Remove offScreen selectable.
2435 if (_additions.isNotEmpty) {
2436 _flushAdditions();
2437 }
2438 didChangeSelectables();
2439 }
2440
2441 void _flushAdditions() {
2442 final List<Selectable> mergingSelectables = _additions.toList()..sort(compareOrder);
2443 final List<Selectable> existingSelectables = selectables;
2444 selectables = <Selectable>[];
2445 int mergingIndex = 0;
2446 int existingIndex = 0;
2447 int selectionStartIndex = currentSelectionStartIndex;
2448 int selectionEndIndex = currentSelectionEndIndex;
2449 // Merge two sorted lists.
2450 while (mergingIndex < mergingSelectables.length || existingIndex < existingSelectables.length) {
2451 if (mergingIndex >= mergingSelectables.length ||
2452 (existingIndex < existingSelectables.length &&
2453 compareOrder(existingSelectables[existingIndex], mergingSelectables[mergingIndex]) <
2454 0)) {
2455 if (existingIndex == currentSelectionStartIndex) {
2456 selectionStartIndex = selectables.length;
2457 }
2458 if (existingIndex == currentSelectionEndIndex) {
2459 selectionEndIndex = selectables.length;
2460 }
2461 selectables.add(existingSelectables[existingIndex]);
2462 existingIndex += 1;
2463 continue;
2464 }
2465
2466 // If the merging selectable falls in the selection range, their selection
2467 // needs to be updated.
2468 final Selectable mergingSelectable = mergingSelectables[mergingIndex];
2469 if (existingIndex < max(currentSelectionStartIndex, currentSelectionEndIndex) &&
2470 existingIndex > min(currentSelectionStartIndex, currentSelectionEndIndex)) {
2471 ensureChildUpdated(mergingSelectable);
2472 }
2473 mergingSelectable.addListener(_handleSelectableGeometryChange);
2474 selectables.add(mergingSelectable);
2475 mergingIndex += 1;
2476 }
2477 assert(
2478 mergingIndex == mergingSelectables.length &&
2479 existingIndex == existingSelectables.length &&
2480 selectables.length == existingIndex + mergingIndex,
2481 );
2482 assert(selectionStartIndex >= -1 || selectionStartIndex < selectables.length);
2483 assert(selectionEndIndex >= -1 || selectionEndIndex < selectables.length);
2484 // selection indices should not be set to -1 unless they originally were.
2485 assert((currentSelectionStartIndex == -1) == (selectionStartIndex == -1));
2486 assert((currentSelectionEndIndex == -1) == (selectionEndIndex == -1));
2487 currentSelectionEndIndex = selectionEndIndex;
2488 currentSelectionStartIndex = selectionStartIndex;
2489 _additions = <Selectable>{};
2490 }
2491
2492 void _removeSelectable(Selectable selectable) {
2493 assert(selectables.contains(selectable), 'The selectable is not in this registrar.');
2494 final int index = selectables.indexOf(selectable);
2495 selectables.removeAt(index);
2496 if (index <= currentSelectionEndIndex) {
2497 currentSelectionEndIndex -= 1;
2498 }
2499 if (index <= currentSelectionStartIndex) {
2500 currentSelectionStartIndex -= 1;
2501 }
2502 selectable.removeListener(_handleSelectableGeometryChange);
2503 }
2504
2505 /// Called when this delegate finishes updating the [Selectable]s.
2506 @protected
2507 @mustCallSuper
2508 void didChangeSelectables() {
2509 _updateSelectionGeometry();
2510 }
2511
2512 @override
2513 SelectionGeometry get value => _selectionGeometry;
2514 SelectionGeometry _selectionGeometry = const SelectionGeometry(
2515 hasContent: false,
2516 status: SelectionStatus.none,
2517 );
2518
2519 /// Updates the [value] in this class and notifies listeners if necessary.
2520 void _updateSelectionGeometry() {
2521 final SelectionGeometry newValue = getSelectionGeometry();
2522 if (_selectionGeometry != newValue) {
2523 _selectionGeometry = newValue;
2524 notifyListeners();
2525 }
2526 _updateHandleLayersAndOwners();
2527 }
2528
2529 static Rect _getBoundingBox(Selectable selectable) {
2530 Rect result = selectable.boundingBoxes.first;
2531 for (int index = 1; index < selectable.boundingBoxes.length; index += 1) {
2532 result = result.expandToInclude(selectable.boundingBoxes[index]);
2533 }
2534 return result;
2535 }
2536
2537 /// The compare function this delegate used for determining the selection
2538 /// order of the selectables.
2539 ///
2540 /// Defaults to screen order.
2541 @protected
2542 Comparator<Selectable> get compareOrder => _compareScreenOrder;
2543
2544 static int _compareScreenOrder(Selectable a, Selectable b) {
2545 final Rect rectA = MatrixUtils.transformRect(a.getTransformTo(null), _getBoundingBox(a));
2546 final Rect rectB = MatrixUtils.transformRect(b.getTransformTo(null), _getBoundingBox(b));
2547 final int result = _compareVertically(rectA, rectB);
2548 if (result != 0) {
2549 return result;
2550 }
2551 return _compareHorizontally(rectA, rectB);
2552 }
2553
2554 /// Compares two rectangles in the screen order solely by their vertical
2555 /// positions.
2556 ///
2557 /// Returns positive if a is lower, negative if a is higher, 0 if their
2558 /// order can't be determine solely by their vertical position.
2559 static int _compareVertically(Rect a, Rect b) {
2560 // The rectangles overlap so defer to horizontal comparison.
2561 if ((a.top - b.top < _kSelectableVerticalComparingThreshold &&
2562 a.bottom - b.bottom > -_kSelectableVerticalComparingThreshold) ||
2563 (b.top - a.top < _kSelectableVerticalComparingThreshold &&
2564 b.bottom - a.bottom > -_kSelectableVerticalComparingThreshold)) {
2565 return 0;
2566 }
2567 if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) {
2568 return a.top > b.top ? 1 : -1;
2569 }
2570 return a.bottom > b.bottom ? 1 : -1;
2571 }
2572
2573 /// Compares two rectangles in the screen order by their horizontal positions
2574 /// assuming one of the rectangles enclose the other rect vertically.
2575 ///
2576 /// Returns positive if a is lower, negative if a is higher.
2577 static int _compareHorizontally(Rect a, Rect b) {
2578 // a encloses b.
2579 if (a.left - b.left < precisionErrorTolerance && a.right - b.right > -precisionErrorTolerance) {
2580 return -1;
2581 }
2582 // b encloses a.
2583 if (b.left - a.left < precisionErrorTolerance && b.right - a.right > -precisionErrorTolerance) {
2584 return 1;
2585 }
2586 if ((a.left - b.left).abs() > precisionErrorTolerance) {
2587 return a.left > b.left ? 1 : -1;
2588 }
2589 return a.right > b.right ? 1 : -1;
2590 }
2591
2592 void _handleSelectableGeometryChange() {
2593 // Geometries of selectable children may change multiple times when handling
2594 // selection events. Ignore these updates since the selection geometry of
2595 // this delegate will be updated after handling the selection events.
2596 if (_isHandlingSelectionEvent) {
2597 return;
2598 }
2599 _updateSelectionGeometry();
2600 }
2601
2602 /// Gets the combined [SelectionGeometry] for child [Selectable]s.
2603 @protected
2604 SelectionGeometry getSelectionGeometry() {
2605 if (currentSelectionEndIndex == -1 || currentSelectionStartIndex == -1 || selectables.isEmpty) {
2606 // There is no valid selection.
2607 return SelectionGeometry(status: SelectionStatus.none, hasContent: selectables.isNotEmpty);
2608 }
2609
2610 if (!_extendSelectionInProgress) {
2611 currentSelectionStartIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
2612 currentSelectionStartIndex,
2613 currentSelectionEndIndex,
2614 );
2615 currentSelectionEndIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
2616 currentSelectionEndIndex,
2617 currentSelectionStartIndex,
2618 );
2619 }
2620
2621 // Need to find the non-null start selection point.
2622 SelectionGeometry startGeometry = selectables[currentSelectionStartIndex].value;
2623 final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
2624 int startIndexWalker = currentSelectionStartIndex;
2625 while (startIndexWalker != currentSelectionEndIndex &&
2626 startGeometry.startSelectionPoint == null) {
2627 startIndexWalker += forwardSelection ? 1 : -1;
2628 startGeometry = selectables[startIndexWalker].value;
2629 }
2630
2631 SelectionPoint? startPoint;
2632 if (startGeometry.startSelectionPoint != null) {
2633 final Matrix4 startTransform = getTransformFrom(selectables[startIndexWalker]);
2634 final Offset start = MatrixUtils.transformPoint(
2635 startTransform,
2636 startGeometry.startSelectionPoint!.localPosition,
2637 );
2638 // It can be NaN if it is detached or off-screen.
2639 if (start.isFinite) {
2640 startPoint = SelectionPoint(
2641 localPosition: start,
2642 lineHeight: startGeometry.startSelectionPoint!.lineHeight,
2643 handleType: startGeometry.startSelectionPoint!.handleType,
2644 );
2645 }
2646 }
2647
2648 // Need to find the non-null end selection point.
2649 SelectionGeometry endGeometry = selectables[currentSelectionEndIndex].value;
2650 int endIndexWalker = currentSelectionEndIndex;
2651 while (endIndexWalker != currentSelectionStartIndex && endGeometry.endSelectionPoint == null) {
2652 endIndexWalker += forwardSelection ? -1 : 1;
2653 endGeometry = selectables[endIndexWalker].value;
2654 }
2655 SelectionPoint? endPoint;
2656 if (endGeometry.endSelectionPoint != null) {
2657 final Matrix4 endTransform = getTransformFrom(selectables[endIndexWalker]);
2658 final Offset end = MatrixUtils.transformPoint(
2659 endTransform,
2660 endGeometry.endSelectionPoint!.localPosition,
2661 );
2662 // It can be NaN if it is detached or off-screen.
2663 if (end.isFinite) {
2664 endPoint = SelectionPoint(
2665 localPosition: end,
2666 lineHeight: endGeometry.endSelectionPoint!.lineHeight,
2667 handleType: endGeometry.endSelectionPoint!.handleType,
2668 );
2669 }
2670 }
2671
2672 // Need to collect selection rects from selectables ranging from the
2673 // currentSelectionStartIndex to the currentSelectionEndIndex.
2674 final List<Rect> selectionRects = <Rect>[];
2675 final Rect? drawableArea =
2676 hasSize ? Rect.fromLTWH(0, 0, containerSize.width, containerSize.height) : null;
2677 for (int index = currentSelectionStartIndex; index <= currentSelectionEndIndex; index++) {
2678 final List<Rect> currSelectableSelectionRects = selectables[index].value.selectionRects;
2679 final List<Rect> selectionRectsWithinDrawableArea =
2680 currSelectableSelectionRects
2681 .map((Rect selectionRect) {
2682 final Matrix4 transform = getTransformFrom(selectables[index]);
2683 final Rect localRect = MatrixUtils.transformRect(transform, selectionRect);
2684 return drawableArea?.intersect(localRect) ?? localRect;
2685 })
2686 .where((Rect selectionRect) {
2687 return selectionRect.isFinite && !selectionRect.isEmpty;
2688 })
2689 .toList();
2690 selectionRects.addAll(selectionRectsWithinDrawableArea);
2691 }
2692
2693 return SelectionGeometry(
2694 startSelectionPoint: startPoint,
2695 endSelectionPoint: endPoint,
2696 selectionRects: selectionRects,
2697 status: startGeometry != endGeometry ? SelectionStatus.uncollapsed : startGeometry.status,
2698 // Would have at least one selectable child.
2699 hasContent: true,
2700 );
2701 }
2702
2703 // The currentSelectionStartIndex or currentSelectionEndIndex may not be
2704 // the current index that contains selection edges. This can happen if the
2705 // selection edge is in between two selectables. One of the selectable will
2706 // have its selection collapsed at the index 0 or contentLength depends on
2707 // whether the selection is reversed or not. The current selection index can
2708 // be point to either one.
2709 //
2710 // This method adjusts the index to point to selectable with valid selection.
2711 int _adjustSelectionIndexBasedOnSelectionGeometry(int currentIndex, int towardIndex) {
2712 final bool forward = towardIndex > currentIndex;
2713 while (currentIndex != towardIndex &&
2714 selectables[currentIndex].value.status != SelectionStatus.uncollapsed) {
2715 currentIndex += forward ? 1 : -1;
2716 }
2717 return currentIndex;
2718 }
2719
2720 @override
2721 void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
2722 if (_startHandleLayer == startHandle && _endHandleLayer == endHandle) {
2723 return;
2724 }
2725 _startHandleLayer = startHandle;
2726 _endHandleLayer = endHandle;
2727 _updateHandleLayersAndOwners();
2728 }
2729
2730 /// Pushes both handle layers to the selectables that contain selection edges.
2731 ///
2732 /// This method needs to be called every time the selectables that contain the
2733 /// selection edges change, i.e. [currentSelectionStartIndex] or
2734 /// [currentSelectionEndIndex] changes. Otherwise, the handle may be painted
2735 /// in the wrong place.
2736 void _updateHandleLayersAndOwners() {
2737 LayerLink? effectiveStartHandle = _startHandleLayer;
2738 LayerLink? effectiveEndHandle = _endHandleLayer;
2739 if (effectiveStartHandle != null || effectiveEndHandle != null) {
2740 final Rect? drawableArea =
2741 hasSize
2742 ? Rect.fromLTWH(
2743 0,
2744 0,
2745 containerSize.width,
2746 containerSize.height,
2747 ).inflate(_kSelectionHandleDrawableAreaPadding)
2748 : null;
2749 final bool hideStartHandle =
2750 value.startSelectionPoint == null ||
2751 drawableArea == null ||
2752 !drawableArea.contains(value.startSelectionPoint!.localPosition);
2753 final bool hideEndHandle =
2754 value.endSelectionPoint == null ||
2755 drawableArea == null ||
2756 !drawableArea.contains(value.endSelectionPoint!.localPosition);
2757 effectiveStartHandle = hideStartHandle ? null : _startHandleLayer;
2758 effectiveEndHandle = hideEndHandle ? null : _endHandleLayer;
2759 }
2760 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
2761 // No valid selection.
2762 if (_startHandleLayerOwner != null) {
2763 _startHandleLayerOwner!.pushHandleLayers(null, null);
2764 _startHandleLayerOwner = null;
2765 }
2766 if (_endHandleLayerOwner != null) {
2767 _endHandleLayerOwner!.pushHandleLayers(null, null);
2768 _endHandleLayerOwner = null;
2769 }
2770 return;
2771 }
2772
2773 if (selectables[currentSelectionStartIndex] != _startHandleLayerOwner) {
2774 _startHandleLayerOwner?.pushHandleLayers(null, null);
2775 }
2776 if (selectables[currentSelectionEndIndex] != _endHandleLayerOwner) {
2777 _endHandleLayerOwner?.pushHandleLayers(null, null);
2778 }
2779
2780 _startHandleLayerOwner = selectables[currentSelectionStartIndex];
2781
2782 if (currentSelectionStartIndex == currentSelectionEndIndex) {
2783 // Selection edges is on the same selectable.
2784 _endHandleLayerOwner = _startHandleLayerOwner;
2785 _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, effectiveEndHandle);
2786 return;
2787 }
2788
2789 _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, null);
2790 _endHandleLayerOwner = selectables[currentSelectionEndIndex];
2791 _endHandleLayerOwner!.pushHandleLayers(null, effectiveEndHandle);
2792 }
2793
2794 /// Copies the selected contents of all [Selectable]s.
2795 @override
2796 SelectedContent? getSelectedContent() {
2797 final List<SelectedContent> selections = <SelectedContent>[
2798 for (final Selectable selectable in selectables)
2799 if (selectable.getSelectedContent() case final SelectedContent data) data,
2800 ];
2801 if (selections.isEmpty) {
2802 return null;
2803 }
2804 final StringBuffer buffer = StringBuffer();
2805 for (final SelectedContent selection in selections) {
2806 buffer.write(selection.plainText);
2807 }
2808 return SelectedContent(plainText: buffer.toString());
2809 }
2810
2811 /// The total length of the content under this [SelectionContainerDelegate].
2812 ///
2813 /// This value is derived from the [Selectable.contentLength] of each [Selectable]
2814 /// managed by this delegate.
2815 @override
2816 int get contentLength =>
2817 selectables.fold<int>(0, (int sum, Selectable selectable) => sum + selectable.contentLength);
2818
2819 /// This method calculates a local [SelectedContentRange] based on the list
2820 /// of [selections] that are accumulated from the [Selectable] children under this
2821 /// delegate. This calculation takes into account the accumulated content
2822 /// length before the active selection, and returns null when either selection
2823 /// edge has not been set.
2824 SelectedContentRange? _calculateLocalRange(List<_SelectionInfo> selections) {
2825 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
2826 return null;
2827 }
2828 int startOffset = 0;
2829 int endOffset = 0;
2830 bool foundStart = false;
2831 bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
2832 if (currentSelectionEndIndex == currentSelectionStartIndex) {
2833 // Determining selection direction is innacurate if currentSelectionStartIndex == currentSelectionEndIndex.
2834 // Use the range from the selectable within the selection as the source of truth for selection direction.
2835 final SelectedContentRange rangeAtSelectableInSelection =
2836 selectables[currentSelectionStartIndex].getSelection()!;
2837 forwardSelection =
2838 rangeAtSelectableInSelection.endOffset >= rangeAtSelectableInSelection.startOffset;
2839 }
2840 for (int index = 0; index < selections.length; index++) {
2841 final _SelectionInfo selection = selections[index];
2842 if (selection.range == null) {
2843 if (foundStart) {
2844 return SelectedContentRange(
2845 startOffset: forwardSelection ? startOffset : endOffset,
2846 endOffset: forwardSelection ? endOffset : startOffset,
2847 );
2848 }
2849 startOffset += selection.contentLength;
2850 endOffset = startOffset;
2851 continue;
2852 }
2853 final int selectionStartNormalized = min(
2854 selection.range!.startOffset,
2855 selection.range!.endOffset,
2856 );
2857 final int selectionEndNormalized = max(
2858 selection.range!.startOffset,
2859 selection.range!.endOffset,
2860 );
2861 if (!foundStart) {
2862 startOffset += selectionStartNormalized;
2863 endOffset = startOffset + (selectionEndNormalized - selectionStartNormalized).abs();
2864 foundStart = true;
2865 } else {
2866 endOffset += (selectionEndNormalized - selectionStartNormalized).abs();
2867 }
2868 }
2869 assert(
2870 foundStart,
2871 'The start of the selection has not been found despite this selection delegate having an existing currentSelectionStartIndex and currentSelectionEndIndex.',
2872 );
2873 return SelectedContentRange(
2874 startOffset: forwardSelection ? startOffset : endOffset,
2875 endOffset: forwardSelection ? endOffset : startOffset,
2876 );
2877 }
2878
2879 /// Returns a [SelectedContentRange] considering the [SelectedContentRange]
2880 /// from each [Selectable] child managed under this delegate.
2881 ///
2882 /// When nothing is selected or either selection edge has not been set,
2883 /// this method will return `null`.
2884 @override
2885 SelectedContentRange? getSelection() {
2886 final List<_SelectionInfo> selections = <_SelectionInfo>[
2887 for (final Selectable selectable in selectables)
2888 (contentLength: selectable.contentLength, range: selectable.getSelection()),
2889 ];
2890 return _calculateLocalRange(selections);
2891 }
2892
2893 // Clears the selection on all selectables not in the range of
2894 // currentSelectionStartIndex..currentSelectionEndIndex.
2895 //
2896 // If one of the edges does not exist, then this method will clear the selection
2897 // in all selectables except the existing edge.
2898 //
2899 // If neither of the edges exist this method immediately returns.
2900 void _flushInactiveSelections() {
2901 if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) {
2902 return;
2903 }
2904 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
2905 final int skipIndex =
2906 currentSelectionStartIndex == -1 ? currentSelectionEndIndex : currentSelectionStartIndex;
2907 selectables
2908 .where((Selectable target) => target != selectables[skipIndex])
2909 .forEach(
2910 (Selectable target) =>
2911 dispatchSelectionEventToChild(target, const ClearSelectionEvent()),
2912 );
2913 return;
2914 }
2915 final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex);
2916 final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex);
2917 for (int index = 0; index < selectables.length; index += 1) {
2918 if (index >= skipStart && index <= skipEnd) {
2919 continue;
2920 }
2921 dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent());
2922 }
2923 }
2924
2925 /// Selects all contents of all [Selectable]s.
2926 @protected
2927 SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
2928 for (final Selectable selectable in selectables) {
2929 dispatchSelectionEventToChild(selectable, event);
2930 }
2931 currentSelectionStartIndex = 0;
2932 currentSelectionEndIndex = selectables.length - 1;
2933 return SelectionResult.none;
2934 }
2935
2936 SelectionResult _handleSelectBoundary(SelectionEvent event) {
2937 assert(
2938 event is SelectWordSelectionEvent || event is SelectParagraphSelectionEvent,
2939 'This method should only be given selection events that select text boundaries.',
2940 );
2941 late final Offset effectiveGlobalPosition;
2942 if (event.type == SelectionEventType.selectWord) {
2943 effectiveGlobalPosition = (event as SelectWordSelectionEvent).globalPosition;
2944 } else if (event.type == SelectionEventType.selectParagraph) {
2945 effectiveGlobalPosition = (event as SelectParagraphSelectionEvent).globalPosition;
2946 }
2947 SelectionResult? lastSelectionResult;
2948 for (int index = 0; index < selectables.length; index += 1) {
2949 bool globalRectsContainPosition = false;
2950 if (selectables[index].boundingBoxes.isNotEmpty) {
2951 for (final Rect rect in selectables[index].boundingBoxes) {
2952 final Rect globalRect = MatrixUtils.transformRect(
2953 selectables[index].getTransformTo(null),
2954 rect,
2955 );
2956 if (globalRect.contains(effectiveGlobalPosition)) {
2957 globalRectsContainPosition = true;
2958 break;
2959 }
2960 }
2961 }
2962 if (globalRectsContainPosition) {
2963 final SelectionGeometry existingGeometry = selectables[index].value;
2964 lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event);
2965 if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) {
2966 return SelectionResult.next;
2967 }
2968 if (lastSelectionResult == SelectionResult.next) {
2969 continue;
2970 }
2971 if (index == 0 && lastSelectionResult == SelectionResult.previous) {
2972 return SelectionResult.previous;
2973 }
2974 if (selectables[index].value != existingGeometry) {
2975 // Geometry has changed as a result of select word, need to clear the
2976 // selection of other selectables to keep selection in sync.
2977 selectables
2978 .where((Selectable target) => target != selectables[index])
2979 .forEach(
2980 (Selectable target) =>
2981 dispatchSelectionEventToChild(target, const ClearSelectionEvent()),
2982 );
2983 currentSelectionStartIndex = currentSelectionEndIndex = index;
2984 }
2985 return SelectionResult.end;
2986 } else {
2987 if (lastSelectionResult == SelectionResult.next) {
2988 currentSelectionStartIndex = currentSelectionEndIndex = index - 1;
2989 return SelectionResult.end;
2990 }
2991 }
2992 }
2993 assert(lastSelectionResult == null);
2994 return SelectionResult.end;
2995 }
2996
2997 /// Selects a word in a [Selectable] at the location
2998 /// [SelectWordSelectionEvent.globalPosition].
2999 @protected
3000 SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
3001 return _handleSelectBoundary(event);
3002 }
3003
3004 /// Selects a paragraph in a [Selectable] at the location
3005 /// [SelectParagraphSelectionEvent.globalPosition].
3006 @protected
3007 SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
3008 return _handleSelectBoundary(event);
3009 }
3010
3011 /// Removes the selection of all [Selectable]s this delegate manages.
3012 @protected
3013 SelectionResult handleClearSelection(ClearSelectionEvent event) {
3014 for (final Selectable selectable in selectables) {
3015 dispatchSelectionEventToChild(selectable, event);
3016 }
3017 currentSelectionEndIndex = -1;
3018 currentSelectionStartIndex = -1;
3019 return SelectionResult.none;
3020 }
3021
3022 /// Extend current selection in a certain [TextGranularity].
3023 @protected
3024 SelectionResult handleGranularlyExtendSelection(GranularlyExtendSelectionEvent event) {
3025 assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
3026 if (currentSelectionStartIndex == -1) {
3027 if (event.forward) {
3028 currentSelectionStartIndex = currentSelectionEndIndex = 0;
3029 } else {
3030 currentSelectionStartIndex = currentSelectionEndIndex = selectables.length - 1;
3031 }
3032 }
3033 int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
3034 SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
3035 if (event.forward) {
3036 assert(result != SelectionResult.previous);
3037 while (targetIndex < selectables.length - 1 && result == SelectionResult.next) {
3038 targetIndex += 1;
3039 result = dispatchSelectionEventToChild(selectables[targetIndex], event);
3040 assert(result != SelectionResult.previous);
3041 }
3042 } else {
3043 assert(result != SelectionResult.next);
3044 while (targetIndex > 0 && result == SelectionResult.previous) {
3045 targetIndex -= 1;
3046 result = dispatchSelectionEventToChild(selectables[targetIndex], event);
3047 assert(result != SelectionResult.next);
3048 }
3049 }
3050 if (event.isEnd) {
3051 currentSelectionEndIndex = targetIndex;
3052 } else {
3053 currentSelectionStartIndex = targetIndex;
3054 }
3055 return result;
3056 }
3057
3058 /// Extend current selection in a certain [TextGranularity].
3059 @protected
3060 SelectionResult handleDirectionallyExtendSelection(DirectionallyExtendSelectionEvent event) {
3061 assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
3062 if (currentSelectionStartIndex == -1) {
3063 currentSelectionStartIndex =
3064 currentSelectionEndIndex = switch (event.direction) {
3065 SelectionExtendDirection.previousLine ||
3066 SelectionExtendDirection.backward => selectables.length - 1,
3067 SelectionExtendDirection.nextLine || SelectionExtendDirection.forward => 0,
3068 };
3069 }
3070 int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
3071 SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
3072 switch (event.direction) {
3073 case SelectionExtendDirection.previousLine:
3074 assert(result == SelectionResult.end || result == SelectionResult.previous);
3075 if (result == SelectionResult.previous) {
3076 if (targetIndex > 0) {
3077 targetIndex -= 1;
3078 result = dispatchSelectionEventToChild(
3079 selectables[targetIndex],
3080 event.copyWith(direction: SelectionExtendDirection.backward),
3081 );
3082 assert(result == SelectionResult.end);
3083 }
3084 }
3085 case SelectionExtendDirection.nextLine:
3086 assert(result == SelectionResult.end || result == SelectionResult.next);
3087 if (result == SelectionResult.next) {
3088 if (targetIndex < selectables.length - 1) {
3089 targetIndex += 1;
3090 result = dispatchSelectionEventToChild(
3091 selectables[targetIndex],
3092 event.copyWith(direction: SelectionExtendDirection.forward),
3093 );
3094 assert(result == SelectionResult.end);
3095 }
3096 }
3097 case SelectionExtendDirection.forward:
3098 case SelectionExtendDirection.backward:
3099 assert(result == SelectionResult.end);
3100 }
3101 if (event.isEnd) {
3102 currentSelectionEndIndex = targetIndex;
3103 } else {
3104 currentSelectionStartIndex = targetIndex;
3105 }
3106 return result;
3107 }
3108
3109 /// Updates the selection edges.
3110 @protected
3111 SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
3112 if (event.type == SelectionEventType.endEdgeUpdate) {
3113 return currentSelectionEndIndex == -1
3114 ? _initSelection(event, isEnd: true)
3115 : _adjustSelection(event, isEnd: true);
3116 }
3117 return currentSelectionStartIndex == -1
3118 ? _initSelection(event, isEnd: false)
3119 : _adjustSelection(event, isEnd: false);
3120 }
3121
3122 @override
3123 SelectionResult dispatchSelectionEvent(SelectionEvent event) {
3124 final bool selectionWillBeInProgress = event is! ClearSelectionEvent;
3125 if (!_selectionInProgress && selectionWillBeInProgress) {
3126 // Sort the selectable every time a selection start.
3127 selectables.sort(compareOrder);
3128 }
3129 _selectionInProgress = selectionWillBeInProgress;
3130 _isHandlingSelectionEvent = true;
3131 late SelectionResult result;
3132 switch (event.type) {
3133 case SelectionEventType.startEdgeUpdate:
3134 case SelectionEventType.endEdgeUpdate:
3135 _extendSelectionInProgress = false;
3136 result = handleSelectionEdgeUpdate(event as SelectionEdgeUpdateEvent);
3137 case SelectionEventType.clear:
3138 _extendSelectionInProgress = false;
3139 result = handleClearSelection(event as ClearSelectionEvent);
3140 case SelectionEventType.selectAll:
3141 _extendSelectionInProgress = false;
3142 result = handleSelectAll(event as SelectAllSelectionEvent);
3143 case SelectionEventType.selectWord:
3144 _extendSelectionInProgress = false;
3145 result = handleSelectWord(event as SelectWordSelectionEvent);
3146 case SelectionEventType.selectParagraph:
3147 _extendSelectionInProgress = false;
3148 result = handleSelectParagraph(event as SelectParagraphSelectionEvent);
3149 case SelectionEventType.granularlyExtendSelection:
3150 _extendSelectionInProgress = true;
3151 result = handleGranularlyExtendSelection(event as GranularlyExtendSelectionEvent);
3152 case SelectionEventType.directionallyExtendSelection:
3153 _extendSelectionInProgress = true;
3154 result = handleDirectionallyExtendSelection(event as DirectionallyExtendSelectionEvent);
3155 }
3156 _isHandlingSelectionEvent = false;
3157 _updateSelectionGeometry();
3158 return result;
3159 }
3160
3161 @override
3162 void dispose() {
3163 for (final Selectable selectable in selectables) {
3164 selectable.removeListener(_handleSelectableGeometryChange);
3165 }
3166 selectables = const <Selectable>[];
3167 _scheduledSelectableUpdate = false;
3168 super.dispose();
3169 }
3170
3171 /// Ensures the [Selectable] child has received up to date selection event.
3172 ///
3173 /// This method is called when a new [Selectable] is added to the delegate,
3174 /// and its screen location falls into the previous selection.
3175 ///
3176 /// Subclasses are responsible for updating the selection of this newly added
3177 /// [Selectable].
3178 @protected
3179 void ensureChildUpdated(Selectable selectable);
3180
3181 /// Dispatches a selection event to a specific [Selectable].
3182 ///
3183 /// Override this method if subclasses need to generate additional events or
3184 /// treatments prior to sending the [SelectionEvent].
3185 @protected
3186 SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
3187 return selectable.dispatchSelectionEvent(event);
3188 }
3189
3190 /// Initializes the selection of the selectable children.
3191 ///
3192 /// The goal is to find the selectable child that contains the selection edge.
3193 /// Returns [SelectionResult.end] if the selection edge ends on any of the
3194 /// children. Otherwise, it returns [SelectionResult.previous] if the selection
3195 /// does not reach any of its children. Returns [SelectionResult.next]
3196 /// if the selection reaches the end of its children.
3197 ///
3198 /// Ideally, this method should only be called twice at the beginning of the
3199 /// drag selection, once for start edge update event, once for end edge update
3200 /// event.
3201 SelectionResult _initSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
3202 assert(
3203 (isEnd && currentSelectionEndIndex == -1) || (!isEnd && currentSelectionStartIndex == -1),
3204 );
3205 int newIndex = -1;
3206 bool hasFoundEdgeIndex = false;
3207 SelectionResult? result;
3208 for (int index = 0; index < selectables.length && !hasFoundEdgeIndex; index += 1) {
3209 final Selectable child = selectables[index];
3210 final SelectionResult childResult = dispatchSelectionEventToChild(child, event);
3211 switch (childResult) {
3212 case SelectionResult.next:
3213 case SelectionResult.none:
3214 newIndex = index;
3215 case SelectionResult.end:
3216 newIndex = index;
3217 result = SelectionResult.end;
3218 hasFoundEdgeIndex = true;
3219 case SelectionResult.previous:
3220 hasFoundEdgeIndex = true;
3221 if (index == 0) {
3222 newIndex = 0;
3223 result = SelectionResult.previous;
3224 }
3225 result ??= SelectionResult.end;
3226 case SelectionResult.pending:
3227 newIndex = index;
3228 result = SelectionResult.pending;
3229 hasFoundEdgeIndex = true;
3230 }
3231 }
3232
3233 if (newIndex == -1) {
3234 assert(selectables.isEmpty);
3235 return SelectionResult.none;
3236 }
3237 if (isEnd) {
3238 currentSelectionEndIndex = newIndex;
3239 } else {
3240 currentSelectionStartIndex = newIndex;
3241 }
3242 _flushInactiveSelections();
3243 // The result can only be null if the loop went through the entire list
3244 // without any of the selection returned end or previous. In this case, the
3245 // caller of this method needs to find the next selectable in their list.
3246 return result ?? SelectionResult.next;
3247 }
3248
3249 /// Adjusts the selection based on the drag selection update event if there
3250 /// is already a selectable child that contains the selection edge.
3251 ///
3252 /// This method starts by sending the selection event to the current
3253 /// selectable that contains the selection edge, and finds forward or backward
3254 /// if that selectable no longer contains the selection edge.
3255 SelectionResult _adjustSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
3256 assert(() {
3257 if (isEnd) {
3258 assert(currentSelectionEndIndex < selectables.length && currentSelectionEndIndex >= 0);
3259 return true;
3260 }
3261 assert(currentSelectionStartIndex < selectables.length && currentSelectionStartIndex >= 0);
3262 return true;
3263 }());
3264 SelectionResult? finalResult;
3265 // Determines if the edge being adjusted is within the current viewport.
3266 // - If so, we begin the search for the new selection edge position at the
3267 // currentSelectionEndIndex/currentSelectionStartIndex.
3268 // - If not, we attempt to locate the new selection edge starting from
3269 // the opposite end.
3270 // - If neither edge is in the current viewport, the search for the new
3271 // selection edge position begins at 0.
3272 //
3273 // This can happen when there is a scrollable child and the edge being adjusted
3274 // has been scrolled out of view.
3275 final bool isCurrentEdgeWithinViewport =
3276 isEnd
3277 ? _selectionGeometry.endSelectionPoint != null
3278 : _selectionGeometry.startSelectionPoint != null;
3279 final bool isOppositeEdgeWithinViewport =
3280 isEnd
3281 ? _selectionGeometry.startSelectionPoint != null
3282 : _selectionGeometry.endSelectionPoint != null;
3283 int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
3284 (true, true, true) => currentSelectionEndIndex,
3285 (true, true, false) => currentSelectionEndIndex,
3286 (true, false, true) => currentSelectionStartIndex,
3287 (true, false, false) => 0,
3288 (false, true, true) => currentSelectionStartIndex,
3289 (false, true, false) => currentSelectionStartIndex,
3290 (false, false, true) => currentSelectionEndIndex,
3291 (false, false, false) => 0,
3292 };
3293 bool? forward;
3294 late SelectionResult currentSelectableResult;
3295 // This loop sends the selection event to one of the following to determine
3296 // the direction of the search.
3297 // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
3298 // is in the current viewport.
3299 // - The opposite edge index if the current edge is not in the current viewport.
3300 // - Index 0 if neither edge is in the current viewport.
3301 //
3302 // If the result is `SelectionResult.next`, this loop look backward.
3303 // Otherwise, it looks forward.
3304 //
3305 // The terminate condition are:
3306 // 1. the selectable returns end, pending, none.
3307 // 2. the selectable returns previous when looking forward.
3308 // 2. the selectable returns next when looking backward.
3309 while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) {
3310 currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event);
3311 switch (currentSelectableResult) {
3312 case SelectionResult.end:
3313 case SelectionResult.pending:
3314 case SelectionResult.none:
3315 finalResult = currentSelectableResult;
3316 case SelectionResult.next:
3317 if (forward == false) {
3318 newIndex += 1;
3319 finalResult = SelectionResult.end;
3320 } else if (newIndex == selectables.length - 1) {
3321 finalResult = currentSelectableResult;
3322 } else {
3323 forward = true;
3324 newIndex += 1;
3325 }
3326 case SelectionResult.previous:
3327 if (forward ?? false) {
3328 newIndex -= 1;
3329 finalResult = SelectionResult.end;
3330 } else if (newIndex == 0) {
3331 finalResult = currentSelectableResult;
3332 } else {
3333 forward = false;
3334 newIndex -= 1;
3335 }
3336 }
3337 }
3338 if (isEnd) {
3339 currentSelectionEndIndex = newIndex;
3340 } else {
3341 currentSelectionStartIndex = newIndex;
3342 }
3343 _flushInactiveSelections();
3344 return finalResult!;
3345 }
3346}
3347
3348/// The length of the content that can be selected, and the range that is
3349/// selected.
3350typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range});
3351
3352/// Signature for a widget builder that builds a context menu for the given
3353/// [SelectableRegionState].
3354///
3355/// See also:
3356///
3357/// * [EditableTextContextMenuBuilder], which performs the same role for
3358/// [EditableText].
3359typedef SelectableRegionContextMenuBuilder =
3360 Widget Function(BuildContext context, SelectableRegionState selectableRegionState);
3361
3362/// The status of the selection under a [SelectableRegion].
3363///
3364/// This value can be accessed for a [SelectableRegion] by using
3365/// [SelectableRegionSelectionStatusScope.maybeOf].
3366///
3367/// This value under a [SelectableRegion] is updated frequently
3368/// during selection gestures such as clicks and taps to select
3369/// and keyboard shortcuts.
3370enum SelectableRegionSelectionStatus {
3371 /// Indicates that the selection under a [SelectableRegion] is changing.
3372 ///
3373 /// A [SelectableRegion]s selection is changing when it is being
3374 /// updated by user through selection gestures and keyboard shortcuts.
3375 /// For example, during a text selection drag with a click + drag,
3376 /// a [SelectableRegion]s selection is considered changing until
3377 /// the user releases the click, then it will be considered finalized.
3378 changing,
3379
3380 /// Indicates that the selection under a [SelectableRegion] is finalized.
3381 ///
3382 /// A [SelectableRegion]s selection is finalized when it is no longer
3383 /// being updated by the user through selection gestures or keyboard
3384 /// shortcuts. For example, the selection will be finalized on a mouse
3385 /// drag end, touch long press drag end, a single click to collapse the
3386 /// selection, a double click/tap to select a word, ctrl + A / cmd + A to
3387 /// select all, or a triple click/tap to select a paragraph.
3388 finalized,
3389}
3390
3391/// Notifies its listeners when the [SelectableRegion] that created this object
3392/// is changing or finalizes its selection.
3393///
3394/// To access the [_SelectableRegionSelectionStatusNotifier] from the nearest [SelectableRegion]
3395/// ancestor, use [SelectableRegionSelectionStatusScope.maybeOf].
3396final class _SelectableRegionSelectionStatusNotifier extends ChangeNotifier
3397 implements ValueListenable<SelectableRegionSelectionStatus> {
3398 _SelectableRegionSelectionStatusNotifier._();
3399
3400 SelectableRegionSelectionStatus _selectableRegionSelectionStatus =
3401 SelectableRegionSelectionStatus.finalized;
3402
3403 /// The current value of the [SelectableRegionSelectionStatus] of the [SelectableRegion]
3404 /// that owns this object.
3405 ///
3406 /// Defaults to [SelectableRegionSelectionStatus.finalized].
3407 @override
3408 SelectableRegionSelectionStatus get value => _selectableRegionSelectionStatus;
3409
3410 /// Sets the [SelectableRegionSelectionStatus] for the [SelectableRegion] that
3411 /// owns this object.
3412 ///
3413 /// Listeners are notified even if the value did not change.
3414 @protected
3415 set value(SelectableRegionSelectionStatus newStatus) {
3416 assert(
3417 newStatus == SelectableRegionSelectionStatus.finalized &&
3418 value == SelectableRegionSelectionStatus.changing ||
3419 newStatus == SelectableRegionSelectionStatus.changing,
3420 'Attempting to finalize the selection when it is already finalized.',
3421 );
3422 _selectableRegionSelectionStatus = newStatus;
3423 notifyListeners();
3424 }
3425}
3426
3427/// Notifies its listeners when the selection under a [SelectableRegion] or
3428/// [SelectionArea] is being changed or finalized.
3429///
3430/// Use [SelectableRegionSelectionStatusScope.maybeOf], to access the [ValueListenable] of type
3431/// [SelectableRegionSelectionStatus] under a [SelectableRegion]. Its listeners
3432/// will be called even when the value of the [SelectableRegionSelectionStatus]
3433/// does not change.
3434final class SelectableRegionSelectionStatusScope extends InheritedWidget {
3435 const SelectableRegionSelectionStatusScope._({
3436 required this.selectionStatusNotifier,
3437 required super.child,
3438 });
3439
3440 /// Tracks updates to the [SelectableRegionSelectionStatus] of the owning
3441 /// [SelectableRegion].
3442 ///
3443 /// Listeners will be called even when the value of the [SelectableRegionSelectionStatus]
3444 /// does not change. The selection under the [SelectableRegion] still may have changed.
3445 final ValueListenable<SelectableRegionSelectionStatus> selectionStatusNotifier;
3446
3447 /// The closest instance of this class that encloses the given context.
3448 ///
3449 /// If there is no enclosing [SelectableRegion] or [SelectionArea] widget, then null is
3450 /// returned.
3451 ///
3452 /// Calling this method will create a dependency on the closest
3453 /// [SelectableRegionSelectionStatusScope] in the [context], if there is one.
3454 static ValueListenable<SelectableRegionSelectionStatus>? maybeOf(BuildContext context) {
3455 return context
3456 .dependOnInheritedWidgetOfExactType<SelectableRegionSelectionStatusScope>()
3457 ?.selectionStatusNotifier;
3458 }
3459
3460 @override
3461 bool updateShouldNotify(SelectableRegionSelectionStatusScope oldWidget) {
3462 return selectionStatusNotifier != oldWidget.selectionStatusNotifier;
3463 }
3464}
3465
3466/// A [SelectionContainer] that allows the user to access the [SelectionDetails] and
3467/// listen to selection changes for the child subtree it wraps under a [SelectionArea]
3468/// or [SelectableRegion].
3469///
3470/// The selection updates are provided through the [selectionNotifier], to listen
3471/// to these updates attach a listener through [SelectionListenerNotifier.addListener].
3472///
3473/// This widget does not listen to selection changes of nested [SelectionArea]s
3474/// or [SelectableRegion]s in its subtree because those widgets are self-contained
3475/// and do not bubble up their selection. To listen to selection changes of a
3476/// [SelectionArea] or [SelectableRegion] under this [SelectionListener], add
3477/// an additional [SelectionListener] under each one.
3478///
3479/// {@tool dartpad}
3480/// This example shows how to use [SelectionListener] to access the [SelectionDetails]
3481/// under a [SelectionArea] or [SelectableRegion].
3482///
3483/// ** See code in examples/api/lib/material/selection_area/selection_area.1.dart **
3484/// {@end-tool}
3485///
3486/// {@tool dartpad}
3487/// This example shows how to color the active selection red under a
3488/// [SelectionArea] or [SelectableRegion].
3489///
3490/// ** See code in examples/api/lib/material/selection_area/selection_area.2.dart **
3491/// {@end-tool}
3492///
3493/// See also:
3494///
3495/// * [SelectableRegion], which provides an overview of the selection system.
3496class SelectionListener extends StatefulWidget {
3497 /// Create a new [SelectionListener] widget.
3498 const SelectionListener({super.key, required this.selectionNotifier, required this.child});
3499
3500 /// Notifies listeners when the selection has changed.
3501 final SelectionListenerNotifier selectionNotifier;
3502
3503 /// The child widget this selection listener applies to.
3504 ///
3505 /// {@macro flutter.widgets.ProxyWidget.child}
3506 final Widget child;
3507
3508 @override
3509 State<SelectionListener> createState() => _SelectionListenerState();
3510}
3511
3512class _SelectionListenerState extends State<SelectionListener> {
3513 late final _SelectionListenerDelegate _selectionDelegate = _SelectionListenerDelegate(
3514 selectionNotifier: widget.selectionNotifier,
3515 );
3516
3517 @override
3518 void didUpdateWidget(SelectionListener oldWidget) {
3519 super.didUpdateWidget(oldWidget);
3520 if (oldWidget.selectionNotifier != widget.selectionNotifier) {
3521 _selectionDelegate._setNotifier(widget.selectionNotifier);
3522 }
3523 }
3524
3525 @override
3526 void dispose() {
3527 _selectionDelegate.dispose();
3528 super.dispose();
3529 }
3530
3531 @override
3532 Widget build(BuildContext context) {
3533 return SelectionContainer(delegate: _selectionDelegate, child: widget.child);
3534 }
3535}
3536
3537final class _SelectionListenerDelegate extends StaticSelectionContainerDelegate
3538 implements SelectionDetails {
3539 _SelectionListenerDelegate({required SelectionListenerNotifier selectionNotifier})
3540 : _selectionNotifier = selectionNotifier {
3541 _selectionNotifier._registerSelectionListenerDelegate(this);
3542 }
3543
3544 SelectionGeometry? _initialSelectionGeometry;
3545
3546 SelectionListenerNotifier _selectionNotifier;
3547 void _setNotifier(SelectionListenerNotifier newNotifier) {
3548 _selectionNotifier._unregisterSelectionListenerDelegate();
3549 _selectionNotifier = newNotifier;
3550 _selectionNotifier._registerSelectionListenerDelegate(this);
3551 }
3552
3553 @override
3554 void notifyListeners() {
3555 super.notifyListeners();
3556 // Skip initial notification if selection is not valid.
3557 if (_initialSelectionGeometry == null && !value.hasSelection) {
3558 _initialSelectionGeometry = value;
3559 return;
3560 }
3561 _selectionNotifier.notifyListeners();
3562 }
3563
3564 @override
3565 void dispose() {
3566 _selectionNotifier._unregisterSelectionListenerDelegate();
3567 _initialSelectionGeometry = null;
3568 super.dispose();
3569 }
3570
3571 @override
3572 SelectedContentRange? get range => getSelection();
3573
3574 @override
3575 SelectionStatus get status => value.status;
3576}
3577
3578/// A read-only interface for accessing the details of a selection under a [SelectionListener].
3579///
3580/// This includes information such as the status of the selection indicating
3581/// if it is collapsed or uncollapsed, the [SelectedContentRange] that includes
3582/// the start and end offsets of the selection local to the [SelectionListener]
3583/// that reports this object.
3584///
3585/// This object is typically accessed by providing a [SelectionListenerNotifier]
3586/// to a [SelectionListener] and retrieving the value from [SelectionListenerNotifier.selection].
3587abstract final class SelectionDetails {
3588 /// The computed selection range of the owning [SelectionListener]s subtree.
3589 ///
3590 /// Returns `null` if there is nothing selected.
3591 SelectedContentRange? get range;
3592
3593 /// The status that indicates whether there is a selection and whether the selection is collapsed.
3594 SelectionStatus get status;
3595}
3596
3597/// Notifies listeners when the selection under a [SelectionListener] has been
3598/// changed.
3599///
3600/// This object is typically provided to a [SelectionListener].
3601final class SelectionListenerNotifier extends ChangeNotifier {
3602 _SelectionListenerDelegate? _selectionDelegate;
3603
3604 /// The details of the selection under the [SelectionListener] that owns this notifier.
3605 ///
3606 /// Throws an exception if this notifier has not been registered to a [SelectionListener].
3607 /// To check if a notifier has been registered to a [SelectionListener] use [registered].
3608 SelectionDetails get selection =>
3609 _selectionDelegate ??
3610 (throw Exception('Selection client has not been registered to this notifier.'));
3611
3612 /// Whether this [SelectionListenerNotifier] has been registered to a [SelectionListener].
3613 bool get registered => _selectionDelegate != null;
3614
3615 void _registerSelectionListenerDelegate(_SelectionListenerDelegate selectionDelegate) {
3616 assert(
3617 !registered,
3618 'This SelectionListenerNotifier is already registered to another SelectionListener. Try providing a new SelectionListenerNotifier.',
3619 );
3620 _selectionDelegate = selectionDelegate;
3621 }
3622
3623 void _unregisterSelectionListenerDelegate() {
3624 _selectionDelegate = null;
3625 }
3626
3627 // From ChangeNotifier.
3628 @override
3629 void dispose() {
3630 _unregisterSelectionListenerDelegate();
3631 super.dispose();
3632 }
3633
3634 /// Calls the listener every time the [SelectionGeometry] of the selection changes under
3635 /// a [SelectionListener].
3636 ///
3637 /// Listeners can be removed with [removeListener].
3638 @override
3639 void addListener(VoidCallback listener) {
3640 super.addListener(listener);
3641 }
3642}
3643

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com