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