Skip to content

Commit

Permalink
Supports global selection for all devices (#95226)
Browse files Browse the repository at this point in the history
* Support global selection

* addressing comments

* add new test

* Addressing review comments

* update

* addressing comments

* addressing comments

* Addressing comments

* fix build
  • Loading branch information
chunhtai committed May 24, 2022
1 parent bd7d34f commit efb9368
Show file tree
Hide file tree
Showing 28 changed files with 6,424 additions and 186 deletions.
131 changes: 131 additions & 0 deletions examples/api/lib/material/selection_area/custom_container.dart
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 examples/api/lib/material/selection_area/custom_selectable.dart
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();
}
}
Loading

0 comments on commit efb9368

Please sign in to comment.