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 {
///
/// 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
/// handles have been dragged to new locations.
class SelectionEdgeUpdateEvent extends SelectionEvent {
/// Creates a selection start edge update event.
///
/// 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({
required this.globalPosition
}) : super._(SelectionEventType.startEdgeUpdate);
required this.globalPosition,
TextGranularity? granularity
}) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.startEdgeUpdate);
/// Creates a selection end edge update event.
///
/// 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({
required this.globalPosition
}) : super._(SelectionEventType.endEdgeUpdate);
required this.globalPosition,
TextGranularity? granularity
}) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.endEdgeUpdate);
/// The new location of the selection edge.
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].
......@@ -686,7 +704,7 @@ class SelectionGeometry {
/// The geometry information of a selection point.
@immutable
class SelectionPoint {
class SelectionPoint with Diagnosticable {
/// Creates a selection point object.
///
/// All properties must not be null.
......@@ -730,6 +748,14 @@ class SelectionPoint {
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.
......
......@@ -1177,11 +1177,11 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
if (event.type == SelectionEventType.endEdgeUpdate) {
_currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition);
final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy);
event = SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset);
event = SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset, granularity: event.granularity);
} else {
_currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition);
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);
......@@ -1430,6 +1430,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy);
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];
if (_currentDragEndRelatedToOrigin != null &&
......@@ -1438,6 +1441,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy);
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() {
// Backwards selection.
await gesture.down(textOffsetToPosition(paragraph, 3));
await tester.pumpAndSettle();
expect(content, isNull);
await gesture.moveTo(textOffsetToPosition(paragraph, 0));
await gesture.up();
......
......@@ -4,6 +4,7 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
......@@ -106,6 +107,89 @@ void main() {
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 {
final ScrollController controller = ScrollController();
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