Skip to content

Commit efb9368

Browse files
authored
Supports global selection for all devices (flutter#95226)
* Support global selection * addressing comments * add new test * Addressing review comments * update * addressing comments * addressing comments * Addressing comments * fix build
1 parent bd7d34f commit efb9368

28 files changed

+6424
-186
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
// This sample demonstrates how to create a [SelectionContainer] that only
6+
// allows selecting everything or nothing with no partial selection.
7+
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter/rendering.dart';
10+
11+
void main() => runApp(const MyApp());
12+
13+
class MyApp extends StatelessWidget {
14+
const MyApp({super.key});
15+
16+
static const String _title = 'Flutter Code Sample';
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return MaterialApp(
21+
title: _title,
22+
home: SelectionArea(
23+
child: Scaffold(
24+
appBar: AppBar(title: const Text(_title)),
25+
body: Center(
26+
child: SelectionAllOrNoneContainer(
27+
child: Column(
28+
mainAxisAlignment: MainAxisAlignment.center,
29+
children: const <Widget>[
30+
Text('Row 1'),
31+
Text('Row 2'),
32+
Text('Row 3'),
33+
],
34+
),
35+
),
36+
),
37+
),
38+
),
39+
);
40+
}
41+
}
42+
43+
class SelectionAllOrNoneContainer extends StatefulWidget {
44+
const SelectionAllOrNoneContainer({
45+
super.key,
46+
required this.child
47+
});
48+
49+
final Widget child;
50+
51+
@override
52+
State<StatefulWidget> createState() => _SelectionAllOrNoneContainerState();
53+
}
54+
55+
class _SelectionAllOrNoneContainerState extends State<SelectionAllOrNoneContainer> {
56+
final SelectAllOrNoneContainerDelegate delegate = SelectAllOrNoneContainerDelegate();
57+
58+
@override
59+
void dispose() {
60+
delegate.dispose();
61+
super.dispose();
62+
}
63+
64+
@override
65+
Widget build(BuildContext context) {
66+
return SelectionContainer(
67+
delegate: delegate,
68+
child: widget.child,
69+
);
70+
}
71+
}
72+
73+
class SelectAllOrNoneContainerDelegate extends MultiSelectableSelectionContainerDelegate {
74+
Offset? _adjustedStartEdge;
75+
Offset? _adjustedEndEdge;
76+
bool _isSelected = false;
77+
78+
// This method is called when newly added selectable is in the current
79+
// selected range.
80+
@override
81+
void ensureChildUpdated(Selectable selectable) {
82+
if (_isSelected) {
83+
dispatchSelectionEventToChild(selectable, const SelectAllSelectionEvent());
84+
}
85+
}
86+
87+
@override
88+
SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
89+
// Treat select word as select all.
90+
return handleSelectAll(const SelectAllSelectionEvent());
91+
}
92+
93+
@override
94+
SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
95+
final Rect containerRect = Rect.fromLTWH(0, 0, containerSize.width, containerSize.height);
96+
final Matrix4 globalToLocal = getTransformTo(null)..invert();
97+
final Offset localOffset = MatrixUtils.transformPoint(globalToLocal, event.globalPosition);
98+
final Offset adjustOffset = SelectionUtils.adjustDragOffset(containerRect, localOffset);
99+
if (event.type == SelectionEventType.startEdgeUpdate) {
100+
_adjustedStartEdge = adjustOffset;
101+
} else {
102+
_adjustedEndEdge = adjustOffset;
103+
}
104+
// Select all content if the selection rect intercepts with the rect.
105+
if (_adjustedStartEdge != null && _adjustedEndEdge != null) {
106+
final Rect selectionRect = Rect.fromPoints(_adjustedStartEdge!, _adjustedEndEdge!);
107+
if (!selectionRect.intersect(containerRect).isEmpty) {
108+
handleSelectAll(const SelectAllSelectionEvent());
109+
} else {
110+
super.handleClearSelection(const ClearSelectionEvent());
111+
}
112+
} else {
113+
super.handleClearSelection(const ClearSelectionEvent());
114+
}
115+
return SelectionUtils.getResultBasedOnRect(containerRect, localOffset);
116+
}
117+
118+
@override
119+
SelectionResult handleClearSelection(ClearSelectionEvent event) {
120+
_adjustedStartEdge = null;
121+
_adjustedEndEdge = null;
122+
_isSelected = false;
123+
return super.handleClearSelection(event);
124+
}
125+
126+
@override
127+
SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
128+
_isSelected = true;
129+
return super.handleSelectAll(event);
130+
}
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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+
// This sample demonstrates how to create an adapter widget that makes any child
6+
// widget selectable.
7+
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter/rendering.dart';
10+
11+
void main() => runApp(const MyApp());
12+
13+
class MyApp extends StatelessWidget {
14+
const MyApp({super.key});
15+
16+
static const String _title = 'Flutter Code Sample';
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return MaterialApp(
21+
title: _title,
22+
home: SelectionArea(
23+
child: Scaffold(
24+
appBar: AppBar(title: const Text(_title)),
25+
body: Center(
26+
child: Column(
27+
mainAxisAlignment: MainAxisAlignment.center,
28+
children: const <Widget>[
29+
Text('Select this icon', style: TextStyle(fontSize: 30)),
30+
SizedBox(height: 10),
31+
MySelectableAdapter(child: Icon(Icons.key, size: 30)),
32+
],
33+
),
34+
),
35+
),
36+
),
37+
);
38+
}
39+
}
40+
41+
class MySelectableAdapter extends StatelessWidget {
42+
const MySelectableAdapter({super.key, required this.child});
43+
44+
final Widget child;
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
49+
if (registrar == null) {
50+
return child;
51+
}
52+
return MouseRegion(
53+
cursor: SystemMouseCursors.text,
54+
child: _SelectableAdapter(
55+
registrar: registrar,
56+
child: child,
57+
),
58+
);
59+
}
60+
}
61+
62+
class _SelectableAdapter extends SingleChildRenderObjectWidget {
63+
const _SelectableAdapter({
64+
required this.registrar,
65+
required Widget child,
66+
}) : super(child: child);
67+
68+
final SelectionRegistrar registrar;
69+
70+
@override
71+
_RenderSelectableAdapter createRenderObject(BuildContext context) {
72+
return _RenderSelectableAdapter(
73+
DefaultSelectionStyle.of(context).selectionColor!,
74+
registrar,
75+
);
76+
}
77+
78+
@override
79+
void updateRenderObject(BuildContext context, _RenderSelectableAdapter renderObject) {
80+
renderObject
81+
..selectionColor = DefaultSelectionStyle.of(context).selectionColor!
82+
..registrar = registrar;
83+
}
84+
}
85+
86+
class _RenderSelectableAdapter extends RenderProxyBox with Selectable, SelectionRegistrant {
87+
_RenderSelectableAdapter(
88+
Color selectionColor,
89+
SelectionRegistrar registrar,
90+
) : _selectionColor = selectionColor,
91+
_geometry = ValueNotifier<SelectionGeometry>(_noSelection) {
92+
this.registrar = registrar;
93+
_geometry.addListener(markNeedsPaint);
94+
}
95+
96+
static const SelectionGeometry _noSelection = SelectionGeometry(status: SelectionStatus.none, hasContent: true);
97+
final ValueNotifier<SelectionGeometry> _geometry;
98+
99+
Color get selectionColor => _selectionColor;
100+
late Color _selectionColor;
101+
set selectionColor(Color value) {
102+
if (_selectionColor == value) {
103+
return;
104+
}
105+
_selectionColor = value;
106+
markNeedsPaint();
107+
}
108+
109+
// ValueListenable APIs
110+
111+
@override
112+
void addListener(VoidCallback listener) => _geometry.addListener(listener);
113+
114+
@override
115+
void removeListener(VoidCallback listener) => _geometry.removeListener(listener);
116+
117+
@override
118+
SelectionGeometry get value => _geometry.value;
119+
120+
// Selectable APIs.
121+
122+
// Adjust this value to enlarge or shrink the selection highlight.
123+
static const double _padding = 10.0;
124+
Rect _getSelectionHighlightRect() {
125+
return Rect.fromLTWH(
126+
0 - _padding,
127+
0 - _padding,
128+
size.width + _padding * 2,
129+
size.height + _padding * 2
130+
);
131+
}
132+
133+
Offset? _start;
134+
Offset? _end;
135+
void _updateGeometry() {
136+
if (_start == null || _end == null) {
137+
_geometry.value = _noSelection;
138+
return;
139+
}
140+
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height);
141+
final Rect selectionRect = Rect.fromPoints(_start!, _end!);
142+
if (renderObjectRect.intersect(selectionRect).isEmpty) {
143+
_geometry.value = _noSelection;
144+
} else {
145+
final Rect selectionRect = _getSelectionHighlightRect();
146+
final SelectionPoint firstSelectionPoint = SelectionPoint(
147+
localPosition: selectionRect.bottomLeft,
148+
lineHeight: selectionRect.size.height,
149+
handleType: TextSelectionHandleType.left,
150+
);
151+
final SelectionPoint secondSelectionPoint = SelectionPoint(
152+
localPosition: selectionRect.bottomRight,
153+
lineHeight: selectionRect.size.height,
154+
handleType: TextSelectionHandleType.right,
155+
);
156+
final bool isReversed;
157+
if (_start!.dy > _end!.dy) {
158+
isReversed = true;
159+
} else if (_start!.dy < _end!.dy) {
160+
isReversed = false;
161+
} else {
162+
isReversed = _start!.dx > _end!.dx;
163+
}
164+
_geometry.value = SelectionGeometry(
165+
status: SelectionStatus.uncollapsed,
166+
hasContent: true,
167+
startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint,
168+
endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint,
169+
);
170+
}
171+
}
172+
173+
@override
174+
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
175+
SelectionResult result = SelectionResult.none;
176+
switch (event.type) {
177+
case SelectionEventType.startEdgeUpdate:
178+
case SelectionEventType.endEdgeUpdate:
179+
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height);
180+
// Normalize offset in case it is out side of the rect.
181+
final Offset point = globalToLocal((event as SelectionEdgeUpdateEvent).globalPosition);
182+
final Offset adjustedPoint = SelectionUtils.adjustDragOffset(renderObjectRect, point);
183+
if (event.type == SelectionEventType.startEdgeUpdate) {
184+
_start = adjustedPoint;
185+
} else {
186+
_end = adjustedPoint;
187+
}
188+
result = SelectionUtils.getResultBasedOnRect(renderObjectRect, point);
189+
break;
190+
case SelectionEventType.clear:
191+
_start = _end = null;
192+
break;
193+
case SelectionEventType.selectAll:
194+
case SelectionEventType.selectWord:
195+
_start = Offset.zero;
196+
_end = Offset.infinite;
197+
break;
198+
}
199+
_updateGeometry();
200+
return result;
201+
}
202+
203+
// This method is called when users want to copy selected content in this
204+
// widget into clipboard.
205+
@override
206+
SelectedContent? getSelectedContent() {
207+
return value.hasSelection ? const SelectedContent(plainText: 'Custom Text') : null;
208+
}
209+
210+
LayerLink? _startHandle;
211+
LayerLink? _endHandle;
212+
213+
@override
214+
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
215+
if (_startHandle == startHandle && _endHandle == endHandle) {
216+
return;
217+
}
218+
_startHandle = startHandle;
219+
_endHandle = endHandle;
220+
markNeedsPaint();
221+
}
222+
223+
@override
224+
void paint(PaintingContext context, Offset offset) {
225+
super.paint(context, offset);
226+
if (!_geometry.value.hasSelection) {
227+
return;
228+
}
229+
// Draw the selection highlight.
230+
final Paint selectionPaint = Paint()
231+
..style = PaintingStyle.fill
232+
..color = _selectionColor;
233+
context.canvas.drawRect(_getSelectionHighlightRect().shift(offset), selectionPaint);
234+
235+
// Push the layer links if any.
236+
if (_startHandle != null) {
237+
context.pushLayer(
238+
LeaderLayer(
239+
link: _startHandle!,
240+
offset: offset + value.startSelectionPoint!.localPosition,
241+
),
242+
(PaintingContext context, Offset offset) { },
243+
Offset.zero,
244+
);
245+
}
246+
if (_endHandle != null) {
247+
context.pushLayer(
248+
LeaderLayer(
249+
link: _endHandle!,
250+
offset: offset + value.endSelectionPoint!.localPosition,
251+
),
252+
(PaintingContext context, Offset offset) { },
253+
Offset.zero,
254+
);
255+
}
256+
}
257+
258+
@override
259+
void dispose() {
260+
_geometry.dispose();
261+
super.dispose();
262+
}
263+
}

0 commit comments

Comments
 (0)