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 'package:flutter/rendering.dart'; |
6 | |
7 | import 'framework.dart'; |
8 | |
9 | /// A container that handles [SelectionEvent]s for the [Selectable]s in |
10 | /// the subtree. |
11 | /// |
12 | /// This widget is useful when one wants to customize selection behaviors for |
13 | /// a group of [Selectable]s |
14 | /// |
15 | /// The state of this container is a single selectable and will register |
16 | /// itself to the [registrar] if provided. Otherwise, it will register to the |
17 | /// [SelectionRegistrar] from the context. Consider using a [SelectionArea] |
18 | /// widget to provide a root registrar. |
19 | /// |
20 | /// The containers handle the [SelectionEvent]s from the registered |
21 | /// [SelectionRegistrar] and delegate the events to the [delegate]. |
22 | /// |
23 | /// This widget uses [SelectionRegistrarScope] to host the [delegate] as the |
24 | /// [SelectionRegistrar] for the subtree to collect the [Selectable]s, and |
25 | /// [SelectionEvent]s received by this container are sent to the [delegate] using |
26 | /// the [SelectionHandler] API of the delegate. |
27 | /// |
28 | /// {@tool dartpad} |
29 | /// This sample demonstrates how to create a [SelectionContainer] that only |
30 | /// allows selecting everything or nothing with no partial selection. |
31 | /// |
32 | /// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart ** |
33 | /// {@end-tool} |
34 | /// |
35 | /// See also: |
36 | /// * [SelectableRegion], which provides an overview of the selection system. |
37 | /// * [SelectionContainer.disabled], which disable selection for a |
38 | /// subtree. |
39 | class SelectionContainer extends StatefulWidget { |
40 | /// Creates a selection container to collect the [Selectable]s in the subtree. |
41 | /// |
42 | /// If [registrar] is not provided, this selection container gets the |
43 | /// [SelectionRegistrar] from the context instead. |
44 | const SelectionContainer({ |
45 | super.key, |
46 | this.registrar, |
47 | required SelectionContainerDelegate this.delegate, |
48 | required this.child, |
49 | }); |
50 | |
51 | /// Creates a selection container that disables selection for the |
52 | /// subtree. |
53 | /// |
54 | /// {@tool dartpad} |
55 | /// This sample demonstrates how to disable selection for a Text under a |
56 | /// SelectionArea. |
57 | /// |
58 | /// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** |
59 | /// {@end-tool} |
60 | const SelectionContainer.disabled({ |
61 | super.key, |
62 | required this.child, |
63 | }) : registrar = null, |
64 | delegate = null; |
65 | |
66 | /// The [SelectionRegistrar] this container is registered to. |
67 | /// |
68 | /// If null, this widget gets the [SelectionRegistrar] from the current |
69 | /// context. |
70 | final SelectionRegistrar? registrar; |
71 | |
72 | /// {@macro flutter.widgets.ProxyWidget.child} |
73 | final Widget child; |
74 | |
75 | /// The delegate for [SelectionEvent]s sent to this selection container. |
76 | /// |
77 | /// The [Selectable]s in the subtree are added or removed from this delegate |
78 | /// using [SelectionRegistrar] API. |
79 | /// |
80 | /// This delegate is responsible for updating the selections for the selectables |
81 | /// under this widget. |
82 | final SelectionContainerDelegate? delegate; |
83 | |
84 | /// Gets the immediate ancestor [SelectionRegistrar] of the [BuildContext]. |
85 | /// |
86 | /// If this returns null, either there is no [SelectionContainer] above |
87 | /// the [BuildContext] or the immediate [SelectionContainer] is not |
88 | /// enabled. |
89 | static SelectionRegistrar? maybeOf(BuildContext context) { |
90 | final SelectionRegistrarScope? scope = context.dependOnInheritedWidgetOfExactType<SelectionRegistrarScope>(); |
91 | return scope?.registrar; |
92 | } |
93 | |
94 | bool get _disabled => delegate == null; |
95 | |
96 | @override |
97 | State<SelectionContainer> createState() => _SelectionContainerState(); |
98 | } |
99 | |
100 | class _SelectionContainerState extends State<SelectionContainer> with Selectable, SelectionRegistrant { |
101 | final Set<VoidCallback> _listeners = <VoidCallback>{}; |
102 | |
103 | static const SelectionGeometry _disabledGeometry = SelectionGeometry( |
104 | status: SelectionStatus.none, |
105 | hasContent: true, |
106 | ); |
107 | |
108 | @override |
109 | void initState() { |
110 | super.initState(); |
111 | if (!widget._disabled) { |
112 | widget.delegate!._selectionContainerContext = context; |
113 | if (widget.registrar != null) { |
114 | registrar = widget.registrar; |
115 | } |
116 | } |
117 | } |
118 | |
119 | @override |
120 | void didUpdateWidget(SelectionContainer oldWidget) { |
121 | super.didUpdateWidget(oldWidget); |
122 | if (oldWidget.delegate != widget.delegate) { |
123 | if (!oldWidget._disabled) { |
124 | oldWidget.delegate!._selectionContainerContext = null; |
125 | _listeners.forEach(oldWidget.delegate!.removeListener); |
126 | } |
127 | if (!widget._disabled) { |
128 | widget.delegate!._selectionContainerContext = context; |
129 | _listeners.forEach(widget.delegate!.addListener); |
130 | } |
131 | if (oldWidget.delegate?.value != widget.delegate?.value) { |
132 | // Avoid concurrent modification. |
133 | for (final VoidCallback listener in _listeners.toList(growable: false)) { |
134 | listener(); |
135 | } |
136 | } |
137 | } |
138 | if (widget._disabled) { |
139 | registrar = null; |
140 | } else if (widget.registrar != null) { |
141 | registrar = widget.registrar; |
142 | } |
143 | assert(!widget._disabled || registrar == null); |
144 | } |
145 | |
146 | @override |
147 | void didChangeDependencies() { |
148 | super.didChangeDependencies(); |
149 | if (widget.registrar == null && !widget._disabled) { |
150 | registrar = SelectionContainer.maybeOf(context); |
151 | } |
152 | assert(!widget._disabled || registrar == null); |
153 | } |
154 | |
155 | @override |
156 | void addListener(VoidCallback listener) { |
157 | assert(!widget._disabled); |
158 | widget.delegate!.addListener(listener); |
159 | _listeners.add(listener); |
160 | } |
161 | |
162 | @override |
163 | void removeListener(VoidCallback listener) { |
164 | widget.delegate?.removeListener(listener); |
165 | _listeners.remove(listener); |
166 | } |
167 | |
168 | @override |
169 | void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) { |
170 | assert(!widget._disabled); |
171 | widget.delegate!.pushHandleLayers(startHandle, endHandle); |
172 | } |
173 | |
174 | @override |
175 | SelectedContent? getSelectedContent() { |
176 | assert(!widget._disabled); |
177 | return widget.delegate!.getSelectedContent(); |
178 | } |
179 | |
180 | @override |
181 | SelectionResult dispatchSelectionEvent(SelectionEvent event) { |
182 | assert(!widget._disabled); |
183 | return widget.delegate!.dispatchSelectionEvent(event); |
184 | } |
185 | |
186 | @override |
187 | SelectionGeometry get value { |
188 | if (widget._disabled) { |
189 | return _disabledGeometry; |
190 | } |
191 | return widget.delegate!.value; |
192 | } |
193 | |
194 | @override |
195 | Matrix4 getTransformTo(RenderObject? ancestor) { |
196 | assert(!widget._disabled); |
197 | return context.findRenderObject()!.getTransformTo(ancestor); |
198 | } |
199 | |
200 | @override |
201 | Size get size => (context.findRenderObject()! as RenderBox).size; |
202 | |
203 | @override |
204 | List<Rect> get boundingBoxes => <Rect>[(context.findRenderObject()! as RenderBox).paintBounds]; |
205 | |
206 | @override |
207 | void dispose() { |
208 | if (!widget._disabled) { |
209 | widget.delegate!._selectionContainerContext = null; |
210 | _listeners.forEach(widget.delegate!.removeListener); |
211 | } |
212 | super.dispose(); |
213 | } |
214 | |
215 | @override |
216 | Widget build(BuildContext context) { |
217 | if (widget._disabled) { |
218 | return SelectionRegistrarScope._disabled(child: widget.child); |
219 | } |
220 | return SelectionRegistrarScope( |
221 | registrar: widget.delegate!, |
222 | child: widget.child, |
223 | ); |
224 | } |
225 | } |
226 | |
227 | /// An inherited widget to host a [SelectionRegistrar] for the subtree. |
228 | /// |
229 | /// Use [SelectionContainer.maybeOf] to get the SelectionRegistrar from |
230 | /// a context. |
231 | /// |
232 | /// This widget is automatically created as part of [SelectionContainer] and |
233 | /// is generally not used directly, except for disabling selection for a part |
234 | /// of subtree. In that case, one can wrap the subtree with |
235 | /// [SelectionContainer.disabled]. |
236 | class SelectionRegistrarScope extends InheritedWidget { |
237 | /// Creates a selection registrar scope that host the [registrar]. |
238 | const SelectionRegistrarScope({ |
239 | super.key, |
240 | required SelectionRegistrar this.registrar, |
241 | required super.child, |
242 | }); |
243 | |
244 | /// Creates a selection registrar scope that disables selection for the |
245 | /// subtree. |
246 | const SelectionRegistrarScope._disabled({ |
247 | required super.child, |
248 | }) : registrar = null; |
249 | |
250 | /// The [SelectionRegistrar] hosted by this widget. |
251 | final SelectionRegistrar? registrar; |
252 | |
253 | @override |
254 | bool updateShouldNotify(SelectionRegistrarScope oldWidget) { |
255 | return oldWidget.registrar != registrar; |
256 | } |
257 | } |
258 | |
259 | /// A delegate to handle [SelectionEvent]s for a [SelectionContainer]. |
260 | /// |
261 | /// This delegate needs to implement [SelectionRegistrar] to register |
262 | /// [Selectable]s in the [SelectionContainer] subtree. |
263 | abstract class SelectionContainerDelegate implements SelectionHandler, SelectionRegistrar { |
264 | BuildContext? _selectionContainerContext; |
265 | |
266 | /// Gets the paint transform from the [Selectable] child to |
267 | /// [SelectionContainer] of this delegate. |
268 | /// |
269 | /// Returns a matrix that maps the [Selectable] paint coordinate system to the |
270 | /// coordinate system of [SelectionContainer]. |
271 | /// |
272 | /// Can only be called after [SelectionContainer] is laid out. |
273 | Matrix4 getTransformFrom(Selectable child) { |
274 | assert( |
275 | _selectionContainerContext?.findRenderObject() != null, |
276 | 'getTransformFrom cannot be called before SelectionContainer is laid out.' , |
277 | ); |
278 | return child.getTransformTo(_selectionContainerContext!.findRenderObject()! as RenderBox); |
279 | } |
280 | |
281 | /// Gets the paint transform from the [SelectionContainer] of this delegate to |
282 | /// the `ancestor`. |
283 | /// |
284 | /// Returns a matrix that maps the [SelectionContainer] paint coordinate |
285 | /// system to the coordinate system of `ancestor`. |
286 | /// |
287 | /// If `ancestor` is null, this method returns a matrix that maps from the |
288 | /// local paint coordinate system to the coordinate system of the |
289 | /// [PipelineOwner.rootNode]. |
290 | /// |
291 | /// Can only be called after [SelectionContainer] is laid out. |
292 | Matrix4 getTransformTo(RenderObject? ancestor) { |
293 | assert( |
294 | _selectionContainerContext?.findRenderObject() != null, |
295 | 'getTransformTo cannot be called before SelectionContainer is laid out.' , |
296 | ); |
297 | final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox; |
298 | return box.getTransformTo(ancestor); |
299 | } |
300 | |
301 | /// Whether the [SelectionContainer] has undergone layout and has a size. |
302 | /// |
303 | /// See also: |
304 | /// |
305 | /// * [RenderBox.hasSize], which is used internally by this method. |
306 | bool get hasSize { |
307 | assert( |
308 | _selectionContainerContext?.findRenderObject() != null, |
309 | 'The _selectionContainerContext must have a renderObject, such as after the first build has completed.' , |
310 | ); |
311 | final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox; |
312 | return box.hasSize; |
313 | } |
314 | |
315 | /// Gets the size of the [SelectionContainer] of this delegate. |
316 | /// |
317 | /// Can only be called after [SelectionContainer] is laid out. |
318 | Size get containerSize { |
319 | assert( |
320 | hasSize, |
321 | 'containerSize cannot be called before SelectionContainer is laid out.' , |
322 | ); |
323 | final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox; |
324 | return box.size; |
325 | } |
326 | } |
327 | |