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'; |
10 | library; |
11 | |
12 | import 'dart:async'; |
13 | import 'dart:math'; |
14 | |
15 | import 'package:flutter/foundation.dart'; |
16 | import 'package:flutter/gestures.dart'; |
17 | import 'package:flutter/rendering.dart'; |
18 | import 'package:flutter/scheduler.dart'; |
19 | import 'package:flutter/services.dart'; |
20 | import 'package:vector_math/vector_math_64.dart'; |
21 | |
22 | import 'actions.dart'; |
23 | import 'basic.dart'; |
24 | import 'context_menu_button_item.dart'; |
25 | import 'debug.dart'; |
26 | import 'focus_manager.dart'; |
27 | import 'focus_scope.dart'; |
28 | import 'framework.dart'; |
29 | import 'gesture_detector.dart'; |
30 | import 'magnifier.dart'; |
31 | import 'media_query.dart'; |
32 | import 'overlay.dart'; |
33 | import 'platform_selectable_region_context_menu.dart'; |
34 | import 'selection_container.dart'; |
35 | import 'text_editing_intents.dart'; |
36 | import 'text_selection.dart'; |
37 | import 'text_selection_toolbar_anchors.dart'; |
38 | |
39 | // Examples can assume: |
40 | // late GlobalKey key; |
41 | |
42 | const 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. |
51 | const 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. |
235 | class 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]. |
341 | class 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]. |
1966 | abstract 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 | |
1978 | class _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 | |
1989 | class _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 | |
2000 | class _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 | |
2013 | class _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 | |
2030 | class _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]. |
2068 | class 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. |
2337 | abstract 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. |
3350 | typedef _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]. |
3359 | typedef 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. |
3370 | enum 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]. |
3396 | final 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. |
3434 | final 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. |
3496 | class 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 | |
3512 | class _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 | |
3537 | final 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]. |
3587 | abstract 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]. |
3601 | final 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 |
Definitions
- _kLongPressSelectionDevices
- _kSelectableVerticalComparingThreshold
- SelectableRegion
- SelectableRegion
- getSelectableButtonItems
- createState
- SelectableRegionState
- _hasSelectionOverlayGeometry
- selectionOverlay
- _focusNode
- initState
- _initProcessTextActions
- didChangeDependencies
- didUpdateWidget
- _makeOverridable
- _handleFocusChanged
- _updateSelectionStatus
- _isPrecisePointerDevice
- _finalizeSelectableRegionStatus
- _getEffectiveConsecutiveTapCount
- _initMouseGestureRecognizer
- _onTapTrackStart
- _onTapTrackReset
- _initTouchGestureRecognizer
- _startNewMouseSelectionGesture
- _handleMouseDragStart
- _handleMouseDragUpdate
- _handleMouseDragEnd
- _handleMouseTapUp
- _updateSelectedContentIfNeeded
- _handleTouchLongPressStart
- _handleTouchLongPressMoveUpdate
- _handleTouchLongPressEnd
- _positionIsOnActiveSelection
- _handleRightClickDown
- _userDraggingSelectionEnd
- _triggerSelectionEndEdgeUpdate
- _onAnyDragEnd
- _stopSelectionEndEdgeUpdate
- _userDraggingSelectionStart
- _triggerSelectionStartEdgeUpdate
- _stopSelectionStartEdgeUpdate
- _handleSelectionStartHandleDragStart
- _handleSelectionStartHandleDragUpdate
- _handleSelectionEndHandleDragStart
- _handleSelectionEndHandleDragUpdate
- _buildInfoForMagnifier
- _createSelectionOverlay
- _updateSelectionOverlay
- _showHandles
- _showToolbar
- _selectEndTo
- _selectStartTo
- _collapseSelectionAt
- _selectWordAt
- _selectParagraphAt
- _finalizeSelection
- clearSelection
- _copy
- _share
- contextMenuAnchors
- _determineIsAdjustingSelectionEnd
- _granularlyExtendSelection
- _directionallyExtendSelection
- contextMenuButtonItems
- _textProcessingActionButtonItems
- startGlyphHeight
- endGlyphHeight
- selectionEndpoints
- cutEnabled
- pasteEnabled
- hideToolbar
- selectAll
- copySelection
- bringIntoView
- cutSelection
- userUpdateTextEditingValue
- pasteText
- add
- remove
- dispose
- build
- _NonOverrideAction
- invokeAction
- invoke
- _SelectAllAction
- _SelectAllAction
- invokeAction
- _CopySelectionAction
- _CopySelectionAction
- invokeAction
- _GranularlyExtendSelectionAction
- _GranularlyExtendSelectionAction
- invokeAction
- _GranularlyExtendCaretSelectionAction
- _GranularlyExtendCaretSelectionAction
- invokeAction
- _DirectionallyExtendCaretSelectionAction
- _DirectionallyExtendCaretSelectionAction
- invokeAction
- StaticSelectionContainerDelegate
- didReceiveSelectionEventFor
- didReceiveSelectionBoundaryEvents
- updateLastSelectionEdgeLocation
- _updateLastSelectionEdgeLocationsFromGeometries
- clearInternalSelectionState
- clearInternalSelectionStateForSelectable
- remove
- handleSelectAll
- handleSelectWord
- handleSelectParagraph
- handleClearSelection
- handleSelectionEdgeUpdate
- dispose
- dispatchSelectionEventToChild
- ensureChildUpdated
- didChangeSelectables
- MultiSelectableSelectionContainerDelegate
- MultiSelectableSelectionContainerDelegate
- add
- remove
- layoutDidChange
- _scheduleSelectableUpdate
- runScheduledTask
- _updateSelectables
- _flushAdditions
- _removeSelectable
- didChangeSelectables
- value
- _updateSelectionGeometry
- _getBoundingBox
- compareOrder
- _compareScreenOrder
- _compareVertically
- _compareHorizontally
- _handleSelectableGeometryChange
- getSelectionGeometry
- _adjustSelectionIndexBasedOnSelectionGeometry
- pushHandleLayers
- _updateHandleLayersAndOwners
- getSelectedContent
- contentLength
- _calculateLocalRange
- getSelection
- _flushInactiveSelections
- handleSelectAll
- _handleSelectBoundary
- handleSelectWord
- handleSelectParagraph
- handleClearSelection
- handleGranularlyExtendSelection
- handleDirectionallyExtendSelection
- handleSelectionEdgeUpdate
- dispatchSelectionEvent
- dispose
- ensureChildUpdated
- dispatchSelectionEventToChild
- _initSelection
- _adjustSelection
- SelectableRegionSelectionStatus
- _SelectableRegionSelectionStatusNotifier
- _
- value
- value
- SelectableRegionSelectionStatusScope
- _
- maybeOf
- updateShouldNotify
- SelectionListener
- SelectionListener
- createState
- _SelectionListenerState
- didUpdateWidget
- dispose
- build
- _SelectionListenerDelegate
- _SelectionListenerDelegate
- _setNotifier
- notifyListeners
- dispose
- range
- status
- SelectionDetails
- range
- status
- SelectionListenerNotifier
- selection
- registered
- _registerSelectionListenerDelegate
- _unregisterSelectionListenerDelegate
- dispose
Learn more about Flutter for embedded and desktop on industrialflutter.com