forked from flutter/plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Supports global selection for all devices (#95226)
* Support global selection * addressing comments * add new test * Addressing review comments * update * addressing comments * addressing comments * Addressing comments * fix build
- Loading branch information
Showing
28 changed files
with
6,424 additions
and
186 deletions.
There are no files selected for viewing
131 changes: 131 additions & 0 deletions
131
examples/api/lib/material/selection_area/custom_container.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
// 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. | ||
|
||
// This sample demonstrates how to create a [SelectionContainer] that only | ||
// allows selecting everything or nothing with no partial selection. | ||
|
||
import 'package:flutter/material.dart'; | ||
import 'package:flutter/rendering.dart'; | ||
|
||
void main() => runApp(const MyApp()); | ||
|
||
class MyApp extends StatelessWidget { | ||
const MyApp({super.key}); | ||
|
||
static const String _title = 'Flutter Code Sample'; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
title: _title, | ||
home: SelectionArea( | ||
child: Scaffold( | ||
appBar: AppBar(title: const Text(_title)), | ||
body: Center( | ||
child: SelectionAllOrNoneContainer( | ||
child: Column( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
children: const <Widget>[ | ||
Text('Row 1'), | ||
Text('Row 2'), | ||
Text('Row 3'), | ||
], | ||
), | ||
), | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
|
||
class SelectionAllOrNoneContainer extends StatefulWidget { | ||
const SelectionAllOrNoneContainer({ | ||
super.key, | ||
required this.child | ||
}); | ||
|
||
final Widget child; | ||
|
||
@override | ||
State<StatefulWidget> createState() => _SelectionAllOrNoneContainerState(); | ||
} | ||
|
||
class _SelectionAllOrNoneContainerState extends State<SelectionAllOrNoneContainer> { | ||
final SelectAllOrNoneContainerDelegate delegate = SelectAllOrNoneContainerDelegate(); | ||
|
||
@override | ||
void dispose() { | ||
delegate.dispose(); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return SelectionContainer( | ||
delegate: delegate, | ||
child: widget.child, | ||
); | ||
} | ||
} | ||
|
||
class SelectAllOrNoneContainerDelegate extends MultiSelectableSelectionContainerDelegate { | ||
Offset? _adjustedStartEdge; | ||
Offset? _adjustedEndEdge; | ||
bool _isSelected = false; | ||
|
||
// This method is called when newly added selectable is in the current | ||
// selected range. | ||
@override | ||
void ensureChildUpdated(Selectable selectable) { | ||
if (_isSelected) { | ||
dispatchSelectionEventToChild(selectable, const SelectAllSelectionEvent()); | ||
} | ||
} | ||
|
||
@override | ||
SelectionResult handleSelectWord(SelectWordSelectionEvent event) { | ||
// Treat select word as select all. | ||
return handleSelectAll(const SelectAllSelectionEvent()); | ||
} | ||
|
||
@override | ||
SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { | ||
final Rect containerRect = Rect.fromLTWH(0, 0, containerSize.width, containerSize.height); | ||
final Matrix4 globalToLocal = getTransformTo(null)..invert(); | ||
final Offset localOffset = MatrixUtils.transformPoint(globalToLocal, event.globalPosition); | ||
final Offset adjustOffset = SelectionUtils.adjustDragOffset(containerRect, localOffset); | ||
if (event.type == SelectionEventType.startEdgeUpdate) { | ||
_adjustedStartEdge = adjustOffset; | ||
} else { | ||
_adjustedEndEdge = adjustOffset; | ||
} | ||
// Select all content if the selection rect intercepts with the rect. | ||
if (_adjustedStartEdge != null && _adjustedEndEdge != null) { | ||
final Rect selectionRect = Rect.fromPoints(_adjustedStartEdge!, _adjustedEndEdge!); | ||
if (!selectionRect.intersect(containerRect).isEmpty) { | ||
handleSelectAll(const SelectAllSelectionEvent()); | ||
} else { | ||
super.handleClearSelection(const ClearSelectionEvent()); | ||
} | ||
} else { | ||
super.handleClearSelection(const ClearSelectionEvent()); | ||
} | ||
return SelectionUtils.getResultBasedOnRect(containerRect, localOffset); | ||
} | ||
|
||
@override | ||
SelectionResult handleClearSelection(ClearSelectionEvent event) { | ||
_adjustedStartEdge = null; | ||
_adjustedEndEdge = null; | ||
_isSelected = false; | ||
return super.handleClearSelection(event); | ||
} | ||
|
||
@override | ||
SelectionResult handleSelectAll(SelectAllSelectionEvent event) { | ||
_isSelected = true; | ||
return super.handleSelectAll(event); | ||
} | ||
} |
263 changes: 263 additions & 0 deletions
263
examples/api/lib/material/selection_area/custom_selectable.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
// 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. | ||
|
||
// This sample demonstrates how to create an adapter widget that makes any child | ||
// widget selectable. | ||
|
||
import 'package:flutter/material.dart'; | ||
import 'package:flutter/rendering.dart'; | ||
|
||
void main() => runApp(const MyApp()); | ||
|
||
class MyApp extends StatelessWidget { | ||
const MyApp({super.key}); | ||
|
||
static const String _title = 'Flutter Code Sample'; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
title: _title, | ||
home: SelectionArea( | ||
child: Scaffold( | ||
appBar: AppBar(title: const Text(_title)), | ||
body: Center( | ||
child: Column( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
children: const <Widget>[ | ||
Text('Select this icon', style: TextStyle(fontSize: 30)), | ||
SizedBox(height: 10), | ||
MySelectableAdapter(child: Icon(Icons.key, size: 30)), | ||
], | ||
), | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
|
||
class MySelectableAdapter extends StatelessWidget { | ||
const MySelectableAdapter({super.key, required this.child}); | ||
|
||
final Widget child; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); | ||
if (registrar == null) { | ||
return child; | ||
} | ||
return MouseRegion( | ||
cursor: SystemMouseCursors.text, | ||
child: _SelectableAdapter( | ||
registrar: registrar, | ||
child: child, | ||
), | ||
); | ||
} | ||
} | ||
|
||
class _SelectableAdapter extends SingleChildRenderObjectWidget { | ||
const _SelectableAdapter({ | ||
required this.registrar, | ||
required Widget child, | ||
}) : super(child: child); | ||
|
||
final SelectionRegistrar registrar; | ||
|
||
@override | ||
_RenderSelectableAdapter createRenderObject(BuildContext context) { | ||
return _RenderSelectableAdapter( | ||
DefaultSelectionStyle.of(context).selectionColor!, | ||
registrar, | ||
); | ||
} | ||
|
||
@override | ||
void updateRenderObject(BuildContext context, _RenderSelectableAdapter renderObject) { | ||
renderObject | ||
..selectionColor = DefaultSelectionStyle.of(context).selectionColor! | ||
..registrar = registrar; | ||
} | ||
} | ||
|
||
class _RenderSelectableAdapter extends RenderProxyBox with Selectable, SelectionRegistrant { | ||
_RenderSelectableAdapter( | ||
Color selectionColor, | ||
SelectionRegistrar registrar, | ||
) : _selectionColor = selectionColor, | ||
_geometry = ValueNotifier<SelectionGeometry>(_noSelection) { | ||
this.registrar = registrar; | ||
_geometry.addListener(markNeedsPaint); | ||
} | ||
|
||
static const SelectionGeometry _noSelection = SelectionGeometry(status: SelectionStatus.none, hasContent: true); | ||
final ValueNotifier<SelectionGeometry> _geometry; | ||
|
||
Color get selectionColor => _selectionColor; | ||
late Color _selectionColor; | ||
set selectionColor(Color value) { | ||
if (_selectionColor == value) { | ||
return; | ||
} | ||
_selectionColor = value; | ||
markNeedsPaint(); | ||
} | ||
|
||
// ValueListenable APIs | ||
|
||
@override | ||
void addListener(VoidCallback listener) => _geometry.addListener(listener); | ||
|
||
@override | ||
void removeListener(VoidCallback listener) => _geometry.removeListener(listener); | ||
|
||
@override | ||
SelectionGeometry get value => _geometry.value; | ||
|
||
// Selectable APIs. | ||
|
||
// Adjust this value to enlarge or shrink the selection highlight. | ||
static const double _padding = 10.0; | ||
Rect _getSelectionHighlightRect() { | ||
return Rect.fromLTWH( | ||
0 - _padding, | ||
0 - _padding, | ||
size.width + _padding * 2, | ||
size.height + _padding * 2 | ||
); | ||
} | ||
|
||
Offset? _start; | ||
Offset? _end; | ||
void _updateGeometry() { | ||
if (_start == null || _end == null) { | ||
_geometry.value = _noSelection; | ||
return; | ||
} | ||
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height); | ||
final Rect selectionRect = Rect.fromPoints(_start!, _end!); | ||
if (renderObjectRect.intersect(selectionRect).isEmpty) { | ||
_geometry.value = _noSelection; | ||
} else { | ||
final Rect selectionRect = _getSelectionHighlightRect(); | ||
final SelectionPoint firstSelectionPoint = SelectionPoint( | ||
localPosition: selectionRect.bottomLeft, | ||
lineHeight: selectionRect.size.height, | ||
handleType: TextSelectionHandleType.left, | ||
); | ||
final SelectionPoint secondSelectionPoint = SelectionPoint( | ||
localPosition: selectionRect.bottomRight, | ||
lineHeight: selectionRect.size.height, | ||
handleType: TextSelectionHandleType.right, | ||
); | ||
final bool isReversed; | ||
if (_start!.dy > _end!.dy) { | ||
isReversed = true; | ||
} else if (_start!.dy < _end!.dy) { | ||
isReversed = false; | ||
} else { | ||
isReversed = _start!.dx > _end!.dx; | ||
} | ||
_geometry.value = SelectionGeometry( | ||
status: SelectionStatus.uncollapsed, | ||
hasContent: true, | ||
startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint, | ||
endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint, | ||
); | ||
} | ||
} | ||
|
||
@override | ||
SelectionResult dispatchSelectionEvent(SelectionEvent event) { | ||
SelectionResult result = SelectionResult.none; | ||
switch (event.type) { | ||
case SelectionEventType.startEdgeUpdate: | ||
case SelectionEventType.endEdgeUpdate: | ||
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height); | ||
// Normalize offset in case it is out side of the rect. | ||
final Offset point = globalToLocal((event as SelectionEdgeUpdateEvent).globalPosition); | ||
final Offset adjustedPoint = SelectionUtils.adjustDragOffset(renderObjectRect, point); | ||
if (event.type == SelectionEventType.startEdgeUpdate) { | ||
_start = adjustedPoint; | ||
} else { | ||
_end = adjustedPoint; | ||
} | ||
result = SelectionUtils.getResultBasedOnRect(renderObjectRect, point); | ||
break; | ||
case SelectionEventType.clear: | ||
_start = _end = null; | ||
break; | ||
case SelectionEventType.selectAll: | ||
case SelectionEventType.selectWord: | ||
_start = Offset.zero; | ||
_end = Offset.infinite; | ||
break; | ||
} | ||
_updateGeometry(); | ||
return result; | ||
} | ||
|
||
// This method is called when users want to copy selected content in this | ||
// widget into clipboard. | ||
@override | ||
SelectedContent? getSelectedContent() { | ||
return value.hasSelection ? const SelectedContent(plainText: 'Custom Text') : null; | ||
} | ||
|
||
LayerLink? _startHandle; | ||
LayerLink? _endHandle; | ||
|
||
@override | ||
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) { | ||
if (_startHandle == startHandle && _endHandle == endHandle) { | ||
return; | ||
} | ||
_startHandle = startHandle; | ||
_endHandle = endHandle; | ||
markNeedsPaint(); | ||
} | ||
|
||
@override | ||
void paint(PaintingContext context, Offset offset) { | ||
super.paint(context, offset); | ||
if (!_geometry.value.hasSelection) { | ||
return; | ||
} | ||
// Draw the selection highlight. | ||
final Paint selectionPaint = Paint() | ||
..style = PaintingStyle.fill | ||
..color = _selectionColor; | ||
context.canvas.drawRect(_getSelectionHighlightRect().shift(offset), selectionPaint); | ||
|
||
// Push the layer links if any. | ||
if (_startHandle != null) { | ||
context.pushLayer( | ||
LeaderLayer( | ||
link: _startHandle!, | ||
offset: offset + value.startSelectionPoint!.localPosition, | ||
), | ||
(PaintingContext context, Offset offset) { }, | ||
Offset.zero, | ||
); | ||
} | ||
if (_endHandle != null) { | ||
context.pushLayer( | ||
LeaderLayer( | ||
link: _endHandle!, | ||
offset: offset + value.endSelectionPoint!.localPosition, | ||
), | ||
(PaintingContext context, Offset offset) { }, | ||
Offset.zero, | ||
); | ||
} | ||
} | ||
|
||
@override | ||
void dispose() { | ||
_geometry.dispose(); | ||
super.dispose(); | ||
} | ||
} |
Oops, something went wrong.