blob: 0ff8081f4dafdf5a53d2c4f3046abb55534b1fb8 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'framework.dart';
/// A container that handles [SelectionEvent]s for the [Selectable]s in
/// the subtree.
///
/// This widget is useful when one wants to customize selection behaviors for
/// a group of [Selectable]s
///
/// The state of this container is a single selectable and will register
/// itself to the [registrar] if provided. Otherwise, it will register to the
/// [SelectionRegistrar] from the context. Consider using a [SelectionArea]
/// widget to provide a root registrar.
///
/// The containers handle the [SelectionEvent]s from the registered
/// [SelectionRegistrar] and delegate the events to the [delegate].
///
/// This widget uses [SelectionRegistrarScope] to host the [delegate] as the
/// [SelectionRegistrar] for the subtree to collect the [Selectable]s, and
/// [SelectionEvent]s received by this container are sent to the [delegate] using
/// the [SelectionHandler] API of the delegate.
///
/// {@tool dartpad}
/// This sample demonstrates how to create a [SelectionContainer] that only
/// allows selecting everything or nothing with no partial selection.
///
/// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart **
/// {@end-tool}
///
/// See also:
/// * [SelectableRegion], which provides an overview of the selection system.
/// * [SelectionContainer.disabled], which disable selection for a
/// subtree.
class SelectionContainer extends StatefulWidget {
/// Creates a selection container to collect the [Selectable]s in the subtree.
///
/// If [registrar] is not provided, this selection container gets the
/// [SelectionRegistrar] from the context instead.
///
/// The [delegate] and [child] must not be null.
const SelectionContainer({
super.key,
this.registrar,
required SelectionContainerDelegate this.delegate,
required this.child,
}) : assert(delegate != null),
assert(child != null);
/// Creates a selection container that disables selection for the
/// subtree.
///
/// {@tool dartpad}
/// This sample demonstrates how to disable selection for a Text under a
/// SelectionArea.
///
/// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart **
/// {@end-tool}
///
/// The [child] must not be null.
const SelectionContainer.disabled({
super.key,
required this.child,
}) : registrar = null,
delegate = null;
/// The [SelectionRegistrar] this container is registered to.
///
/// If null, this widget gets the [SelectionRegistrar] from the current
/// context.
final SelectionRegistrar? registrar;
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// The delegate for [SelectionEvent]s sent to this selection container.
///
/// The [Selectable]s in the subtree are added or removed from this delegate
/// using [SelectionRegistrar] API.
///
/// This delegate is responsible for updating the selections for the selectables
/// under this widget.
final SelectionContainerDelegate? delegate;
/// Gets the immediate ancestor [SelectionRegistrar] of the [BuildContext].
///
/// If this returns null, either there is no [SelectionContainer] above
/// the [BuildContext] or the immediate [SelectionContainer] is not
/// enabled.
static SelectionRegistrar? maybeOf(BuildContext context) {
final SelectionRegistrarScope? scope = context.dependOnInheritedWidgetOfExactType<SelectionRegistrarScope>();
return scope?.registrar;
}
bool get _disabled => delegate == null;
@override
State<SelectionContainer> createState() => _SelectionContainerState();
}
class _SelectionContainerState extends State<SelectionContainer> with Selectable, SelectionRegistrant {
final Set<VoidCallback> _listeners = <VoidCallback>{};
static const SelectionGeometry _disabledGeometry = SelectionGeometry(
status: SelectionStatus.none,
hasContent: true,
);
@override
void initState() {
super.initState();
if (!widget._disabled) {
widget.delegate!._selectionContainerContext = context;
if (widget.registrar != null) {
registrar = widget.registrar;
}
}
}
@override
void didUpdateWidget(SelectionContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.delegate != widget.delegate) {
if (!oldWidget._disabled) {
oldWidget.delegate!._selectionContainerContext = null;
_listeners.forEach(oldWidget.delegate!.removeListener);
}
if (!widget._disabled) {
widget.delegate!._selectionContainerContext = context;
_listeners.forEach(widget.delegate!.addListener);
}
if (oldWidget.delegate?.value != widget.delegate?.value) {
for (final VoidCallback listener in _listeners) {
listener();
}
}
}
if (widget._disabled) {
registrar = null;
} else if (widget.registrar != null) {
registrar = widget.registrar;
}
assert(!widget._disabled || registrar == null);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (widget.registrar == null && !widget._disabled) {
registrar = SelectionContainer.maybeOf(context);
}
assert(!widget._disabled || registrar == null);
}
@override
void addListener(VoidCallback listener) {
assert(!widget._disabled);
widget.delegate!.addListener(listener);
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
widget.delegate?.removeListener(listener);
_listeners.remove(listener);
}
@override
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
assert(!widget._disabled);
widget.delegate!.pushHandleLayers(startHandle, endHandle);
}
@override
SelectedContent? getSelectedContent() {
assert(!widget._disabled);
return widget.delegate!.getSelectedContent();
}
@override
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
assert(!widget._disabled);
return widget.delegate!.dispatchSelectionEvent(event);
}
@override
SelectionGeometry get value {
if (widget._disabled) {
return _disabledGeometry;
}
return widget.delegate!.value;
}
@override
Matrix4 getTransformTo(RenderObject? ancestor) {
assert(!widget._disabled);
return context.findRenderObject()!.getTransformTo(ancestor);
}
@override
Size get size => (context.findRenderObject()! as RenderBox).size;
@override
void dispose() {
if (!widget._disabled) {
widget.delegate!._selectionContainerContext = null;
_listeners.forEach(widget.delegate!.removeListener);
}
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget._disabled) {
return SelectionRegistrarScope._disabled(child: widget.child);
}
return SelectionRegistrarScope(
registrar: widget.delegate!,
child: widget.child,
);
}
}
/// An inherited widget to host a [SelectionRegistrar] for the subtree.
///
/// Use [SelectionContainer.maybeOf] to get the SelectionRegistrar from
/// a context.
///
/// This widget is automatically created as part of [SelectionContainer] and
/// is generally not used directly, except for disabling selection for a part
/// of subtree. In that case, one can wrap the subtree with
/// [SelectionContainer.disabled].
class SelectionRegistrarScope extends InheritedWidget {
/// Creates a selection registrar scope that host the [registrar].
const SelectionRegistrarScope({
super.key,
required SelectionRegistrar this.registrar,
required super.child,
}) : assert(registrar != null);
/// Creates a selection registrar scope that disables selection for the
/// subtree.
const SelectionRegistrarScope._disabled({
required super.child,
}) : registrar = null;
/// The [SelectionRegistrar] hosted by this widget.
final SelectionRegistrar? registrar;
@override
bool updateShouldNotify(SelectionRegistrarScope oldWidget) {
return oldWidget.registrar != registrar;
}
}
/// A delegate to handle [SelectionEvent]s for a [SelectionContainer].
///
/// This delegate needs to implement [SelectionRegistrar] to register
/// [Selectable]s in the [SelectionContainer] subtree.
abstract class SelectionContainerDelegate implements SelectionHandler, SelectionRegistrar {
BuildContext? _selectionContainerContext;
/// Gets the paint transform from the [Selectable] child to
/// [SelectionContainer] of this delegate.
///
/// Returns a matrix that maps the [Selectable] paint coordinate system to the
/// coordinate system of [SelectionContainer].
///
/// Can only be called after [SelectionContainer] is laid out.
Matrix4 getTransformFrom(Selectable child) {
assert(
_selectionContainerContext?.findRenderObject() != null,
'getTransformFrom cannot be called before SelectionContainer is laid out.',
);
return child.getTransformTo(_selectionContainerContext!.findRenderObject()! as RenderBox);
}
/// Gets the paint transform from the [SelectionContainer] of this delegate to
/// the `ancestor`.
///
/// Returns a matrix that maps the [SelectionContainer] paint coordinate
/// system to the coordinate system of `ancestor`.
///
/// If `ancestor` is null, this method returns a matrix that maps from the
/// local paint coordinate system to the coordinate system of the
/// [PipelineOwner.rootNode].
///
/// Can only be called after [SelectionContainer] is laid out.
Matrix4 getTransformTo(RenderObject? ancestor) {
assert(
_selectionContainerContext?.findRenderObject() != null,
'getTransformTo cannot be called before SelectionContainer is laid out.',
);
final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox;
return box.getTransformTo(ancestor);
}
/// Gets the size of the [SelectionContainer] of this delegate.
///
/// Can only be called after [SelectionContainer] is laid out.
Size get containerSize {
assert(
_selectionContainerContext?.findRenderObject() != null,
'containerSize cannot be called before SelectionContainer is laid out.',
);
final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox;
return box.size;
}
}