Unverified Commit 457d0044 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Add double click and double click + drag gestures to SelectionArea (#124817)

Adds double click to select a word.
Adds double click + drag to select word by word.

https://user-images.githubusercontent.com/948037/234363577-941c36bc-ac42-4b7f-84aa-26b106c9ff05.mov

Partially fixes #104552
parent b3096225
...@@ -375,26 +375,44 @@ class SelectWordSelectionEvent extends SelectionEvent { ...@@ -375,26 +375,44 @@ class SelectWordSelectionEvent extends SelectionEvent {
/// ///
/// The [globalPosition] contains the new offset of the edge. /// The [globalPosition] contains the new offset of the edge.
/// ///
/// This event is dispatched when the framework detects [DragStartDetails] in /// The [granularity] contains the granularity that the selection edge should move by.
/// Only [TextGranularity.character] and [TextGranularity.word] are currently supported.
///
/// This event is dispatched when the framework detects [TapDragStartDetails] in
/// [SelectionArea]'s gesture recognizers for mouse devices, or the selection /// [SelectionArea]'s gesture recognizers for mouse devices, or the selection
/// handles have been dragged to new locations. /// handles have been dragged to new locations.
class SelectionEdgeUpdateEvent extends SelectionEvent { class SelectionEdgeUpdateEvent extends SelectionEvent {
/// Creates a selection start edge update event. /// Creates a selection start edge update event.
/// ///
/// The [globalPosition] contains the location of the selection start edge. /// The [globalPosition] contains the location of the selection start edge.
///
/// The [granularity] contains the granularity which the selection edge should move by.
/// This value defaults to [TextGranularity.character].
const SelectionEdgeUpdateEvent.forStart({ const SelectionEdgeUpdateEvent.forStart({
required this.globalPosition required this.globalPosition,
}) : super._(SelectionEventType.startEdgeUpdate); TextGranularity? granularity
}) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.startEdgeUpdate);
/// Creates a selection end edge update event. /// Creates a selection end edge update event.
/// ///
/// The [globalPosition] contains the new location of the selection end edge. /// The [globalPosition] contains the new location of the selection end edge.
///
/// The [granularity] contains the granularity which the selection edge should move by.
/// This value defaults to [TextGranularity.character].
const SelectionEdgeUpdateEvent.forEnd({ const SelectionEdgeUpdateEvent.forEnd({
required this.globalPosition required this.globalPosition,
}) : super._(SelectionEventType.endEdgeUpdate); TextGranularity? granularity
}) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.endEdgeUpdate);
/// The new location of the selection edge. /// The new location of the selection edge.
final Offset globalPosition; final Offset globalPosition;
/// The granularity for which the selection moves.
///
/// Only [TextGranularity.character] and [TextGranularity.word] are currently supported.
///
/// Defaults to [TextGranularity.character].
final TextGranularity granularity;
} }
/// Extends the start or end of the selection by a given [TextGranularity]. /// Extends the start or end of the selection by a given [TextGranularity].
...@@ -686,7 +704,7 @@ class SelectionGeometry { ...@@ -686,7 +704,7 @@ class SelectionGeometry {
/// The geometry information of a selection point. /// The geometry information of a selection point.
@immutable @immutable
class SelectionPoint { class SelectionPoint with Diagnosticable {
/// Creates a selection point object. /// Creates a selection point object.
/// ///
/// All properties must not be null. /// All properties must not be null.
...@@ -730,6 +748,14 @@ class SelectionPoint { ...@@ -730,6 +748,14 @@ class SelectionPoint {
handleType, handleType,
); );
} }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition));
properties.add(DoubleProperty('lineHeight', lineHeight));
properties.add(EnumProperty<TextSelectionHandleType>('handleType', handleType));
}
} }
/// The type of selection handle to be displayed. /// The type of selection handle to be displayed.
......
...@@ -1177,11 +1177,11 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont ...@@ -1177,11 +1177,11 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
if (event.type == SelectionEventType.endEdgeUpdate) { if (event.type == SelectionEventType.endEdgeUpdate) {
_currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition); _currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition);
final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy);
event = SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset); event = SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset, granularity: event.granularity);
} else { } else {
_currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition); _currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition);
final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy);
event = SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset); event = SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset, granularity: event.granularity);
} }
final SelectionResult result = super.handleSelectionEdgeUpdate(event); final SelectionResult result = super.handleSelectionEdgeUpdate(event);
...@@ -1430,6 +1430,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont ...@@ -1430,6 +1430,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy);
selectable.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset)); selectable.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset));
// Make sure we track that we have synthesized a start event for this selectable,
// so we don't synthesize events unnecessarily.
_selectableStartEdgeUpdateRecords[selectable] = state.position.pixels;
} }
final double? previousEndRecord = _selectableEndEdgeUpdateRecords[selectable]; final double? previousEndRecord = _selectableEndEdgeUpdateRecords[selectable];
if (_currentDragEndRelatedToOrigin != null && if (_currentDragEndRelatedToOrigin != null &&
...@@ -1438,6 +1441,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont ...@@ -1438,6 +1441,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy);
selectable.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset)); selectable.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset));
// Make sure we track that we have synthesized an end event for this selectable,
// so we don't synthesize events unnecessarily.
_selectableEndEdgeUpdateRecords[selectable] = state.position.pixels;
} }
} }
......
...@@ -157,6 +157,7 @@ void main() { ...@@ -157,6 +157,7 @@ void main() {
// Backwards selection. // Backwards selection.
await gesture.down(textOffsetToPosition(paragraph, 3)); await gesture.down(textOffsetToPosition(paragraph, 3));
await tester.pumpAndSettle();
expect(content, isNull); expect(content, isNull);
await gesture.moveTo(textOffsetToPosition(paragraph, 0)); await gesture.moveTo(textOffsetToPosition(paragraph, 0));
await gesture.up(); await gesture.up();
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -106,6 +107,89 @@ void main() { ...@@ -106,6 +107,89 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('mouse can select multiple widgets on double-click drag', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: SelectionArea(
selectionControls: materialTextSelectionControls,
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
),
));
await tester.pumpAndSettle();
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 0'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: ui.PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph1, 2));
await tester.pumpAndSettle();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 1'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 4));
// Should select the rest of paragraph 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 3'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph3, 3));
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('mouse can select multiple widgets on double-click drag - horizontal', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: SelectionArea(
selectionControls: materialTextSelectionControls,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
),
));
await tester.pumpAndSettle();
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 0'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: ui.PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph1, 2));
await tester.pumpAndSettle();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
await tester.pump();
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 1'), matching: find.byType(RichText)));
await gesture.moveTo(textOffsetToPosition(paragraph2, 5) + const Offset(0, 5));
// Should select the rest of paragraph 1.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
testWidgets('select to scroll forward', (WidgetTester tester) async { testWidgets('select to scroll forward', (WidgetTester tester) async {
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment