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 | import 'dart:async'; |
6 | import 'dart:math'; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/gestures.dart'; |
10 | import 'package:flutter/rendering.dart'; |
11 | import 'package:flutter/scheduler.dart'; |
12 | import 'package:flutter/services.dart'; |
13 | import 'package:vector_math/vector_math_64.dart' ; |
14 | |
15 | import 'actions.dart'; |
16 | import 'basic.dart'; |
17 | import 'context_menu_button_item.dart'; |
18 | import 'debug.dart'; |
19 | import 'focus_manager.dart'; |
20 | import 'focus_scope.dart'; |
21 | import 'framework.dart'; |
22 | import 'gesture_detector.dart'; |
23 | import 'magnifier.dart'; |
24 | import 'media_query.dart'; |
25 | import 'overlay.dart'; |
26 | import 'platform_selectable_region_context_menu.dart'; |
27 | import 'selection_container.dart'; |
28 | import 'text_editing_intents.dart'; |
29 | import 'text_selection.dart'; |
30 | import 'text_selection_toolbar_anchors.dart'; |
31 | |
32 | // Examples can assume: |
33 | // FocusNode _focusNode = FocusNode(); |
34 | // late GlobalKey key; |
35 | |
36 | const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{ |
37 | PointerDeviceKind.touch, |
38 | PointerDeviceKind.stylus, |
39 | PointerDeviceKind.invertedStylus, |
40 | }; |
41 | |
42 | // In practice some selectables like widgetspan shift several pixels. So when |
43 | // the vertical position diff is within the threshold, compare the horizontal |
44 | // position to make the compareScreenOrder function more robust. |
45 | const double _kSelectableVerticalComparingThreshold = 3.0; |
46 | |
47 | /// A widget that introduces an area for user selections. |
48 | /// |
49 | /// Flutter widgets are not selectable by default. Wrapping a widget subtree |
50 | /// with a [SelectableRegion] widget enables selection within that subtree (for |
51 | /// example, [Text] widgets automatically look for selectable regions to enable |
52 | /// selection). The wrapped subtree can be selected by users using mouse or |
53 | /// touch gestures, e.g. users can select widgets by holding the mouse |
54 | /// left-click and dragging across widgets, or they can use long press gestures |
55 | /// to select words on touch devices. |
56 | /// |
57 | /// A [SelectableRegion] widget requires configuration; in particular specific |
58 | /// [selectionControls] must be provided. |
59 | /// |
60 | /// The [SelectionArea] widget from the [material] library configures a |
61 | /// [SelectableRegion] in a platform-specific manner (e.g. using a Material |
62 | /// toolbar on Android, a Cupertino toolbar on iOS), and it may therefore be |
63 | /// simpler to use that widget rather than using [SelectableRegion] directly. |
64 | /// |
65 | /// ## An overview of the selection system. |
66 | /// |
67 | /// Every [Selectable] under the [SelectableRegion] can be selected. They form a |
68 | /// selection tree structure to handle the selection. |
69 | /// |
70 | /// The [SelectableRegion] is a wrapper over [SelectionContainer]. It listens to |
71 | /// user gestures and sends corresponding [SelectionEvent]s to the |
72 | /// [SelectionContainer] it creates. |
73 | /// |
74 | /// A [SelectionContainer] is a single [Selectable] that handles |
75 | /// [SelectionEvent]s on behalf of child [Selectable]s in the subtree. It |
76 | /// creates a [SelectionRegistrarScope] with its [SelectionContainer.delegate] |
77 | /// to collect child [Selectable]s and sends the [SelectionEvent]s it receives |
78 | /// from the parent [SelectionRegistrar] to the appropriate child [Selectable]s. |
79 | /// It creates an abstraction for the parent [SelectionRegistrar] as if it is |
80 | /// interacting with a single [Selectable]. |
81 | /// |
82 | /// The [SelectionContainer] created by [SelectableRegion] is the root node of a |
83 | /// selection tree. Each non-leaf node in the tree is a [SelectionContainer], |
84 | /// and the leaf node is a leaf widget whose render object implements |
85 | /// [Selectable]. They are connected through [SelectionRegistrarScope]s created |
86 | /// by [SelectionContainer]s. |
87 | /// |
88 | /// Both [SelectionContainer]s and the leaf [Selectable]s need to register |
89 | /// themselves to the [SelectionRegistrar] from the |
90 | /// [SelectionContainer.maybeOf] if they want to participate in the |
91 | /// selection. |
92 | /// |
93 | /// An example selection tree will look like: |
94 | /// |
95 | /// {@tool snippet} |
96 | /// |
97 | /// ```dart |
98 | /// MaterialApp( |
99 | /// home: SelectableRegion( |
100 | /// selectionControls: materialTextSelectionControls, |
101 | /// focusNode: _focusNode, // initialized to FocusNode() |
102 | /// child: Scaffold( |
103 | /// appBar: AppBar(title: const Text('Flutter Code Sample')), |
104 | /// body: ListView( |
105 | /// children: const <Widget>[ |
106 | /// Text('Item 0', style: TextStyle(fontSize: 50.0)), |
107 | /// Text('Item 1', style: TextStyle(fontSize: 50.0)), |
108 | /// ], |
109 | /// ), |
110 | /// ), |
111 | /// ), |
112 | /// ) |
113 | /// ``` |
114 | /// {@end-tool} |
115 | /// |
116 | /// |
117 | /// SelectionContainer |
118 | /// (SelectableRegion) |
119 | /// / \ |
120 | /// / \ |
121 | /// / \ |
122 | /// Selectable \ |
123 | /// ("Flutter Code Sample") \ |
124 | /// \ |
125 | /// SelectionContainer |
126 | /// (ListView) |
127 | /// / \ |
128 | /// / \ |
129 | /// / \ |
130 | /// Selectable Selectable |
131 | /// ("Item 0") ("Item 1") |
132 | /// |
133 | /// |
134 | /// ## Making a widget selectable |
135 | /// |
136 | /// Some leaf widgets, such as [Text], have all of the selection logic wired up |
137 | /// automatically and can be selected as long as they are under a |
138 | /// [SelectableRegion]. |
139 | /// |
140 | /// To make a custom selectable widget, its render object needs to mix in |
141 | /// [Selectable] and implement the required APIs to handle [SelectionEvent]s |
142 | /// as well as paint appropriate selection highlights. |
143 | /// |
144 | /// The render object also needs to register itself to a [SelectionRegistrar]. |
145 | /// For the most cases, one can use [SelectionRegistrant] to auto-register |
146 | /// itself with the register returned from [SelectionContainer.maybeOf] as |
147 | /// seen in the example below. |
148 | /// |
149 | /// {@tool dartpad} |
150 | /// This sample demonstrates how to create an adapter widget that makes any |
151 | /// child widget selectable. |
152 | /// |
153 | /// ** See code in examples/api/lib/material/selectable_region/selectable_region.0.dart ** |
154 | /// {@end-tool} |
155 | /// |
156 | /// ## Complex layout |
157 | /// |
158 | /// By default, the screen order is used as the selection order. If a group of |
159 | /// [Selectable]s needs to select differently, consider wrapping them with a |
160 | /// [SelectionContainer] to customize its selection behavior. |
161 | /// |
162 | /// {@tool dartpad} |
163 | /// This sample demonstrates how to create a [SelectionContainer] that only |
164 | /// allows selecting everything or nothing with no partial selection. |
165 | /// |
166 | /// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart ** |
167 | /// {@end-tool} |
168 | /// |
169 | /// In the case where a group of widgets should be excluded from selection under |
170 | /// a [SelectableRegion], consider wrapping that group of widgets using |
171 | /// [SelectionContainer.disabled]. |
172 | /// |
173 | /// {@tool dartpad} |
174 | /// This sample demonstrates how to disable selection for a Text in a Column. |
175 | /// |
176 | /// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** |
177 | /// {@end-tool} |
178 | /// |
179 | /// To create a separate selection system from its parent selection area, |
180 | /// wrap part of the subtree with another [SelectableRegion]. The selection of the |
181 | /// child selection area can not extend past its subtree, and the selection of |
182 | /// the parent selection area can not extend inside the child selection area. |
183 | /// |
184 | /// ## Tests |
185 | /// |
186 | /// In a test, a region can be selected either by faking drag events (e.g. using |
187 | /// [WidgetTester.dragFrom]) or by sending intents to a widget inside the region |
188 | /// that has been given a [GlobalKey], e.g.: |
189 | /// |
190 | /// ```dart |
191 | /// Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard)); |
192 | /// ``` |
193 | /// |
194 | /// See also: |
195 | /// * [SelectionArea], which creates a [SelectableRegion] with |
196 | /// platform-adaptive selection controls. |
197 | /// * [SelectionHandler], which contains APIs to handle selection events from the |
198 | /// [SelectableRegion]. |
199 | /// * [Selectable], which provides API to participate in the selection system. |
200 | /// * [SelectionRegistrar], which [Selectable] needs to subscribe to receive |
201 | /// selection events. |
202 | /// * [SelectionContainer], which collects selectable widgets in the subtree |
203 | /// and provides api to dispatch selection event to the collected widget. |
204 | class SelectableRegion extends StatefulWidget { |
205 | /// Create a new [SelectableRegion] widget. |
206 | /// |
207 | /// The [selectionControls] are used for building the selection handles and |
208 | /// toolbar for mobile devices. |
209 | const SelectableRegion({ |
210 | super.key, |
211 | this.contextMenuBuilder, |
212 | required this.focusNode, |
213 | required this.selectionControls, |
214 | required this.child, |
215 | this.magnifierConfiguration = TextMagnifierConfiguration.disabled, |
216 | this.onSelectionChanged, |
217 | }); |
218 | |
219 | /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro} |
220 | /// |
221 | /// {@macro flutter.widgets.magnifier.intro} |
222 | /// |
223 | /// By default, [SelectableRegion]'s [TextMagnifierConfiguration] is disabled. |
224 | /// |
225 | /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details} |
226 | final TextMagnifierConfiguration magnifierConfiguration; |
227 | |
228 | /// {@macro flutter.widgets.Focus.focusNode} |
229 | final FocusNode focusNode; |
230 | |
231 | /// The child widget this selection area applies to. |
232 | /// |
233 | /// {@macro flutter.widgets.ProxyWidget.child} |
234 | final Widget child; |
235 | |
236 | /// {@macro flutter.widgets.EditableText.contextMenuBuilder} |
237 | final SelectableRegionContextMenuBuilder? contextMenuBuilder; |
238 | |
239 | /// The delegate to build the selection handles and toolbar for mobile |
240 | /// devices. |
241 | /// |
242 | /// The [emptyTextSelectionControls] global variable provides a default |
243 | /// [TextSelectionControls] implementation with no controls. |
244 | final TextSelectionControls selectionControls; |
245 | |
246 | /// Called when the selected content changes. |
247 | final ValueChanged<SelectedContent?>? onSelectionChanged; |
248 | |
249 | /// Returns the [ContextMenuButtonItem]s representing the buttons in this |
250 | /// platform's default selection menu. |
251 | /// |
252 | /// For example, [SelectableRegion] uses this to generate the default buttons |
253 | /// for its context menu. |
254 | /// |
255 | /// See also: |
256 | /// |
257 | /// * [SelectableRegionState.contextMenuButtonItems], which gives the |
258 | /// [ContextMenuButtonItem]s for a specific SelectableRegion. |
259 | /// * [EditableText.getEditableButtonItems], which performs a similar role but |
260 | /// for content that is both selectable and editable. |
261 | /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can |
262 | /// take a list of [ContextMenuButtonItem]s with |
263 | /// [AdaptiveTextSelectionToolbar.buttonItems]. |
264 | /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button |
265 | /// Widgets for the current platform given [ContextMenuButtonItem]s. |
266 | static List<ContextMenuButtonItem> getSelectableButtonItems({ |
267 | required final SelectionGeometry selectionGeometry, |
268 | required final VoidCallback onCopy, |
269 | required final VoidCallback onSelectAll, |
270 | }) { |
271 | final bool canCopy = selectionGeometry.status == SelectionStatus.uncollapsed; |
272 | final bool canSelectAll = selectionGeometry.hasContent; |
273 | |
274 | // Determine which buttons will appear so that the order and total number is |
275 | // known. A button's position in the menu can slightly affect its |
276 | // appearance. |
277 | return <ContextMenuButtonItem>[ |
278 | if (canCopy) |
279 | ContextMenuButtonItem( |
280 | onPressed: onCopy, |
281 | type: ContextMenuButtonType.copy, |
282 | ), |
283 | if (canSelectAll) |
284 | ContextMenuButtonItem( |
285 | onPressed: onSelectAll, |
286 | type: ContextMenuButtonType.selectAll, |
287 | ), |
288 | ]; |
289 | } |
290 | |
291 | @override |
292 | State<StatefulWidget> createState() => SelectableRegionState(); |
293 | } |
294 | |
295 | /// State for a [SelectableRegion]. |
296 | class SelectableRegionState extends State<SelectableRegion> with TextSelectionDelegate implements SelectionRegistrar { |
297 | late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{ |
298 | SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), |
299 | CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), |
300 | ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(this, granularity: TextGranularity.word)), |
301 | ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExpandSelectionToDocumentBoundaryIntent>(this, granularity: TextGranularity.document)), |
302 | ExpandSelectionToLineBreakIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExpandSelectionToLineBreakIntent>(this, granularity: TextGranularity.line)), |
303 | ExtendSelectionByCharacterIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionByCharacterIntent>(this, granularity: TextGranularity.character)), |
304 | ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, granularity: TextGranularity.word)), |
305 | ExtendSelectionToLineBreakIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToLineBreakIntent>(this, granularity: TextGranularity.line)), |
306 | ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_DirectionallyExtendCaretSelectionAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this)), |
307 | ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, granularity: TextGranularity.document)), |
308 | }; |
309 | |
310 | final Map<Type, GestureRecognizerFactory> _gestureRecognizers = <Type, GestureRecognizerFactory>{}; |
311 | SelectionOverlay? _selectionOverlay; |
312 | final LayerLink _startHandleLayerLink = LayerLink(); |
313 | final LayerLink _endHandleLayerLink = LayerLink(); |
314 | final LayerLink _toolbarLayerLink = LayerLink(); |
315 | final _SelectableRegionContainerDelegate _selectionDelegate = _SelectableRegionContainerDelegate(); |
316 | // there should only ever be one selectable, which is the SelectionContainer. |
317 | Selectable? _selectable; |
318 | |
319 | bool get _hasSelectionOverlayGeometry => _selectionDelegate.value.startSelectionPoint != null |
320 | || _selectionDelegate.value.endSelectionPoint != null; |
321 | |
322 | Orientation? _lastOrientation; |
323 | SelectedContent? _lastSelectedContent; |
324 | |
325 | /// {@macro flutter.rendering.RenderEditable.lastSecondaryTapDownPosition} |
326 | Offset? lastSecondaryTapDownPosition; |
327 | |
328 | /// The [SelectionOverlay] that is currently visible on the screen. |
329 | /// |
330 | /// Can be null if there is no visible [SelectionOverlay]. |
331 | @visibleForTesting |
332 | SelectionOverlay? get selectionOverlay => _selectionOverlay; |
333 | |
334 | @override |
335 | void initState() { |
336 | super.initState(); |
337 | widget.focusNode.addListener(_handleFocusChanged); |
338 | _initMouseGestureRecognizer(); |
339 | _initTouchGestureRecognizer(); |
340 | // Taps and right clicks. |
341 | _gestureRecognizers[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( |
342 | () => TapGestureRecognizer(debugOwner: this), |
343 | (TapGestureRecognizer instance) { |
344 | instance.onTapUp = (TapUpDetails details) { |
345 | if (defaultTargetPlatform == TargetPlatform.iOS && _positionIsOnActiveSelection(globalPosition: details.globalPosition)) { |
346 | // On iOS when the tap occurs on the previous selection, instead of |
347 | // moving the selection, the context menu will be toggled. |
348 | final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false; |
349 | if (toolbarIsVisible) { |
350 | hideToolbar(false); |
351 | } else { |
352 | _showToolbar(location: details.globalPosition); |
353 | } |
354 | } else { |
355 | hideToolbar(); |
356 | _collapseSelectionAt(offset: details.globalPosition); |
357 | } |
358 | }; |
359 | instance.onSecondaryTapDown = _handleRightClickDown; |
360 | }, |
361 | ); |
362 | } |
363 | |
364 | @override |
365 | void didChangeDependencies() { |
366 | super.didChangeDependencies(); |
367 | switch (defaultTargetPlatform) { |
368 | case TargetPlatform.android: |
369 | case TargetPlatform.iOS: |
370 | break; |
371 | case TargetPlatform.fuchsia: |
372 | case TargetPlatform.linux: |
373 | case TargetPlatform.macOS: |
374 | case TargetPlatform.windows: |
375 | return; |
376 | } |
377 | |
378 | // Hide the text selection toolbar on mobile when orientation changes. |
379 | final Orientation orientation = MediaQuery.orientationOf(context); |
380 | if (_lastOrientation == null) { |
381 | _lastOrientation = orientation; |
382 | return; |
383 | } |
384 | if (orientation != _lastOrientation) { |
385 | _lastOrientation = orientation; |
386 | hideToolbar(defaultTargetPlatform == TargetPlatform.android); |
387 | } |
388 | } |
389 | |
390 | @override |
391 | void didUpdateWidget(SelectableRegion oldWidget) { |
392 | super.didUpdateWidget(oldWidget); |
393 | if (widget.focusNode != oldWidget.focusNode) { |
394 | oldWidget.focusNode.removeListener(_handleFocusChanged); |
395 | widget.focusNode.addListener(_handleFocusChanged); |
396 | if (widget.focusNode.hasFocus != oldWidget.focusNode.hasFocus) { |
397 | _handleFocusChanged(); |
398 | } |
399 | } |
400 | } |
401 | |
402 | Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) { |
403 | return Action<T>.overridable(context: context, defaultAction: defaultAction); |
404 | } |
405 | |
406 | void _handleFocusChanged() { |
407 | if (!widget.focusNode.hasFocus) { |
408 | if (kIsWeb) { |
409 | PlatformSelectableRegionContextMenu.detach(_selectionDelegate); |
410 | } |
411 | _clearSelection(); |
412 | } |
413 | if (kIsWeb) { |
414 | PlatformSelectableRegionContextMenu.attach(_selectionDelegate); |
415 | } |
416 | } |
417 | |
418 | void _updateSelectionStatus() { |
419 | final TextSelection selection; |
420 | final SelectionGeometry geometry = _selectionDelegate.value; |
421 | switch (geometry.status) { |
422 | case SelectionStatus.uncollapsed: |
423 | case SelectionStatus.collapsed: |
424 | selection = const TextSelection(baseOffset: 0, extentOffset: 1); |
425 | case SelectionStatus.none: |
426 | selection = const TextSelection.collapsed(offset: 1); |
427 | } |
428 | textEditingValue = TextEditingValue(text: '__' , selection: selection); |
429 | if (_hasSelectionOverlayGeometry) { |
430 | _updateSelectionOverlay(); |
431 | } else { |
432 | _selectionOverlay?.dispose(); |
433 | _selectionOverlay = null; |
434 | } |
435 | } |
436 | |
437 | // gestures. |
438 | |
439 | // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, |
440 | // which can grow to be infinitely large, to a value between 1 and the supported |
441 | // max consecutive tap count. The value that the raw count is converted to is |
442 | // based on the default observed behavior on the native platforms. |
443 | // |
444 | // This method should be used in all instances when details.consecutiveTapCount |
445 | // would be used. |
446 | static int _getEffectiveConsecutiveTapCount(int rawCount) { |
447 | const int maxConsecutiveTap = 2; |
448 | switch (defaultTargetPlatform) { |
449 | case TargetPlatform.android: |
450 | case TargetPlatform.fuchsia: |
451 | case TargetPlatform.linux: |
452 | // From observation, these platforms reset their tap count to 0 when |
453 | // the number of consecutive taps exceeds the max consecutive tap supported. |
454 | // For example on Debian Linux with GTK, when going past a triple click, |
455 | // on the fourth click the selection is moved to the precise click |
456 | // position, on the fifth click the word at the position is selected, and |
457 | // on the sixth click the paragraph at the position is selected. |
458 | return rawCount <= maxConsecutiveTap ? rawCount : (rawCount % maxConsecutiveTap == 0 ? maxConsecutiveTap : rawCount % maxConsecutiveTap); |
459 | case TargetPlatform.iOS: |
460 | case TargetPlatform.macOS: |
461 | case TargetPlatform.windows: |
462 | // From observation, these platforms either hold their tap count at the max |
463 | // consecutive tap supported. For example on macOS, when going past a triple |
464 | // click, the selection should be retained at the paragraph that was first |
465 | // selected on triple click. |
466 | return min(rawCount, maxConsecutiveTap); |
467 | } |
468 | } |
469 | |
470 | void _initMouseGestureRecognizer() { |
471 | _gestureRecognizers[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>( |
472 | () => TapAndPanGestureRecognizer(debugOwner:this, supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.mouse }), |
473 | (TapAndPanGestureRecognizer instance) { |
474 | instance |
475 | ..onTapDown = _startNewMouseSelectionGesture |
476 | ..onTapUp = _handleMouseTapUp |
477 | ..onDragStart = _handleMouseDragStart |
478 | ..onDragUpdate = _handleMouseDragUpdate |
479 | ..onDragEnd = _handleMouseDragEnd |
480 | ..onCancel = _clearSelection |
481 | ..dragStartBehavior = DragStartBehavior.down; |
482 | }, |
483 | ); |
484 | } |
485 | |
486 | void _initTouchGestureRecognizer() { |
487 | _gestureRecognizers[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>( |
488 | () => LongPressGestureRecognizer(debugOwner: this, supportedDevices: _kLongPressSelectionDevices), |
489 | (LongPressGestureRecognizer instance) { |
490 | instance |
491 | ..onLongPressStart = _handleTouchLongPressStart |
492 | ..onLongPressMoveUpdate = _handleTouchLongPressMoveUpdate |
493 | ..onLongPressEnd = _handleTouchLongPressEnd; |
494 | }, |
495 | ); |
496 | } |
497 | |
498 | void _startNewMouseSelectionGesture(TapDragDownDetails details) { |
499 | switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { |
500 | case 1: |
501 | widget.focusNode.requestFocus(); |
502 | hideToolbar(); |
503 | switch (defaultTargetPlatform) { |
504 | case TargetPlatform.android: |
505 | case TargetPlatform.fuchsia: |
506 | case TargetPlatform.iOS: |
507 | // On mobile platforms the selection is set on tap up. |
508 | break; |
509 | case TargetPlatform.macOS: |
510 | case TargetPlatform.linux: |
511 | case TargetPlatform.windows: |
512 | _collapseSelectionAt(offset: details.globalPosition); |
513 | } |
514 | case 2: |
515 | _selectWordAt(offset: details.globalPosition); |
516 | } |
517 | _updateSelectedContentIfNeeded(); |
518 | } |
519 | |
520 | void _handleMouseDragStart(TapDragStartDetails details) { |
521 | switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { |
522 | case 1: |
523 | _selectStartTo(offset: details.globalPosition); |
524 | } |
525 | _updateSelectedContentIfNeeded(); |
526 | } |
527 | |
528 | void _handleMouseDragUpdate(TapDragUpdateDetails details) { |
529 | switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { |
530 | case 1: |
531 | _selectEndTo(offset: details.globalPosition, continuous: true); |
532 | case 2: |
533 | _selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word); |
534 | } |
535 | _updateSelectedContentIfNeeded(); |
536 | } |
537 | |
538 | void _handleMouseDragEnd(TapDragEndDetails details) { |
539 | _finalizeSelection(); |
540 | _updateSelectedContentIfNeeded(); |
541 | } |
542 | |
543 | void _handleMouseTapUp(TapDragUpDetails details) { |
544 | switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { |
545 | case 1: |
546 | switch (defaultTargetPlatform) { |
547 | case TargetPlatform.android: |
548 | case TargetPlatform.fuchsia: |
549 | case TargetPlatform.iOS: |
550 | _collapseSelectionAt(offset: details.globalPosition); |
551 | case TargetPlatform.macOS: |
552 | case TargetPlatform.linux: |
553 | case TargetPlatform.windows: |
554 | // On desktop platforms the selection is set on tap down. |
555 | break; |
556 | } |
557 | } |
558 | _updateSelectedContentIfNeeded(); |
559 | } |
560 | |
561 | void _updateSelectedContentIfNeeded() { |
562 | if (_lastSelectedContent?.plainText != _selectable?.getSelectedContent()?.plainText) { |
563 | _lastSelectedContent = _selectable?.getSelectedContent(); |
564 | widget.onSelectionChanged?.call(_lastSelectedContent); |
565 | } |
566 | } |
567 | |
568 | void _handleTouchLongPressStart(LongPressStartDetails details) { |
569 | HapticFeedback.selectionClick(); |
570 | widget.focusNode.requestFocus(); |
571 | _selectWordAt(offset: details.globalPosition); |
572 | // Platforms besides Android will show the text selection handles when |
573 | // the long press is initiated. Android shows the text selection handles when |
574 | // the long press has ended, usually after a pointer up event is received. |
575 | if (defaultTargetPlatform != TargetPlatform.android) { |
576 | _showHandles(); |
577 | } |
578 | _updateSelectedContentIfNeeded(); |
579 | } |
580 | |
581 | void _handleTouchLongPressMoveUpdate(LongPressMoveUpdateDetails details) { |
582 | _selectEndTo(offset: details.globalPosition, textGranularity: TextGranularity.word); |
583 | _updateSelectedContentIfNeeded(); |
584 | } |
585 | |
586 | void _handleTouchLongPressEnd(LongPressEndDetails details) { |
587 | _finalizeSelection(); |
588 | _updateSelectedContentIfNeeded(); |
589 | _showToolbar(); |
590 | if (defaultTargetPlatform == TargetPlatform.android) { |
591 | _showHandles(); |
592 | } |
593 | } |
594 | |
595 | bool _positionIsOnActiveSelection({required Offset globalPosition}) { |
596 | for (final Rect selectionRect in _selectionDelegate.value.selectionRects) { |
597 | final Matrix4 transform = _selectable!.getTransformTo(null); |
598 | final Rect globalRect = MatrixUtils.transformRect(transform, selectionRect); |
599 | if (globalRect.contains(globalPosition)) { |
600 | return true; |
601 | } |
602 | } |
603 | return false; |
604 | } |
605 | |
606 | void _handleRightClickDown(TapDownDetails details) { |
607 | final Offset? previousSecondaryTapDownPosition = lastSecondaryTapDownPosition; |
608 | final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false; |
609 | lastSecondaryTapDownPosition = details.globalPosition; |
610 | widget.focusNode.requestFocus(); |
611 | switch (defaultTargetPlatform) { |
612 | case TargetPlatform.android: |
613 | case TargetPlatform.fuchsia: |
614 | case TargetPlatform.windows: |
615 | // If lastSecondaryTapDownPosition is within the current selection then |
616 | // keep the current selection, if not then collapse it. |
617 | final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition); |
618 | if (!lastSecondaryTapDownPositionWasOnActiveSelection) { |
619 | _collapseSelectionAt(offset: lastSecondaryTapDownPosition!); |
620 | } |
621 | _showHandles(); |
622 | _showToolbar(location: lastSecondaryTapDownPosition); |
623 | case TargetPlatform.iOS: |
624 | _selectWordAt(offset: lastSecondaryTapDownPosition!); |
625 | _showHandles(); |
626 | _showToolbar(location: lastSecondaryTapDownPosition); |
627 | case TargetPlatform.macOS: |
628 | if (previousSecondaryTapDownPosition == lastSecondaryTapDownPosition && toolbarIsVisible) { |
629 | hideToolbar(); |
630 | return; |
631 | } |
632 | _selectWordAt(offset: lastSecondaryTapDownPosition!); |
633 | _showHandles(); |
634 | _showToolbar(location: lastSecondaryTapDownPosition); |
635 | case TargetPlatform.linux: |
636 | if (toolbarIsVisible) { |
637 | hideToolbar(); |
638 | return; |
639 | } |
640 | // If lastSecondaryTapDownPosition is within the current selection then |
641 | // keep the current selection, if not then collapse it. |
642 | final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition); |
643 | if (!lastSecondaryTapDownPositionWasOnActiveSelection) { |
644 | _collapseSelectionAt(offset: lastSecondaryTapDownPosition!); |
645 | } |
646 | _showHandles(); |
647 | _showToolbar(location: lastSecondaryTapDownPosition); |
648 | } |
649 | _updateSelectedContentIfNeeded(); |
650 | } |
651 | |
652 | // Selection update helper methods. |
653 | |
654 | Offset? _selectionEndPosition; |
655 | bool get _userDraggingSelectionEnd => _selectionEndPosition != null; |
656 | bool _scheduledSelectionEndEdgeUpdate = false; |
657 | |
658 | /// Sends end [SelectionEdgeUpdateEvent] to the selectable subtree. |
659 | /// |
660 | /// If the selectable subtree returns a [SelectionResult.pending], this method |
661 | /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result |
662 | /// is not pending or users end their gestures. |
663 | void _triggerSelectionEndEdgeUpdate({TextGranularity? textGranularity}) { |
664 | // This method can be called when the drag is not in progress. This can |
665 | // happen if the child scrollable returns SelectionResult.pending, and |
666 | // the selection area scheduled a selection update for the next frame, but |
667 | // the drag is lifted before the scheduled selection update is run. |
668 | if (_scheduledSelectionEndEdgeUpdate || !_userDraggingSelectionEnd) { |
669 | return; |
670 | } |
671 | if (_selectable?.dispatchSelectionEvent( |
672 | SelectionEdgeUpdateEvent.forEnd(globalPosition: _selectionEndPosition!, granularity: textGranularity)) == SelectionResult.pending) { |
673 | _scheduledSelectionEndEdgeUpdate = true; |
674 | SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { |
675 | if (!_scheduledSelectionEndEdgeUpdate) { |
676 | return; |
677 | } |
678 | _scheduledSelectionEndEdgeUpdate = false; |
679 | _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); |
680 | }, debugLabel: 'SelectableRegion.endEdgeUpdate' ); |
681 | return; |
682 | } |
683 | } |
684 | |
685 | void _onAnyDragEnd(DragEndDetails details) { |
686 | if (widget.selectionControls is! TextSelectionHandleControls) { |
687 | _selectionOverlay!.hideMagnifier(); |
688 | _selectionOverlay!.showToolbar(); |
689 | } else { |
690 | _selectionOverlay!.hideMagnifier(); |
691 | _selectionOverlay!.showToolbar( |
692 | context: context, |
693 | contextMenuBuilder: (BuildContext context) { |
694 | return widget.contextMenuBuilder!(context, this); |
695 | }, |
696 | ); |
697 | } |
698 | _stopSelectionStartEdgeUpdate(); |
699 | _stopSelectionEndEdgeUpdate(); |
700 | _updateSelectedContentIfNeeded(); |
701 | } |
702 | |
703 | void _stopSelectionEndEdgeUpdate() { |
704 | _scheduledSelectionEndEdgeUpdate = false; |
705 | _selectionEndPosition = null; |
706 | } |
707 | |
708 | Offset? _selectionStartPosition; |
709 | bool get _userDraggingSelectionStart => _selectionStartPosition != null; |
710 | bool _scheduledSelectionStartEdgeUpdate = false; |
711 | |
712 | /// Sends start [SelectionEdgeUpdateEvent] to the selectable subtree. |
713 | /// |
714 | /// If the selectable subtree returns a [SelectionResult.pending], this method |
715 | /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result |
716 | /// is not pending or users end their gestures. |
717 | void _triggerSelectionStartEdgeUpdate({TextGranularity? textGranularity}) { |
718 | // This method can be called when the drag is not in progress. This can |
719 | // happen if the child scrollable returns SelectionResult.pending, and |
720 | // the selection area scheduled a selection update for the next frame, but |
721 | // the drag is lifted before the scheduled selection update is run. |
722 | if (_scheduledSelectionStartEdgeUpdate || !_userDraggingSelectionStart) { |
723 | return; |
724 | } |
725 | if (_selectable?.dispatchSelectionEvent( |
726 | SelectionEdgeUpdateEvent.forStart(globalPosition: _selectionStartPosition!, granularity: textGranularity)) == SelectionResult.pending) { |
727 | _scheduledSelectionStartEdgeUpdate = true; |
728 | SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { |
729 | if (!_scheduledSelectionStartEdgeUpdate) { |
730 | return; |
731 | } |
732 | _scheduledSelectionStartEdgeUpdate = false; |
733 | _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); |
734 | }, debugLabel: 'SelectableRegion.startEdgeUpdate' ); |
735 | return; |
736 | } |
737 | } |
738 | |
739 | void _stopSelectionStartEdgeUpdate() { |
740 | _scheduledSelectionStartEdgeUpdate = false; |
741 | _selectionEndPosition = null; |
742 | } |
743 | |
744 | // SelectionOverlay helper methods. |
745 | |
746 | late Offset _selectionStartHandleDragPosition; |
747 | late Offset _selectionEndHandleDragPosition; |
748 | |
749 | void _handleSelectionStartHandleDragStart(DragStartDetails details) { |
750 | assert(_selectionDelegate.value.startSelectionPoint != null); |
751 | |
752 | final Offset localPosition = _selectionDelegate.value.startSelectionPoint!.localPosition; |
753 | final Matrix4 globalTransform = _selectable!.getTransformTo(null); |
754 | _selectionStartHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition); |
755 | |
756 | _selectionOverlay!.showMagnifier(_buildInfoForMagnifier( |
757 | details.globalPosition, |
758 | _selectionDelegate.value.startSelectionPoint!, |
759 | )); |
760 | _updateSelectedContentIfNeeded(); |
761 | } |
762 | |
763 | void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) { |
764 | _selectionStartHandleDragPosition = _selectionStartHandleDragPosition + details.delta; |
765 | // The value corresponds to the paint origin of the selection handle. |
766 | // Offset it to the center of the line to make it feel more natural. |
767 | _selectionStartPosition = _selectionStartHandleDragPosition - Offset(0, _selectionDelegate.value.startSelectionPoint!.lineHeight / 2); |
768 | _triggerSelectionStartEdgeUpdate(); |
769 | |
770 | _selectionOverlay!.updateMagnifier(_buildInfoForMagnifier( |
771 | details.globalPosition, |
772 | _selectionDelegate.value.startSelectionPoint!, |
773 | )); |
774 | _updateSelectedContentIfNeeded(); |
775 | } |
776 | |
777 | void _handleSelectionEndHandleDragStart(DragStartDetails details) { |
778 | assert(_selectionDelegate.value.endSelectionPoint != null); |
779 | final Offset localPosition = _selectionDelegate.value.endSelectionPoint!.localPosition; |
780 | final Matrix4 globalTransform = _selectable!.getTransformTo(null); |
781 | _selectionEndHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition); |
782 | |
783 | _selectionOverlay!.showMagnifier(_buildInfoForMagnifier( |
784 | details.globalPosition, |
785 | _selectionDelegate.value.endSelectionPoint!, |
786 | )); |
787 | _updateSelectedContentIfNeeded(); |
788 | } |
789 | |
790 | void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) { |
791 | _selectionEndHandleDragPosition = _selectionEndHandleDragPosition + details.delta; |
792 | // The value corresponds to the paint origin of the selection handle. |
793 | // Offset it to the center of the line to make it feel more natural. |
794 | _selectionEndPosition = _selectionEndHandleDragPosition - Offset(0, _selectionDelegate.value.endSelectionPoint!.lineHeight / 2); |
795 | _triggerSelectionEndEdgeUpdate(); |
796 | |
797 | _selectionOverlay!.updateMagnifier(_buildInfoForMagnifier( |
798 | details.globalPosition, |
799 | _selectionDelegate.value.endSelectionPoint!, |
800 | )); |
801 | _updateSelectedContentIfNeeded(); |
802 | } |
803 | |
804 | MagnifierInfo _buildInfoForMagnifier(Offset globalGesturePosition, SelectionPoint selectionPoint) { |
805 | final Vector3 globalTransform = _selectable!.getTransformTo(null).getTranslation(); |
806 | final Offset globalTransformAsOffset = Offset(globalTransform.x, globalTransform.y); |
807 | final Offset globalSelectionPointPosition = selectionPoint.localPosition + globalTransformAsOffset; |
808 | final Rect caretRect = Rect.fromLTWH( |
809 | globalSelectionPointPosition.dx, |
810 | globalSelectionPointPosition.dy - selectionPoint.lineHeight, |
811 | 0, |
812 | selectionPoint.lineHeight |
813 | ); |
814 | |
815 | return MagnifierInfo( |
816 | globalGesturePosition: globalGesturePosition, |
817 | caretRect: caretRect, |
818 | fieldBounds: globalTransformAsOffset & _selectable!.size, |
819 | currentLineBoundaries: globalTransformAsOffset & _selectable!.size, |
820 | ); |
821 | } |
822 | |
823 | void _createSelectionOverlay() { |
824 | assert(_hasSelectionOverlayGeometry); |
825 | if (_selectionOverlay != null) { |
826 | return; |
827 | } |
828 | final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; |
829 | final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; |
830 | _selectionOverlay = SelectionOverlay( |
831 | context: context, |
832 | debugRequiredFor: widget, |
833 | startHandleType: start?.handleType ?? TextSelectionHandleType.left, |
834 | lineHeightAtStart: start?.lineHeight ?? end!.lineHeight, |
835 | onStartHandleDragStart: _handleSelectionStartHandleDragStart, |
836 | onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate, |
837 | onStartHandleDragEnd: _onAnyDragEnd, |
838 | endHandleType: end?.handleType ?? TextSelectionHandleType.right, |
839 | lineHeightAtEnd: end?.lineHeight ?? start!.lineHeight, |
840 | onEndHandleDragStart: _handleSelectionEndHandleDragStart, |
841 | onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate, |
842 | onEndHandleDragEnd: _onAnyDragEnd, |
843 | selectionEndpoints: selectionEndpoints, |
844 | selectionControls: widget.selectionControls, |
845 | selectionDelegate: this, |
846 | clipboardStatus: null, |
847 | startHandleLayerLink: _startHandleLayerLink, |
848 | endHandleLayerLink: _endHandleLayerLink, |
849 | toolbarLayerLink: _toolbarLayerLink, |
850 | magnifierConfiguration: widget.magnifierConfiguration |
851 | ); |
852 | } |
853 | |
854 | void _updateSelectionOverlay() { |
855 | if (_selectionOverlay == null) { |
856 | return; |
857 | } |
858 | assert(_hasSelectionOverlayGeometry); |
859 | final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; |
860 | final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; |
861 | _selectionOverlay! |
862 | ..startHandleType = start?.handleType ?? TextSelectionHandleType.left |
863 | ..lineHeightAtStart = start?.lineHeight ?? end!.lineHeight |
864 | ..endHandleType = end?.handleType ?? TextSelectionHandleType.right |
865 | ..lineHeightAtEnd = end?.lineHeight ?? start!.lineHeight |
866 | ..selectionEndpoints = selectionEndpoints; |
867 | } |
868 | |
869 | /// Shows the selection handles. |
870 | /// |
871 | /// Returns true if the handles are shown, false if the handles can't be |
872 | /// shown. |
873 | bool _showHandles() { |
874 | if (_selectionOverlay != null) { |
875 | _selectionOverlay!.showHandles(); |
876 | return true; |
877 | } |
878 | |
879 | if (!_hasSelectionOverlayGeometry) { |
880 | return false; |
881 | } |
882 | |
883 | _createSelectionOverlay(); |
884 | _selectionOverlay!.showHandles(); |
885 | return true; |
886 | } |
887 | |
888 | /// Shows the text selection toolbar. |
889 | /// |
890 | /// If the parameter `location` is set, the toolbar will be shown at the |
891 | /// location. Otherwise, the toolbar location will be calculated based on the |
892 | /// handles' locations. The `location` is in the coordinates system of the |
893 | /// [Overlay]. |
894 | /// |
895 | /// Returns true if the toolbar is shown, false if the toolbar can't be shown. |
896 | bool _showToolbar({Offset? location}) { |
897 | if (!_hasSelectionOverlayGeometry && _selectionOverlay == null) { |
898 | return false; |
899 | } |
900 | |
901 | // Web is using native dom elements to enable clipboard functionality of the |
902 | // context menu: copy, paste, select, cut. It might also provide additional |
903 | // functionality depending on the browser (such as translate). Due to this, |
904 | // we should not show a Flutter toolbar for the editable text elements |
905 | // unless the browser's context menu is explicitly disabled. |
906 | if (kIsWeb && BrowserContextMenu.enabled) { |
907 | return false; |
908 | } |
909 | |
910 | if (_selectionOverlay == null) { |
911 | _createSelectionOverlay(); |
912 | } |
913 | |
914 | _selectionOverlay!.toolbarLocation = location; |
915 | if (widget.selectionControls is! TextSelectionHandleControls) { |
916 | _selectionOverlay!.showToolbar(); |
917 | return true; |
918 | } |
919 | |
920 | _selectionOverlay!.hideToolbar(); |
921 | |
922 | _selectionOverlay!.showToolbar( |
923 | context: context, |
924 | contextMenuBuilder: (BuildContext context) { |
925 | return widget.contextMenuBuilder!(context, this); |
926 | }, |
927 | ); |
928 | return true; |
929 | } |
930 | |
931 | /// Sets or updates selection end edge to the `offset` location. |
932 | /// |
933 | /// A selection always contains a select start edge and selection end edge. |
934 | /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or |
935 | /// use other selection APIs, such as [_selectWordAt] or [selectAll]. |
936 | /// |
937 | /// This method sets or updates the selection end edge by sending |
938 | /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s. |
939 | /// |
940 | /// If `continuous` is set to true and the update causes scrolling, the |
941 | /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the |
942 | /// child [Selectable]s every frame until the scrolling finishes or a |
943 | /// [_finalizeSelection] is called. |
944 | /// |
945 | /// The `continuous` argument defaults to false. |
946 | /// |
947 | /// The `offset` is in global coordinates. |
948 | /// |
949 | /// Provide the `textGranularity` if the selection should not move by the default |
950 | /// [TextGranularity.character]. Only [TextGranularity.character] and |
951 | /// [TextGranularity.word] are currently supported. |
952 | /// |
953 | /// See also: |
954 | /// * [_selectStartTo], which sets or updates selection start edge. |
955 | /// * [_finalizeSelection], which stops the `continuous` updates. |
956 | /// * [_clearSelection], which clears the ongoing selection. |
957 | /// * [_selectWordAt], which selects a whole word at the location. |
958 | /// * [_collapseSelectionAt], which collapses the selection at the location. |
959 | /// * [selectAll], which selects the entire content. |
960 | void _selectEndTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) { |
961 | if (!continuous) { |
962 | _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: offset, granularity: textGranularity)); |
963 | return; |
964 | } |
965 | if (_selectionEndPosition != offset) { |
966 | _selectionEndPosition = offset; |
967 | _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); |
968 | } |
969 | } |
970 | |
971 | /// Sets or updates selection start edge to the `offset` location. |
972 | /// |
973 | /// A selection always contains a select start edge and selection end edge. |
974 | /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or |
975 | /// use other selection APIs, such as [_selectWordAt] or [selectAll]. |
976 | /// |
977 | /// This method sets or updates the selection start edge by sending |
978 | /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s. |
979 | /// |
980 | /// If `continuous` is set to true and the update causes scrolling, the |
981 | /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the |
982 | /// child [Selectable]s every frame until the scrolling finishes or a |
983 | /// [_finalizeSelection] is called. |
984 | /// |
985 | /// The `continuous` argument defaults to false. |
986 | /// |
987 | /// The `offset` is in global coordinates. |
988 | /// |
989 | /// Provide the `textGranularity` if the selection should not move by the default |
990 | /// [TextGranularity.character]. Only [TextGranularity.character] and |
991 | /// [TextGranularity.word] are currently supported. |
992 | /// |
993 | /// See also: |
994 | /// * [_selectEndTo], which sets or updates selection end edge. |
995 | /// * [_finalizeSelection], which stops the `continuous` updates. |
996 | /// * [_clearSelection], which clears the ongoing selection. |
997 | /// * [_selectWordAt], which selects a whole word at the location. |
998 | /// * [_collapseSelectionAt], which collapses the selection at the location. |
999 | /// * [selectAll], which selects the entire content. |
1000 | void _selectStartTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) { |
1001 | if (!continuous) { |
1002 | _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forStart(globalPosition: offset, granularity: textGranularity)); |
1003 | return; |
1004 | } |
1005 | if (_selectionStartPosition != offset) { |
1006 | _selectionStartPosition = offset; |
1007 | _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); |
1008 | } |
1009 | } |
1010 | |
1011 | /// Collapses the selection at the given `offset` location. |
1012 | /// |
1013 | /// See also: |
1014 | /// * [_selectStartTo], which sets or updates selection start edge. |
1015 | /// * [_selectEndTo], which sets or updates selection end edge. |
1016 | /// * [_finalizeSelection], which stops the `continuous` updates. |
1017 | /// * [_clearSelection], which clears the ongoing selection. |
1018 | /// * [_selectWordAt], which selects a whole word at the location. |
1019 | /// * [selectAll], which selects the entire content. |
1020 | void _collapseSelectionAt({required Offset offset}) { |
1021 | _selectStartTo(offset: offset); |
1022 | _selectEndTo(offset: offset); |
1023 | } |
1024 | |
1025 | /// Selects a whole word at the `offset` location. |
1026 | /// |
1027 | /// If the whole word is already in the current selection, selection won't |
1028 | /// change. One call [_clearSelection] first if the selection needs to be |
1029 | /// updated even if the word is already covered by the current selection. |
1030 | /// |
1031 | /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection |
1032 | /// edges after calling this method. |
1033 | /// |
1034 | /// See also: |
1035 | /// * [_selectStartTo], which sets or updates selection start edge. |
1036 | /// * [_selectEndTo], which sets or updates selection end edge. |
1037 | /// * [_finalizeSelection], which stops the `continuous` updates. |
1038 | /// * [_clearSelection], which clears the ongoing selection. |
1039 | /// * [_collapseSelectionAt], which collapses the selection at the location. |
1040 | /// * [selectAll], which selects the entire content. |
1041 | void _selectWordAt({required Offset offset}) { |
1042 | // There may be other selection ongoing. |
1043 | _finalizeSelection(); |
1044 | _selectable?.dispatchSelectionEvent(SelectWordSelectionEvent(globalPosition: offset)); |
1045 | } |
1046 | |
1047 | /// Stops any ongoing selection updates. |
1048 | /// |
1049 | /// This method is different from [_clearSelection] that it does not remove |
1050 | /// the current selection. It only stops the continuous updates. |
1051 | /// |
1052 | /// A continuous update can happen as result of calling [_selectStartTo] or |
1053 | /// [_selectEndTo] with `continuous` sets to true which causes a [Selectable] |
1054 | /// to scroll. Calling this method will stop the update as well as the |
1055 | /// scrolling. |
1056 | void _finalizeSelection() { |
1057 | _stopSelectionEndEdgeUpdate(); |
1058 | _stopSelectionStartEdgeUpdate(); |
1059 | } |
1060 | |
1061 | /// Removes the ongoing selection. |
1062 | void _clearSelection() { |
1063 | _finalizeSelection(); |
1064 | _directionalHorizontalBaseline = null; |
1065 | _adjustingSelectionEnd = null; |
1066 | _selectable?.dispatchSelectionEvent(const ClearSelectionEvent()); |
1067 | _updateSelectedContentIfNeeded(); |
1068 | } |
1069 | |
1070 | Future<void> _copy() async { |
1071 | final SelectedContent? data = _selectable?.getSelectedContent(); |
1072 | if (data == null) { |
1073 | return; |
1074 | } |
1075 | await Clipboard.setData(ClipboardData(text: data.plainText)); |
1076 | } |
1077 | |
1078 | /// {@macro flutter.widgets.EditableText.getAnchors} |
1079 | /// |
1080 | /// See also: |
1081 | /// |
1082 | /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s |
1083 | /// for the default context menu buttons. |
1084 | TextSelectionToolbarAnchors get contextMenuAnchors { |
1085 | if (lastSecondaryTapDownPosition != null) { |
1086 | return TextSelectionToolbarAnchors( |
1087 | primaryAnchor: lastSecondaryTapDownPosition!, |
1088 | ); |
1089 | } |
1090 | final RenderBox renderBox = context.findRenderObject()! as RenderBox; |
1091 | return TextSelectionToolbarAnchors.fromSelection( |
1092 | renderBox: renderBox, |
1093 | startGlyphHeight: startGlyphHeight, |
1094 | endGlyphHeight: endGlyphHeight, |
1095 | selectionEndpoints: selectionEndpoints, |
1096 | ); |
1097 | } |
1098 | |
1099 | bool? _adjustingSelectionEnd; |
1100 | bool _determineIsAdjustingSelectionEnd(bool forward) { |
1101 | if (_adjustingSelectionEnd != null) { |
1102 | return _adjustingSelectionEnd!; |
1103 | } |
1104 | final bool isReversed; |
1105 | final SelectionPoint start = _selectionDelegate.value |
1106 | .startSelectionPoint!; |
1107 | final SelectionPoint end = _selectionDelegate.value.endSelectionPoint!; |
1108 | if (start.localPosition.dy > end.localPosition.dy) { |
1109 | isReversed = true; |
1110 | } else if (start.localPosition.dy < end.localPosition.dy) { |
1111 | isReversed = false; |
1112 | } else { |
1113 | isReversed = start.localPosition.dx > end.localPosition.dx; |
1114 | } |
1115 | // Always move the selection edge that increases the selection range. |
1116 | return _adjustingSelectionEnd = forward != isReversed; |
1117 | } |
1118 | |
1119 | void _granularlyExtendSelection(TextGranularity granularity, bool forward) { |
1120 | _directionalHorizontalBaseline = null; |
1121 | if (!_selectionDelegate.value.hasSelection) { |
1122 | return; |
1123 | } |
1124 | _selectable?.dispatchSelectionEvent( |
1125 | GranularlyExtendSelectionEvent( |
1126 | forward: forward, |
1127 | isEnd: _determineIsAdjustingSelectionEnd(forward), |
1128 | granularity: granularity, |
1129 | ), |
1130 | ); |
1131 | _updateSelectedContentIfNeeded(); |
1132 | } |
1133 | |
1134 | double? _directionalHorizontalBaseline; |
1135 | |
1136 | void _directionallyExtendSelection(bool forward) { |
1137 | if (!_selectionDelegate.value.hasSelection) { |
1138 | return; |
1139 | } |
1140 | final bool adjustingSelectionExtend = _determineIsAdjustingSelectionEnd(forward); |
1141 | final SelectionPoint baseLinePoint = adjustingSelectionExtend |
1142 | ? _selectionDelegate.value.endSelectionPoint! |
1143 | : _selectionDelegate.value.startSelectionPoint!; |
1144 | _directionalHorizontalBaseline ??= baseLinePoint.localPosition.dx; |
1145 | final Offset globalSelectionPointOffset = MatrixUtils.transformPoint(context.findRenderObject()!.getTransformTo(null), Offset(_directionalHorizontalBaseline!, 0)); |
1146 | _selectable?.dispatchSelectionEvent( |
1147 | DirectionallyExtendSelectionEvent( |
1148 | isEnd: _adjustingSelectionEnd!, |
1149 | direction: forward ? SelectionExtendDirection.nextLine : SelectionExtendDirection.previousLine, |
1150 | dx: globalSelectionPointOffset.dx, |
1151 | ), |
1152 | ); |
1153 | _updateSelectedContentIfNeeded(); |
1154 | } |
1155 | |
1156 | // [TextSelectionDelegate] overrides. |
1157 | |
1158 | /// Returns the [ContextMenuButtonItem]s representing the buttons in this |
1159 | /// platform's default selection menu. |
1160 | /// |
1161 | /// See also: |
1162 | /// |
1163 | /// * [SelectableRegion.getSelectableButtonItems], which performs a similar role, |
1164 | /// but for any selectable text, not just specifically SelectableRegion. |
1165 | /// * [EditableTextState.contextMenuButtonItems], which performs a similar role |
1166 | /// but for content that is not just selectable but also editable. |
1167 | /// * [contextMenuAnchors], which provides the anchor points for the default |
1168 | /// context menu. |
1169 | /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can |
1170 | /// take a list of [ContextMenuButtonItem]s with |
1171 | /// [AdaptiveTextSelectionToolbar.buttonItems]. |
1172 | /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the |
1173 | /// button Widgets for the current platform given [ContextMenuButtonItem]s. |
1174 | List<ContextMenuButtonItem> get contextMenuButtonItems { |
1175 | return SelectableRegion.getSelectableButtonItems( |
1176 | selectionGeometry: _selectionDelegate.value, |
1177 | onCopy: () { |
1178 | _copy(); |
1179 | |
1180 | // In Android copy should clear the selection. |
1181 | switch (defaultTargetPlatform) { |
1182 | case TargetPlatform.android: |
1183 | case TargetPlatform.fuchsia: |
1184 | _clearSelection(); |
1185 | case TargetPlatform.iOS: |
1186 | hideToolbar(false); |
1187 | case TargetPlatform.linux: |
1188 | case TargetPlatform.macOS: |
1189 | case TargetPlatform.windows: |
1190 | hideToolbar(); |
1191 | } |
1192 | }, |
1193 | onSelectAll: () { |
1194 | switch (defaultTargetPlatform) { |
1195 | case TargetPlatform.android: |
1196 | case TargetPlatform.iOS: |
1197 | case TargetPlatform.fuchsia: |
1198 | selectAll(SelectionChangedCause.toolbar); |
1199 | case TargetPlatform.linux: |
1200 | case TargetPlatform.macOS: |
1201 | case TargetPlatform.windows: |
1202 | selectAll(); |
1203 | hideToolbar(); |
1204 | } |
1205 | }, |
1206 | ); |
1207 | } |
1208 | |
1209 | /// The line height at the start of the current selection. |
1210 | double get startGlyphHeight { |
1211 | return _selectionDelegate.value.startSelectionPoint!.lineHeight; |
1212 | } |
1213 | |
1214 | /// The line height at the end of the current selection. |
1215 | double get endGlyphHeight { |
1216 | return _selectionDelegate.value.endSelectionPoint!.lineHeight; |
1217 | } |
1218 | |
1219 | /// Returns the local coordinates of the endpoints of the current selection. |
1220 | List<TextSelectionPoint> get selectionEndpoints { |
1221 | final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; |
1222 | final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; |
1223 | late List<TextSelectionPoint> points; |
1224 | final Offset startLocalPosition = start?.localPosition ?? end!.localPosition; |
1225 | final Offset endLocalPosition = end?.localPosition ?? start!.localPosition; |
1226 | if (startLocalPosition.dy > endLocalPosition.dy) { |
1227 | points = <TextSelectionPoint>[ |
1228 | TextSelectionPoint(endLocalPosition, TextDirection.ltr), |
1229 | TextSelectionPoint(startLocalPosition, TextDirection.ltr), |
1230 | ]; |
1231 | } else { |
1232 | points = <TextSelectionPoint>[ |
1233 | TextSelectionPoint(startLocalPosition, TextDirection.ltr), |
1234 | TextSelectionPoint(endLocalPosition, TextDirection.ltr), |
1235 | ]; |
1236 | } |
1237 | return points; |
1238 | } |
1239 | |
1240 | // [TextSelectionDelegate] overrides. |
1241 | // TODO(justinmc): After deprecations have been removed, remove |
1242 | // TextSelectionDelegate from this class. |
1243 | // https://github.com/flutter/flutter/issues/111213 |
1244 | |
1245 | @Deprecated( |
1246 | 'Use `contextMenuBuilder` instead. ' |
1247 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1248 | ) |
1249 | @override |
1250 | bool get cutEnabled => false; |
1251 | |
1252 | @Deprecated( |
1253 | 'Use `contextMenuBuilder` instead. ' |
1254 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1255 | ) |
1256 | @override |
1257 | bool get pasteEnabled => false; |
1258 | |
1259 | @override |
1260 | void hideToolbar([bool hideHandles = true]) { |
1261 | _selectionOverlay?.hideToolbar(); |
1262 | if (hideHandles) { |
1263 | _selectionOverlay?.hideHandles(); |
1264 | } |
1265 | } |
1266 | |
1267 | @override |
1268 | void selectAll([SelectionChangedCause? cause]) { |
1269 | _clearSelection(); |
1270 | _selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent()); |
1271 | if (cause == SelectionChangedCause.toolbar) { |
1272 | _showToolbar(); |
1273 | _showHandles(); |
1274 | } |
1275 | _updateSelectedContentIfNeeded(); |
1276 | } |
1277 | |
1278 | @Deprecated( |
1279 | 'Use `contextMenuBuilder` instead. ' |
1280 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1281 | ) |
1282 | @override |
1283 | void copySelection(SelectionChangedCause cause) { |
1284 | _copy(); |
1285 | _clearSelection(); |
1286 | } |
1287 | |
1288 | @Deprecated( |
1289 | 'Use `contextMenuBuilder` instead. ' |
1290 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1291 | ) |
1292 | @override |
1293 | TextEditingValue textEditingValue = const TextEditingValue(text: '_' ); |
1294 | |
1295 | @Deprecated( |
1296 | 'Use `contextMenuBuilder` instead. ' |
1297 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1298 | ) |
1299 | @override |
1300 | void bringIntoView(TextPosition position) {/* SelectableRegion must be in view at this point. */} |
1301 | |
1302 | @Deprecated( |
1303 | 'Use `contextMenuBuilder` instead. ' |
1304 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1305 | ) |
1306 | @override |
1307 | void cutSelection(SelectionChangedCause cause) { |
1308 | assert(false); |
1309 | } |
1310 | |
1311 | @Deprecated( |
1312 | 'Use `contextMenuBuilder` instead. ' |
1313 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1314 | ) |
1315 | @override |
1316 | void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {/* SelectableRegion maintains its own state */} |
1317 | |
1318 | @Deprecated( |
1319 | 'Use `contextMenuBuilder` instead. ' |
1320 | 'This feature was deprecated after v3.3.0-0.5.pre.' , |
1321 | ) |
1322 | @override |
1323 | Future<void> pasteText(SelectionChangedCause cause) async { |
1324 | assert(false); |
1325 | } |
1326 | |
1327 | // [SelectionRegistrar] override. |
1328 | |
1329 | @override |
1330 | void add(Selectable selectable) { |
1331 | assert(_selectable == null); |
1332 | _selectable = selectable; |
1333 | _selectable!.addListener(_updateSelectionStatus); |
1334 | _selectable!.pushHandleLayers(_startHandleLayerLink, _endHandleLayerLink); |
1335 | } |
1336 | |
1337 | @override |
1338 | void remove(Selectable selectable) { |
1339 | assert(_selectable == selectable); |
1340 | _selectable!.removeListener(_updateSelectionStatus); |
1341 | _selectable!.pushHandleLayers(null, null); |
1342 | _selectable = null; |
1343 | } |
1344 | |
1345 | @override |
1346 | void dispose() { |
1347 | _selectable?.removeListener(_updateSelectionStatus); |
1348 | _selectable?.pushHandleLayers(null, null); |
1349 | _selectionDelegate.dispose(); |
1350 | // In case dispose was triggered before gesture end, remove the magnifier |
1351 | // so it doesn't remain stuck in the overlay forever. |
1352 | _selectionOverlay?.hideMagnifier(); |
1353 | _selectionOverlay?.dispose(); |
1354 | _selectionOverlay = null; |
1355 | super.dispose(); |
1356 | } |
1357 | |
1358 | @override |
1359 | Widget build(BuildContext context) { |
1360 | assert(debugCheckHasOverlay(context)); |
1361 | Widget result = SelectionContainer( |
1362 | registrar: this, |
1363 | delegate: _selectionDelegate, |
1364 | child: widget.child, |
1365 | ); |
1366 | if (kIsWeb) { |
1367 | result = PlatformSelectableRegionContextMenu( |
1368 | child: result, |
1369 | ); |
1370 | } |
1371 | return CompositedTransformTarget( |
1372 | link: _toolbarLayerLink, |
1373 | child: RawGestureDetector( |
1374 | gestures: _gestureRecognizers, |
1375 | behavior: HitTestBehavior.translucent, |
1376 | excludeFromSemantics: true, |
1377 | child: Actions( |
1378 | actions: _actions, |
1379 | child: Focus( |
1380 | includeSemantics: false, |
1381 | focusNode: widget.focusNode, |
1382 | child: result, |
1383 | ), |
1384 | ), |
1385 | ), |
1386 | ); |
1387 | } |
1388 | } |
1389 | |
1390 | /// An action that does not override any [Action.overridable] in the subtree. |
1391 | /// |
1392 | /// If this action is invoked by an [Action.overridable], it will immediately |
1393 | /// invoke the [Action.overridable] and do nothing else. Otherwise, it will call |
1394 | /// [invokeAction]. |
1395 | abstract class _NonOverrideAction<T extends Intent> extends ContextAction<T> { |
1396 | Object? invokeAction(T intent, [BuildContext? context]); |
1397 | |
1398 | @override |
1399 | Object? invoke(T intent, [BuildContext? context]) { |
1400 | if (callingAction != null) { |
1401 | return callingAction!.invoke(intent); |
1402 | } |
1403 | return invokeAction(intent, context); |
1404 | } |
1405 | } |
1406 | |
1407 | class _SelectAllAction extends _NonOverrideAction<SelectAllTextIntent> { |
1408 | _SelectAllAction(this.state); |
1409 | |
1410 | final SelectableRegionState state; |
1411 | |
1412 | @override |
1413 | void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) { |
1414 | state.selectAll(SelectionChangedCause.keyboard); |
1415 | } |
1416 | } |
1417 | |
1418 | class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> { |
1419 | _CopySelectionAction(this.state); |
1420 | |
1421 | final SelectableRegionState state; |
1422 | |
1423 | @override |
1424 | void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) { |
1425 | state._copy(); |
1426 | } |
1427 | } |
1428 | |
1429 | class _GranularlyExtendSelectionAction<T extends DirectionalTextEditingIntent> extends _NonOverrideAction<T> { |
1430 | _GranularlyExtendSelectionAction(this.state, {required this.granularity}); |
1431 | |
1432 | final SelectableRegionState state; |
1433 | final TextGranularity granularity; |
1434 | |
1435 | @override |
1436 | void invokeAction(T intent, [BuildContext? context]) { |
1437 | state._granularlyExtendSelection(granularity, intent.forward); |
1438 | } |
1439 | } |
1440 | |
1441 | class _GranularlyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent> extends _NonOverrideAction<T> { |
1442 | _GranularlyExtendCaretSelectionAction(this.state, {required this.granularity}); |
1443 | |
1444 | final SelectableRegionState state; |
1445 | final TextGranularity granularity; |
1446 | |
1447 | @override |
1448 | void invokeAction(T intent, [BuildContext? context]) { |
1449 | if (intent.collapseSelection) { |
1450 | // Selectable region never collapses selection. |
1451 | return; |
1452 | } |
1453 | state._granularlyExtendSelection(granularity, intent.forward); |
1454 | } |
1455 | } |
1456 | |
1457 | class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent> extends _NonOverrideAction<T> { |
1458 | _DirectionallyExtendCaretSelectionAction(this.state); |
1459 | |
1460 | final SelectableRegionState state; |
1461 | |
1462 | @override |
1463 | void invokeAction(T intent, [BuildContext? context]) { |
1464 | if (intent.collapseSelection) { |
1465 | // Selectable region never collapses selection. |
1466 | return; |
1467 | } |
1468 | state._directionallyExtendSelection(intent.forward); |
1469 | } |
1470 | } |
1471 | |
1472 | class _SelectableRegionContainerDelegate extends MultiSelectableSelectionContainerDelegate { |
1473 | final Set<Selectable> _hasReceivedStartEvent = <Selectable>{}; |
1474 | final Set<Selectable> _hasReceivedEndEvent = <Selectable>{}; |
1475 | |
1476 | Offset? _lastStartEdgeUpdateGlobalPosition; |
1477 | Offset? _lastEndEdgeUpdateGlobalPosition; |
1478 | |
1479 | @override |
1480 | void remove(Selectable selectable) { |
1481 | _hasReceivedStartEvent.remove(selectable); |
1482 | _hasReceivedEndEvent.remove(selectable); |
1483 | super.remove(selectable); |
1484 | } |
1485 | |
1486 | void _updateLastEdgeEventsFromGeometries() { |
1487 | if (currentSelectionStartIndex != -1 && selectables[currentSelectionStartIndex].value.hasSelection) { |
1488 | final Selectable start = selectables[currentSelectionStartIndex]; |
1489 | final Offset localStartEdge = start.value.startSelectionPoint!.localPosition + |
1490 | Offset(0, - start.value.startSelectionPoint!.lineHeight / 2); |
1491 | _lastStartEdgeUpdateGlobalPosition = MatrixUtils.transformPoint(start.getTransformTo(null), localStartEdge); |
1492 | } |
1493 | if (currentSelectionEndIndex != -1 && selectables[currentSelectionEndIndex].value.hasSelection) { |
1494 | final Selectable end = selectables[currentSelectionEndIndex]; |
1495 | final Offset localEndEdge = end.value.endSelectionPoint!.localPosition + |
1496 | Offset(0, -end.value.endSelectionPoint!.lineHeight / 2); |
1497 | _lastEndEdgeUpdateGlobalPosition = MatrixUtils.transformPoint(end.getTransformTo(null), localEndEdge); |
1498 | } |
1499 | } |
1500 | |
1501 | @override |
1502 | SelectionResult handleSelectAll(SelectAllSelectionEvent event) { |
1503 | final SelectionResult result = super.handleSelectAll(event); |
1504 | for (final Selectable selectable in selectables) { |
1505 | _hasReceivedStartEvent.add(selectable); |
1506 | _hasReceivedEndEvent.add(selectable); |
1507 | } |
1508 | // Synthesize last update event so the edge updates continue to work. |
1509 | _updateLastEdgeEventsFromGeometries(); |
1510 | return result; |
1511 | } |
1512 | |
1513 | /// Selects a word in a selectable at the location |
1514 | /// [SelectWordSelectionEvent.globalPosition]. |
1515 | @override |
1516 | SelectionResult handleSelectWord(SelectWordSelectionEvent event) { |
1517 | final SelectionResult result = super.handleSelectWord(event); |
1518 | if (currentSelectionStartIndex != -1) { |
1519 | _hasReceivedStartEvent.add(selectables[currentSelectionStartIndex]); |
1520 | } |
1521 | if (currentSelectionEndIndex != -1) { |
1522 | _hasReceivedEndEvent.add(selectables[currentSelectionEndIndex]); |
1523 | } |
1524 | _updateLastEdgeEventsFromGeometries(); |
1525 | return result; |
1526 | } |
1527 | |
1528 | @override |
1529 | SelectionResult handleClearSelection(ClearSelectionEvent event) { |
1530 | final SelectionResult result = super.handleClearSelection(event); |
1531 | _hasReceivedStartEvent.clear(); |
1532 | _hasReceivedEndEvent.clear(); |
1533 | _lastStartEdgeUpdateGlobalPosition = null; |
1534 | _lastEndEdgeUpdateGlobalPosition = null; |
1535 | return result; |
1536 | } |
1537 | |
1538 | @override |
1539 | SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { |
1540 | if (event.type == SelectionEventType.endEdgeUpdate) { |
1541 | _lastEndEdgeUpdateGlobalPosition = event.globalPosition; |
1542 | } else { |
1543 | _lastStartEdgeUpdateGlobalPosition = event.globalPosition; |
1544 | } |
1545 | return super.handleSelectionEdgeUpdate(event); |
1546 | } |
1547 | |
1548 | @override |
1549 | void dispose() { |
1550 | _hasReceivedStartEvent.clear(); |
1551 | _hasReceivedEndEvent.clear(); |
1552 | super.dispose(); |
1553 | } |
1554 | |
1555 | @override |
1556 | SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) { |
1557 | switch (event.type) { |
1558 | case SelectionEventType.startEdgeUpdate: |
1559 | _hasReceivedStartEvent.add(selectable); |
1560 | ensureChildUpdated(selectable); |
1561 | case SelectionEventType.endEdgeUpdate: |
1562 | _hasReceivedEndEvent.add(selectable); |
1563 | ensureChildUpdated(selectable); |
1564 | case SelectionEventType.clear: |
1565 | _hasReceivedStartEvent.remove(selectable); |
1566 | _hasReceivedEndEvent.remove(selectable); |
1567 | case SelectionEventType.selectAll: |
1568 | case SelectionEventType.selectWord: |
1569 | break; |
1570 | case SelectionEventType.granularlyExtendSelection: |
1571 | case SelectionEventType.directionallyExtendSelection: |
1572 | _hasReceivedStartEvent.add(selectable); |
1573 | _hasReceivedEndEvent.add(selectable); |
1574 | ensureChildUpdated(selectable); |
1575 | } |
1576 | return super.dispatchSelectionEventToChild(selectable, event); |
1577 | } |
1578 | |
1579 | @override |
1580 | void ensureChildUpdated(Selectable selectable) { |
1581 | if (_lastEndEdgeUpdateGlobalPosition != null && _hasReceivedEndEvent.add(selectable)) { |
1582 | final SelectionEdgeUpdateEvent synthesizedEvent = SelectionEdgeUpdateEvent.forEnd( |
1583 | globalPosition: _lastEndEdgeUpdateGlobalPosition!, |
1584 | ); |
1585 | if (currentSelectionEndIndex == -1) { |
1586 | handleSelectionEdgeUpdate(synthesizedEvent); |
1587 | } |
1588 | selectable.dispatchSelectionEvent(synthesizedEvent); |
1589 | } |
1590 | if (_lastStartEdgeUpdateGlobalPosition != null && _hasReceivedStartEvent.add(selectable)) { |
1591 | final SelectionEdgeUpdateEvent synthesizedEvent = SelectionEdgeUpdateEvent.forStart( |
1592 | globalPosition: _lastStartEdgeUpdateGlobalPosition!, |
1593 | ); |
1594 | if (currentSelectionStartIndex == -1) { |
1595 | handleSelectionEdgeUpdate(synthesizedEvent); |
1596 | } |
1597 | selectable.dispatchSelectionEvent(synthesizedEvent); |
1598 | } |
1599 | } |
1600 | |
1601 | @override |
1602 | void didChangeSelectables() { |
1603 | if (_lastEndEdgeUpdateGlobalPosition != null) { |
1604 | handleSelectionEdgeUpdate( |
1605 | SelectionEdgeUpdateEvent.forEnd( |
1606 | globalPosition: _lastEndEdgeUpdateGlobalPosition!, |
1607 | ), |
1608 | ); |
1609 | } |
1610 | if (_lastStartEdgeUpdateGlobalPosition != null) { |
1611 | handleSelectionEdgeUpdate( |
1612 | SelectionEdgeUpdateEvent.forStart( |
1613 | globalPosition: _lastStartEdgeUpdateGlobalPosition!, |
1614 | ), |
1615 | ); |
1616 | } |
1617 | final Set<Selectable> selectableSet = selectables.toSet(); |
1618 | _hasReceivedEndEvent.removeWhere((Selectable selectable) => !selectableSet.contains(selectable)); |
1619 | _hasReceivedStartEvent.removeWhere((Selectable selectable) => !selectableSet.contains(selectable)); |
1620 | super.didChangeSelectables(); |
1621 | } |
1622 | } |
1623 | |
1624 | /// An abstract base class for updating multiple selectable children. |
1625 | /// |
1626 | /// This class provide basic [SelectionEvent] handling and child [Selectable] |
1627 | /// updating. The subclass needs to implement [ensureChildUpdated] to ensure |
1628 | /// child [Selectable] is updated properly. |
1629 | /// |
1630 | /// This class optimize the selection update by keeping track of the |
1631 | /// [Selectable]s that currently contain the selection edges. |
1632 | abstract class MultiSelectableSelectionContainerDelegate extends SelectionContainerDelegate with ChangeNotifier { |
1633 | /// Creates an instance of [MultiSelectableSelectionContainerDelegate]. |
1634 | MultiSelectableSelectionContainerDelegate() { |
1635 | if (kFlutterMemoryAllocationsEnabled) { |
1636 | ChangeNotifier.maybeDispatchObjectCreation(this); |
1637 | } |
1638 | } |
1639 | |
1640 | /// Gets the list of selectables this delegate is managing. |
1641 | List<Selectable> selectables = <Selectable>[]; |
1642 | |
1643 | /// The number of additional pixels added to the selection handle drawable |
1644 | /// area. |
1645 | /// |
1646 | /// Selection handles that are outside of the drawable area will be hidden. |
1647 | /// That logic prevents handles that get scrolled off the viewport from being |
1648 | /// drawn on the screen. |
1649 | /// |
1650 | /// The drawable area = current rectangle of [SelectionContainer] + |
1651 | /// _kSelectionHandleDrawableAreaPadding on each side. |
1652 | /// |
1653 | /// This was an eyeballed value to create smooth user experiences. |
1654 | static const double _kSelectionHandleDrawableAreaPadding = 5.0; |
1655 | |
1656 | /// The current selectable that contains the selection end edge. |
1657 | @protected |
1658 | int currentSelectionEndIndex = -1; |
1659 | |
1660 | /// The current selectable that contains the selection start edge. |
1661 | @protected |
1662 | int currentSelectionStartIndex = -1; |
1663 | |
1664 | LayerLink? _startHandleLayer; |
1665 | Selectable? _startHandleLayerOwner; |
1666 | LayerLink? _endHandleLayer; |
1667 | Selectable? _endHandleLayerOwner; |
1668 | |
1669 | bool _isHandlingSelectionEvent = false; |
1670 | bool _scheduledSelectableUpdate = false; |
1671 | bool _selectionInProgress = false; |
1672 | Set<Selectable> _additions = <Selectable>{}; |
1673 | |
1674 | bool _extendSelectionInProgress = false; |
1675 | |
1676 | @override |
1677 | void add(Selectable selectable) { |
1678 | assert(!selectables.contains(selectable)); |
1679 | _additions.add(selectable); |
1680 | _scheduleSelectableUpdate(); |
1681 | } |
1682 | |
1683 | @override |
1684 | void remove(Selectable selectable) { |
1685 | if (_additions.remove(selectable)) { |
1686 | // The same selectable was added in the same frame and is not yet |
1687 | // incorporated into the selectables. |
1688 | // |
1689 | // Removing such selectable doesn't require selection geometry update. |
1690 | return; |
1691 | } |
1692 | _removeSelectable(selectable); |
1693 | _scheduleSelectableUpdate(); |
1694 | } |
1695 | |
1696 | /// Notifies this delegate that layout of the container has changed. |
1697 | void layoutDidChange() { |
1698 | _updateSelectionGeometry(); |
1699 | } |
1700 | |
1701 | void _scheduleSelectableUpdate() { |
1702 | if (!_scheduledSelectableUpdate) { |
1703 | _scheduledSelectableUpdate = true; |
1704 | void runScheduledTask([Duration? duration]) { |
1705 | if (!_scheduledSelectableUpdate) { |
1706 | return; |
1707 | } |
1708 | _scheduledSelectableUpdate = false; |
1709 | _updateSelectables(); |
1710 | } |
1711 | |
1712 | if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.postFrameCallbacks) { |
1713 | // A new task can be scheduled as a result of running the scheduled task |
1714 | // from another MultiSelectableSelectionContainerDelegate. This can |
1715 | // happen if nesting two SelectionContainers. The selectable can be |
1716 | // safely updated in the same frame in this case. |
1717 | scheduleMicrotask(runScheduledTask); |
1718 | } else { |
1719 | SchedulerBinding.instance.addPostFrameCallback( |
1720 | runScheduledTask, |
1721 | debugLabel: 'SelectionContainer.runScheduledTask' , |
1722 | ); |
1723 | } |
1724 | } |
1725 | } |
1726 | |
1727 | void _updateSelectables() { |
1728 | // Remove offScreen selectable. |
1729 | if (_additions.isNotEmpty) { |
1730 | _flushAdditions(); |
1731 | } |
1732 | didChangeSelectables(); |
1733 | } |
1734 | |
1735 | void _flushAdditions() { |
1736 | final List<Selectable> mergingSelectables = _additions.toList()..sort(compareOrder); |
1737 | final List<Selectable> existingSelectables = selectables; |
1738 | selectables = <Selectable>[]; |
1739 | int mergingIndex = 0; |
1740 | int existingIndex = 0; |
1741 | int selectionStartIndex = currentSelectionStartIndex; |
1742 | int selectionEndIndex = currentSelectionEndIndex; |
1743 | // Merge two sorted lists. |
1744 | while (mergingIndex < mergingSelectables.length || existingIndex < existingSelectables.length) { |
1745 | if (mergingIndex >= mergingSelectables.length || |
1746 | (existingIndex < existingSelectables.length && |
1747 | compareOrder(existingSelectables[existingIndex], mergingSelectables[mergingIndex]) < 0)) { |
1748 | if (existingIndex == currentSelectionStartIndex) { |
1749 | selectionStartIndex = selectables.length; |
1750 | } |
1751 | if (existingIndex == currentSelectionEndIndex) { |
1752 | selectionEndIndex = selectables.length; |
1753 | } |
1754 | selectables.add(existingSelectables[existingIndex]); |
1755 | existingIndex += 1; |
1756 | continue; |
1757 | } |
1758 | |
1759 | // If the merging selectable falls in the selection range, their selection |
1760 | // needs to be updated. |
1761 | final Selectable mergingSelectable = mergingSelectables[mergingIndex]; |
1762 | if (existingIndex < max(currentSelectionStartIndex, currentSelectionEndIndex) && |
1763 | existingIndex > min(currentSelectionStartIndex, currentSelectionEndIndex)) { |
1764 | ensureChildUpdated(mergingSelectable); |
1765 | } |
1766 | mergingSelectable.addListener(_handleSelectableGeometryChange); |
1767 | selectables.add(mergingSelectable); |
1768 | mergingIndex += 1; |
1769 | } |
1770 | assert(mergingIndex == mergingSelectables.length && |
1771 | existingIndex == existingSelectables.length && |
1772 | selectables.length == existingIndex + mergingIndex); |
1773 | assert(selectionStartIndex >= -1 || selectionStartIndex < selectables.length); |
1774 | assert(selectionEndIndex >= -1 || selectionEndIndex < selectables.length); |
1775 | // selection indices should not be set to -1 unless they originally were. |
1776 | assert((currentSelectionStartIndex == -1) == (selectionStartIndex == -1)); |
1777 | assert((currentSelectionEndIndex == -1) == (selectionEndIndex == -1)); |
1778 | currentSelectionEndIndex = selectionEndIndex; |
1779 | currentSelectionStartIndex = selectionStartIndex; |
1780 | _additions = <Selectable>{}; |
1781 | } |
1782 | |
1783 | void _removeSelectable(Selectable selectable) { |
1784 | assert(selectables.contains(selectable), 'The selectable is not in this registrar.' ); |
1785 | final int index = selectables.indexOf(selectable); |
1786 | selectables.removeAt(index); |
1787 | if (index <= currentSelectionEndIndex) { |
1788 | currentSelectionEndIndex -= 1; |
1789 | } |
1790 | if (index <= currentSelectionStartIndex) { |
1791 | currentSelectionStartIndex -= 1; |
1792 | } |
1793 | selectable.removeListener(_handleSelectableGeometryChange); |
1794 | } |
1795 | |
1796 | /// Called when this delegate finishes updating the selectables. |
1797 | @protected |
1798 | @mustCallSuper |
1799 | void didChangeSelectables() { |
1800 | _updateSelectionGeometry(); |
1801 | } |
1802 | |
1803 | @override |
1804 | SelectionGeometry get value => _selectionGeometry; |
1805 | SelectionGeometry _selectionGeometry = const SelectionGeometry( |
1806 | hasContent: false, |
1807 | status: SelectionStatus.none, |
1808 | ); |
1809 | |
1810 | /// Updates the [value] in this class and notifies listeners if necessary. |
1811 | void _updateSelectionGeometry() { |
1812 | final SelectionGeometry newValue = getSelectionGeometry(); |
1813 | if (_selectionGeometry != newValue) { |
1814 | _selectionGeometry = newValue; |
1815 | notifyListeners(); |
1816 | } |
1817 | _updateHandleLayersAndOwners(); |
1818 | } |
1819 | |
1820 | Rect _getBoundingBox(Selectable selectable) { |
1821 | Rect result = selectable.boundingBoxes.first; |
1822 | for (int index = 1; index < selectable.boundingBoxes.length; index += 1) { |
1823 | result = result.expandToInclude(selectable.boundingBoxes[index]); |
1824 | } |
1825 | return result; |
1826 | } |
1827 | |
1828 | /// The compare function this delegate used for determining the selection |
1829 | /// order of the selectables. |
1830 | /// |
1831 | /// Defaults to screen order. |
1832 | @protected |
1833 | Comparator<Selectable> get compareOrder => _compareScreenOrder; |
1834 | |
1835 | int _compareScreenOrder(Selectable a, Selectable b) { |
1836 | final Rect rectA = MatrixUtils.transformRect( |
1837 | a.getTransformTo(null), |
1838 | _getBoundingBox(a), |
1839 | ); |
1840 | final Rect rectB = MatrixUtils.transformRect( |
1841 | b.getTransformTo(null), |
1842 | _getBoundingBox(b), |
1843 | ); |
1844 | final int result = _compareVertically(rectA, rectB); |
1845 | if (result != 0) { |
1846 | return result; |
1847 | } |
1848 | return _compareHorizontally(rectA, rectB); |
1849 | } |
1850 | |
1851 | /// Compares two rectangles in the screen order solely by their vertical |
1852 | /// positions. |
1853 | /// |
1854 | /// Returns positive if a is lower, negative if a is higher, 0 if their |
1855 | /// order can't be determine solely by their vertical position. |
1856 | static int _compareVertically(Rect a, Rect b) { |
1857 | // The rectangles overlap so defer to horizontal comparison. |
1858 | if ((a.top - b.top < _kSelectableVerticalComparingThreshold && a.bottom - b.bottom > - _kSelectableVerticalComparingThreshold) || |
1859 | (b.top - a.top < _kSelectableVerticalComparingThreshold && b.bottom - a.bottom > - _kSelectableVerticalComparingThreshold)) { |
1860 | return 0; |
1861 | } |
1862 | if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) { |
1863 | return a.top > b.top ? 1 : -1; |
1864 | } |
1865 | return a.bottom > b.bottom ? 1 : -1; |
1866 | } |
1867 | |
1868 | /// Compares two rectangles in the screen order by their horizontal positions |
1869 | /// assuming one of the rectangles enclose the other rect vertically. |
1870 | /// |
1871 | /// Returns positive if a is lower, negative if a is higher. |
1872 | static int _compareHorizontally(Rect a, Rect b) { |
1873 | // a encloses b. |
1874 | if (a.left - b.left < precisionErrorTolerance && a.right - b.right > - precisionErrorTolerance) { |
1875 | return -1; |
1876 | } |
1877 | // b encloses a. |
1878 | if (b.left - a.left < precisionErrorTolerance && b.right - a.right > - precisionErrorTolerance) { |
1879 | return 1; |
1880 | } |
1881 | if ((a.left - b.left).abs() > precisionErrorTolerance) { |
1882 | return a.left > b.left ? 1 : -1; |
1883 | } |
1884 | return a.right > b.right ? 1 : -1; |
1885 | } |
1886 | |
1887 | void _handleSelectableGeometryChange() { |
1888 | // Geometries of selectable children may change multiple times when handling |
1889 | // selection events. Ignore these updates since the selection geometry of |
1890 | // this delegate will be updated after handling the selection events. |
1891 | if (_isHandlingSelectionEvent) { |
1892 | return; |
1893 | } |
1894 | _updateSelectionGeometry(); |
1895 | } |
1896 | |
1897 | /// Gets the combined selection geometry for child selectables. |
1898 | @protected |
1899 | SelectionGeometry getSelectionGeometry() { |
1900 | if (currentSelectionEndIndex == -1 || |
1901 | currentSelectionStartIndex == -1 || |
1902 | selectables.isEmpty) { |
1903 | // There is no valid selection. |
1904 | return SelectionGeometry( |
1905 | status: SelectionStatus.none, |
1906 | hasContent: selectables.isNotEmpty, |
1907 | ); |
1908 | } |
1909 | |
1910 | if (!_extendSelectionInProgress) { |
1911 | currentSelectionStartIndex = _adjustSelectionIndexBasedOnSelectionGeometry( |
1912 | currentSelectionStartIndex, |
1913 | currentSelectionEndIndex, |
1914 | ); |
1915 | currentSelectionEndIndex = _adjustSelectionIndexBasedOnSelectionGeometry( |
1916 | currentSelectionEndIndex, |
1917 | currentSelectionStartIndex, |
1918 | ); |
1919 | } |
1920 | |
1921 | // Need to find the non-null start selection point. |
1922 | SelectionGeometry startGeometry = selectables[currentSelectionStartIndex].value; |
1923 | final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex; |
1924 | int startIndexWalker = currentSelectionStartIndex; |
1925 | while (startIndexWalker != currentSelectionEndIndex && startGeometry.startSelectionPoint == null) { |
1926 | startIndexWalker += forwardSelection ? 1 : -1; |
1927 | startGeometry = selectables[startIndexWalker].value; |
1928 | } |
1929 | |
1930 | SelectionPoint? startPoint; |
1931 | if (startGeometry.startSelectionPoint != null) { |
1932 | final Matrix4 startTransform = getTransformFrom(selectables[startIndexWalker]); |
1933 | final Offset start = MatrixUtils.transformPoint(startTransform, startGeometry.startSelectionPoint!.localPosition); |
1934 | // It can be NaN if it is detached or off-screen. |
1935 | if (start.isFinite) { |
1936 | startPoint = SelectionPoint( |
1937 | localPosition: start, |
1938 | lineHeight: startGeometry.startSelectionPoint!.lineHeight, |
1939 | handleType: startGeometry.startSelectionPoint!.handleType, |
1940 | ); |
1941 | } |
1942 | } |
1943 | |
1944 | // Need to find the non-null end selection point. |
1945 | SelectionGeometry endGeometry = selectables[currentSelectionEndIndex].value; |
1946 | int endIndexWalker = currentSelectionEndIndex; |
1947 | while (endIndexWalker != currentSelectionStartIndex && endGeometry.endSelectionPoint == null) { |
1948 | endIndexWalker += forwardSelection ? -1 : 1; |
1949 | endGeometry = selectables[endIndexWalker].value; |
1950 | } |
1951 | SelectionPoint? endPoint; |
1952 | if (endGeometry.endSelectionPoint != null) { |
1953 | final Matrix4 endTransform = getTransformFrom(selectables[endIndexWalker]); |
1954 | final Offset end = MatrixUtils.transformPoint(endTransform, endGeometry.endSelectionPoint!.localPosition); |
1955 | // It can be NaN if it is detached or off-screen. |
1956 | if (end.isFinite) { |
1957 | endPoint = SelectionPoint( |
1958 | localPosition: end, |
1959 | lineHeight: endGeometry.endSelectionPoint!.lineHeight, |
1960 | handleType: endGeometry.endSelectionPoint!.handleType, |
1961 | ); |
1962 | } |
1963 | } |
1964 | |
1965 | // Need to collect selection rects from selectables ranging from the |
1966 | // currentSelectionStartIndex to the currentSelectionEndIndex. |
1967 | final List<Rect> selectionRects = <Rect>[]; |
1968 | final Rect? drawableArea = hasSize ? Rect |
1969 | .fromLTWH(0, 0, containerSize.width, containerSize.height) : null; |
1970 | for (int index = currentSelectionStartIndex; index <= currentSelectionEndIndex; index++) { |
1971 | final List<Rect> currSelectableSelectionRects = selectables[index].value.selectionRects; |
1972 | final List<Rect> selectionRectsWithinDrawableArea = currSelectableSelectionRects.map((Rect selectionRect) { |
1973 | final Matrix4 transform = getTransformFrom(selectables[index]); |
1974 | final Rect localRect = MatrixUtils.transformRect(transform, selectionRect); |
1975 | if (drawableArea != null) { |
1976 | return drawableArea.intersect(localRect); |
1977 | } |
1978 | return localRect; |
1979 | }).where((Rect selectionRect) { |
1980 | return selectionRect.isFinite && !selectionRect.isEmpty; |
1981 | }).toList(); |
1982 | selectionRects.addAll(selectionRectsWithinDrawableArea); |
1983 | } |
1984 | |
1985 | return SelectionGeometry( |
1986 | startSelectionPoint: startPoint, |
1987 | endSelectionPoint: endPoint, |
1988 | selectionRects: selectionRects, |
1989 | status: startGeometry != endGeometry |
1990 | ? SelectionStatus.uncollapsed |
1991 | : startGeometry.status, |
1992 | // Would have at least one selectable child. |
1993 | hasContent: true, |
1994 | ); |
1995 | } |
1996 | |
1997 | // The currentSelectionStartIndex or currentSelectionEndIndex may not be |
1998 | // the current index that contains selection edges. This can happen if the |
1999 | // selection edge is in between two selectables. One of the selectable will |
2000 | // have its selection collapsed at the index 0 or contentLength depends on |
2001 | // whether the selection is reversed or not. The current selection index can |
2002 | // be point to either one. |
2003 | // |
2004 | // This method adjusts the index to point to selectable with valid selection. |
2005 | int _adjustSelectionIndexBasedOnSelectionGeometry(int currentIndex, int towardIndex) { |
2006 | final bool forward = towardIndex > currentIndex; |
2007 | while (currentIndex != towardIndex && |
2008 | selectables[currentIndex].value.status != SelectionStatus.uncollapsed) { |
2009 | currentIndex += forward ? 1 : -1; |
2010 | } |
2011 | return currentIndex; |
2012 | } |
2013 | |
2014 | @override |
2015 | void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) { |
2016 | if (_startHandleLayer == startHandle && _endHandleLayer == endHandle) { |
2017 | return; |
2018 | } |
2019 | _startHandleLayer = startHandle; |
2020 | _endHandleLayer = endHandle; |
2021 | _updateHandleLayersAndOwners(); |
2022 | } |
2023 | |
2024 | /// Pushes both handle layers to the selectables that contain selection edges. |
2025 | /// |
2026 | /// This method needs to be called every time the selectables that contain the |
2027 | /// selection edges change, i.e. [currentSelectionStartIndex] or |
2028 | /// [currentSelectionEndIndex] changes. Otherwise, the handle may be painted |
2029 | /// in the wrong place. |
2030 | void _updateHandleLayersAndOwners() { |
2031 | LayerLink? effectiveStartHandle = _startHandleLayer; |
2032 | LayerLink? effectiveEndHandle = _endHandleLayer; |
2033 | if (effectiveStartHandle != null || effectiveEndHandle != null) { |
2034 | final Rect? drawableArea = hasSize ? Rect |
2035 | .fromLTWH(0, 0, containerSize.width, containerSize.height) |
2036 | .inflate(_kSelectionHandleDrawableAreaPadding) : null; |
2037 | final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || !drawableArea.contains(value.startSelectionPoint!.localPosition); |
2038 | final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null|| !drawableArea.contains(value.endSelectionPoint!.localPosition); |
2039 | effectiveStartHandle = hideStartHandle ? null : _startHandleLayer; |
2040 | effectiveEndHandle = hideEndHandle ? null : _endHandleLayer; |
2041 | } |
2042 | if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { |
2043 | // No valid selection. |
2044 | if (_startHandleLayerOwner != null) { |
2045 | _startHandleLayerOwner!.pushHandleLayers(null, null); |
2046 | _startHandleLayerOwner = null; |
2047 | } |
2048 | if (_endHandleLayerOwner != null) { |
2049 | _endHandleLayerOwner!.pushHandleLayers(null, null); |
2050 | _endHandleLayerOwner = null; |
2051 | } |
2052 | return; |
2053 | } |
2054 | |
2055 | if (selectables[currentSelectionStartIndex] != _startHandleLayerOwner) { |
2056 | _startHandleLayerOwner?.pushHandleLayers(null, null); |
2057 | } |
2058 | if (selectables[currentSelectionEndIndex] != _endHandleLayerOwner) { |
2059 | _endHandleLayerOwner?.pushHandleLayers(null, null); |
2060 | } |
2061 | |
2062 | _startHandleLayerOwner = selectables[currentSelectionStartIndex]; |
2063 | |
2064 | if (currentSelectionStartIndex == currentSelectionEndIndex) { |
2065 | // Selection edges is on the same selectable. |
2066 | _endHandleLayerOwner = _startHandleLayerOwner; |
2067 | _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, effectiveEndHandle); |
2068 | return; |
2069 | } |
2070 | |
2071 | _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, null); |
2072 | _endHandleLayerOwner = selectables[currentSelectionEndIndex]; |
2073 | _endHandleLayerOwner!.pushHandleLayers(null, effectiveEndHandle); |
2074 | } |
2075 | |
2076 | /// Copies the selected contents of all selectables. |
2077 | @override |
2078 | SelectedContent? getSelectedContent() { |
2079 | final List<SelectedContent> selections = <SelectedContent>[]; |
2080 | for (final Selectable selectable in selectables) { |
2081 | final SelectedContent? data = selectable.getSelectedContent(); |
2082 | if (data != null) { |
2083 | selections.add(data); |
2084 | } |
2085 | } |
2086 | if (selections.isEmpty) { |
2087 | return null; |
2088 | } |
2089 | final StringBuffer buffer = StringBuffer(); |
2090 | for (final SelectedContent selection in selections) { |
2091 | buffer.write(selection.plainText); |
2092 | } |
2093 | return SelectedContent( |
2094 | plainText: buffer.toString(), |
2095 | ); |
2096 | } |
2097 | |
2098 | // Clears the selection on all selectables not in the range of |
2099 | // currentSelectionStartIndex..currentSelectionEndIndex. |
2100 | // |
2101 | // If one of the edges does not exist, then this method will clear the selection |
2102 | // in all selectables except the existing edge. |
2103 | // |
2104 | // If neither of the edges exist this method immediately returns. |
2105 | void _flushInactiveSelections() { |
2106 | if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) { |
2107 | return; |
2108 | } |
2109 | if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { |
2110 | final int skipIndex = currentSelectionStartIndex == -1 ? currentSelectionEndIndex : currentSelectionStartIndex; |
2111 | selectables |
2112 | .where((Selectable target) => target != selectables[skipIndex]) |
2113 | .forEach((Selectable target) => dispatchSelectionEventToChild(target, const ClearSelectionEvent())); |
2114 | return; |
2115 | } |
2116 | final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex); |
2117 | final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex); |
2118 | for (int index = 0; index < selectables.length; index += 1) { |
2119 | if (index >= skipStart && index <= skipEnd) { |
2120 | continue; |
2121 | } |
2122 | dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent()); |
2123 | } |
2124 | } |
2125 | |
2126 | /// Selects all contents of all selectables. |
2127 | @protected |
2128 | SelectionResult handleSelectAll(SelectAllSelectionEvent event) { |
2129 | for (final Selectable selectable in selectables) { |
2130 | dispatchSelectionEventToChild(selectable, event); |
2131 | } |
2132 | currentSelectionStartIndex = 0; |
2133 | currentSelectionEndIndex = selectables.length - 1; |
2134 | return SelectionResult.none; |
2135 | } |
2136 | |
2137 | /// Selects a word in a selectable at the location |
2138 | /// [SelectWordSelectionEvent.globalPosition]. |
2139 | @protected |
2140 | SelectionResult handleSelectWord(SelectWordSelectionEvent event) { |
2141 | SelectionResult? lastSelectionResult; |
2142 | for (int index = 0; index < selectables.length; index += 1) { |
2143 | bool globalRectsContainsPosition = false; |
2144 | if (selectables[index].boundingBoxes.isNotEmpty) { |
2145 | for (final Rect rect in selectables[index].boundingBoxes) { |
2146 | final Rect globalRect = MatrixUtils.transformRect(selectables[index].getTransformTo(null), rect); |
2147 | if (globalRect.contains(event.globalPosition)) { |
2148 | globalRectsContainsPosition = true; |
2149 | break; |
2150 | } |
2151 | } |
2152 | } |
2153 | if (globalRectsContainsPosition) { |
2154 | final SelectionGeometry existingGeometry = selectables[index].value; |
2155 | lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event); |
2156 | if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) { |
2157 | return SelectionResult.next; |
2158 | } |
2159 | if (lastSelectionResult == SelectionResult.next) { |
2160 | continue; |
2161 | } |
2162 | if (index == 0 && lastSelectionResult == SelectionResult.previous) { |
2163 | return SelectionResult.previous; |
2164 | } |
2165 | if (selectables[index].value != existingGeometry) { |
2166 | // Geometry has changed as a result of select word, need to clear the |
2167 | // selection of other selectables to keep selection in sync. |
2168 | selectables |
2169 | .where((Selectable target) => target != selectables[index]) |
2170 | .forEach((Selectable target) => dispatchSelectionEventToChild(target, const ClearSelectionEvent())); |
2171 | currentSelectionStartIndex = currentSelectionEndIndex = index; |
2172 | } |
2173 | return SelectionResult.end; |
2174 | } else { |
2175 | if (lastSelectionResult == SelectionResult.next) { |
2176 | currentSelectionStartIndex = currentSelectionEndIndex = index - 1; |
2177 | return SelectionResult.end; |
2178 | } |
2179 | } |
2180 | } |
2181 | assert(lastSelectionResult == null); |
2182 | return SelectionResult.end; |
2183 | } |
2184 | |
2185 | /// Removes the selection of all selectables this delegate manages. |
2186 | @protected |
2187 | SelectionResult handleClearSelection(ClearSelectionEvent event) { |
2188 | for (final Selectable selectable in selectables) { |
2189 | dispatchSelectionEventToChild(selectable, event); |
2190 | } |
2191 | currentSelectionEndIndex = -1; |
2192 | currentSelectionStartIndex = -1; |
2193 | return SelectionResult.none; |
2194 | } |
2195 | |
2196 | /// Extend current selection in a certain text granularity. |
2197 | @protected |
2198 | SelectionResult handleGranularlyExtendSelection(GranularlyExtendSelectionEvent event) { |
2199 | assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1)); |
2200 | if (currentSelectionStartIndex == -1) { |
2201 | if (event.forward) { |
2202 | currentSelectionStartIndex = currentSelectionEndIndex = 0; |
2203 | } else { |
2204 | currentSelectionStartIndex = currentSelectionEndIndex = selectables.length; |
2205 | } |
2206 | } |
2207 | int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex; |
2208 | SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event); |
2209 | if (event.forward) { |
2210 | assert(result != SelectionResult.previous); |
2211 | while (targetIndex < selectables.length - 1 && result == SelectionResult.next) { |
2212 | targetIndex += 1; |
2213 | result = dispatchSelectionEventToChild(selectables[targetIndex], event); |
2214 | assert(result != SelectionResult.previous); |
2215 | } |
2216 | } else { |
2217 | assert(result != SelectionResult.next); |
2218 | while (targetIndex > 0 && result == SelectionResult.previous) { |
2219 | targetIndex -= 1; |
2220 | result = dispatchSelectionEventToChild(selectables[targetIndex], event); |
2221 | assert(result != SelectionResult.next); |
2222 | } |
2223 | } |
2224 | if (event.isEnd) { |
2225 | currentSelectionEndIndex = targetIndex; |
2226 | } else { |
2227 | currentSelectionStartIndex = targetIndex; |
2228 | } |
2229 | return result; |
2230 | } |
2231 | |
2232 | /// Extend current selection in a certain text granularity. |
2233 | @protected |
2234 | SelectionResult handleDirectionallyExtendSelection(DirectionallyExtendSelectionEvent event) { |
2235 | assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1)); |
2236 | if (currentSelectionStartIndex == -1) { |
2237 | switch (event.direction) { |
2238 | case SelectionExtendDirection.previousLine: |
2239 | case SelectionExtendDirection.backward: |
2240 | currentSelectionStartIndex = currentSelectionEndIndex = selectables.length; |
2241 | case SelectionExtendDirection.nextLine: |
2242 | case SelectionExtendDirection.forward: |
2243 | currentSelectionStartIndex = currentSelectionEndIndex = 0; |
2244 | } |
2245 | } |
2246 | int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex; |
2247 | SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event); |
2248 | switch (event.direction) { |
2249 | case SelectionExtendDirection.previousLine: |
2250 | assert(result == SelectionResult.end || result == SelectionResult.previous); |
2251 | if (result == SelectionResult.previous) { |
2252 | if (targetIndex > 0) { |
2253 | targetIndex -= 1; |
2254 | result = dispatchSelectionEventToChild( |
2255 | selectables[targetIndex], |
2256 | event.copyWith(direction: SelectionExtendDirection.backward), |
2257 | ); |
2258 | assert(result == SelectionResult.end); |
2259 | } |
2260 | } |
2261 | case SelectionExtendDirection.nextLine: |
2262 | assert(result == SelectionResult.end || result == SelectionResult.next); |
2263 | if (result == SelectionResult.next) { |
2264 | if (targetIndex < selectables.length - 1) { |
2265 | targetIndex += 1; |
2266 | result = dispatchSelectionEventToChild( |
2267 | selectables[targetIndex], |
2268 | event.copyWith(direction: SelectionExtendDirection.forward), |
2269 | ); |
2270 | assert(result == SelectionResult.end); |
2271 | } |
2272 | } |
2273 | case SelectionExtendDirection.forward: |
2274 | case SelectionExtendDirection.backward: |
2275 | assert(result == SelectionResult.end); |
2276 | } |
2277 | if (event.isEnd) { |
2278 | currentSelectionEndIndex = targetIndex; |
2279 | } else { |
2280 | currentSelectionStartIndex = targetIndex; |
2281 | } |
2282 | return result; |
2283 | } |
2284 | |
2285 | /// Updates the selection edges. |
2286 | @protected |
2287 | SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { |
2288 | if (event.type == SelectionEventType.endEdgeUpdate) { |
2289 | return currentSelectionEndIndex == -1 ? _initSelection(event, isEnd: true) : _adjustSelection(event, isEnd: true); |
2290 | } |
2291 | return currentSelectionStartIndex == -1 ? _initSelection(event, isEnd: false) : _adjustSelection(event, isEnd: false); |
2292 | } |
2293 | |
2294 | @override |
2295 | SelectionResult dispatchSelectionEvent(SelectionEvent event) { |
2296 | final bool selectionWillbeInProgress = event is! ClearSelectionEvent; |
2297 | if (!_selectionInProgress && selectionWillbeInProgress) { |
2298 | // Sort the selectable every time a selection start. |
2299 | selectables.sort(compareOrder); |
2300 | } |
2301 | _selectionInProgress = selectionWillbeInProgress; |
2302 | _isHandlingSelectionEvent = true; |
2303 | late SelectionResult result; |
2304 | switch (event.type) { |
2305 | case SelectionEventType.startEdgeUpdate: |
2306 | case SelectionEventType.endEdgeUpdate: |
2307 | _extendSelectionInProgress = false; |
2308 | result = handleSelectionEdgeUpdate(event as SelectionEdgeUpdateEvent); |
2309 | case SelectionEventType.clear: |
2310 | _extendSelectionInProgress = false; |
2311 | result = handleClearSelection(event as ClearSelectionEvent); |
2312 | case SelectionEventType.selectAll: |
2313 | _extendSelectionInProgress = false; |
2314 | result = handleSelectAll(event as SelectAllSelectionEvent); |
2315 | case SelectionEventType.selectWord: |
2316 | _extendSelectionInProgress = false; |
2317 | result = handleSelectWord(event as SelectWordSelectionEvent); |
2318 | case SelectionEventType.granularlyExtendSelection: |
2319 | _extendSelectionInProgress = true; |
2320 | result = handleGranularlyExtendSelection(event as GranularlyExtendSelectionEvent); |
2321 | case SelectionEventType.directionallyExtendSelection: |
2322 | _extendSelectionInProgress = true; |
2323 | result = handleDirectionallyExtendSelection(event as DirectionallyExtendSelectionEvent); |
2324 | } |
2325 | _isHandlingSelectionEvent = false; |
2326 | _updateSelectionGeometry(); |
2327 | return result; |
2328 | } |
2329 | |
2330 | @override |
2331 | void dispose() { |
2332 | for (final Selectable selectable in selectables) { |
2333 | selectable.removeListener(_handleSelectableGeometryChange); |
2334 | } |
2335 | selectables = const <Selectable>[]; |
2336 | _scheduledSelectableUpdate = false; |
2337 | super.dispose(); |
2338 | } |
2339 | |
2340 | /// Ensures the selectable child has received up to date selection event. |
2341 | /// |
2342 | /// This method is called when a new [Selectable] is added to the delegate, |
2343 | /// and its screen location falls into the previous selection. |
2344 | /// |
2345 | /// Subclasses are responsible for updating the selection of this newly added |
2346 | /// [Selectable]. |
2347 | @protected |
2348 | void ensureChildUpdated(Selectable selectable); |
2349 | |
2350 | /// Dispatches a selection event to a specific selectable. |
2351 | /// |
2352 | /// Override this method if subclasses need to generate additional events or |
2353 | /// treatments prior to sending the selection events. |
2354 | @protected |
2355 | SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) { |
2356 | return selectable.dispatchSelectionEvent(event); |
2357 | } |
2358 | |
2359 | /// Initializes the selection of the selectable children. |
2360 | /// |
2361 | /// The goal is to find the selectable child that contains the selection edge. |
2362 | /// Returns [SelectionResult.end] if the selection edge ends on any of the |
2363 | /// children. Otherwise, it returns [SelectionResult.previous] if the selection |
2364 | /// does not reach any of its children. Returns [SelectionResult.next] |
2365 | /// if the selection reaches the end of its children. |
2366 | /// |
2367 | /// Ideally, this method should only be called twice at the beginning of the |
2368 | /// drag selection, once for start edge update event, once for end edge update |
2369 | /// event. |
2370 | SelectionResult _initSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) { |
2371 | assert((isEnd && currentSelectionEndIndex == -1) || (!isEnd && currentSelectionStartIndex == -1)); |
2372 | int newIndex = -1; |
2373 | bool hasFoundEdgeIndex = false; |
2374 | SelectionResult? result; |
2375 | for (int index = 0; index < selectables.length && !hasFoundEdgeIndex; index += 1) { |
2376 | final Selectable child = selectables[index]; |
2377 | final SelectionResult childResult = dispatchSelectionEventToChild(child, event); |
2378 | switch (childResult) { |
2379 | case SelectionResult.next: |
2380 | case SelectionResult.none: |
2381 | newIndex = index; |
2382 | case SelectionResult.end: |
2383 | newIndex = index; |
2384 | result = SelectionResult.end; |
2385 | hasFoundEdgeIndex = true; |
2386 | case SelectionResult.previous: |
2387 | hasFoundEdgeIndex = true; |
2388 | if (index == 0) { |
2389 | newIndex = 0; |
2390 | result = SelectionResult.previous; |
2391 | } |
2392 | result ??= SelectionResult.end; |
2393 | case SelectionResult.pending: |
2394 | newIndex = index; |
2395 | result = SelectionResult.pending; |
2396 | hasFoundEdgeIndex = true; |
2397 | } |
2398 | } |
2399 | |
2400 | if (newIndex == -1) { |
2401 | assert(selectables.isEmpty); |
2402 | return SelectionResult.none; |
2403 | } |
2404 | if (isEnd) { |
2405 | currentSelectionEndIndex = newIndex; |
2406 | } else { |
2407 | currentSelectionStartIndex = newIndex; |
2408 | } |
2409 | _flushInactiveSelections(); |
2410 | // The result can only be null if the loop went through the entire list |
2411 | // without any of the selection returned end or previous. In this case, the |
2412 | // caller of this method needs to find the next selectable in their list. |
2413 | return result ?? SelectionResult.next; |
2414 | } |
2415 | |
2416 | /// Adjusts the selection based on the drag selection update event if there |
2417 | /// is already a selectable child that contains the selection edge. |
2418 | /// |
2419 | /// This method starts by sending the selection event to the current |
2420 | /// selectable that contains the selection edge, and finds forward or backward |
2421 | /// if that selectable no longer contains the selection edge. |
2422 | SelectionResult _adjustSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) { |
2423 | assert(() { |
2424 | if (isEnd) { |
2425 | assert(currentSelectionEndIndex < selectables.length && currentSelectionEndIndex >= 0); |
2426 | return true; |
2427 | } |
2428 | assert(currentSelectionStartIndex < selectables.length && currentSelectionStartIndex >= 0); |
2429 | return true; |
2430 | }()); |
2431 | SelectionResult? finalResult; |
2432 | // Determines if the edge being adjusted is within the current viewport. |
2433 | // - If so, we begin the search for the new selection edge position at the |
2434 | // currentSelectionEndIndex/currentSelectionStartIndex. |
2435 | // - If not, we attempt to locate the new selection edge starting from |
2436 | // the opposite end. |
2437 | // - If neither edge is in the current viewport, the search for the new |
2438 | // selection edge position begins at 0. |
2439 | // |
2440 | // This can happen when there is a scrollable child and the edge being adjusted |
2441 | // has been scrolled out of view. |
2442 | final bool isCurrentEdgeWithinViewport = isEnd ? _selectionGeometry.endSelectionPoint != null : _selectionGeometry.startSelectionPoint != null; |
2443 | final bool isOppositeEdgeWithinViewport = isEnd ? _selectionGeometry.startSelectionPoint != null : _selectionGeometry.endSelectionPoint != null; |
2444 | int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) { |
2445 | (true, true, true) => currentSelectionEndIndex, |
2446 | (true, true, false) => currentSelectionEndIndex, |
2447 | (true, false, true) => currentSelectionStartIndex, |
2448 | (true, false, false) => 0, |
2449 | (false, true, true) => currentSelectionStartIndex, |
2450 | (false, true, false) => currentSelectionStartIndex, |
2451 | (false, false, true) => currentSelectionEndIndex, |
2452 | (false, false, false) => 0, |
2453 | }; |
2454 | bool? forward; |
2455 | late SelectionResult currentSelectableResult; |
2456 | // This loop sends the selection event to one of the following to determine |
2457 | // the direction of the search. |
2458 | // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge |
2459 | // is in the current viewport. |
2460 | // - The opposite edge index if the current edge is not in the current viewport. |
2461 | // - Index 0 if neither edge is in the current viewport. |
2462 | // |
2463 | // If the result is `SelectionResult.next`, this loop look backward. |
2464 | // Otherwise, it looks forward. |
2465 | // |
2466 | // The terminate condition are: |
2467 | // 1. the selectable returns end, pending, none. |
2468 | // 2. the selectable returns previous when looking forward. |
2469 | // 2. the selectable returns next when looking backward. |
2470 | while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) { |
2471 | currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event); |
2472 | switch (currentSelectableResult) { |
2473 | case SelectionResult.end: |
2474 | case SelectionResult.pending: |
2475 | case SelectionResult.none: |
2476 | finalResult = currentSelectableResult; |
2477 | case SelectionResult.next: |
2478 | if (forward == false) { |
2479 | newIndex += 1; |
2480 | finalResult = SelectionResult.end; |
2481 | } else if (newIndex == selectables.length - 1) { |
2482 | finalResult = currentSelectableResult; |
2483 | } else { |
2484 | forward = true; |
2485 | newIndex += 1; |
2486 | } |
2487 | case SelectionResult.previous: |
2488 | if (forward ?? false) { |
2489 | newIndex -= 1; |
2490 | finalResult = SelectionResult.end; |
2491 | } else if (newIndex == 0) { |
2492 | finalResult = currentSelectableResult; |
2493 | } else { |
2494 | forward = false; |
2495 | newIndex -= 1; |
2496 | } |
2497 | } |
2498 | } |
2499 | if (isEnd) { |
2500 | currentSelectionEndIndex = newIndex; |
2501 | } else { |
2502 | currentSelectionStartIndex = newIndex; |
2503 | } |
2504 | _flushInactiveSelections(); |
2505 | return finalResult!; |
2506 | } |
2507 | } |
2508 | |
2509 | /// Signature for a widget builder that builds a context menu for the given |
2510 | /// [SelectableRegionState]. |
2511 | /// |
2512 | /// See also: |
2513 | /// |
2514 | /// * [EditableTextContextMenuBuilder], which performs the same role for |
2515 | /// [EditableText]. |
2516 | typedef SelectableRegionContextMenuBuilder = Widget Function( |
2517 | BuildContext context, |
2518 | SelectableRegionState selectableRegionState, |
2519 | ); |
2520 | |