6725 lines
258 KiB
Dart
6725 lines
258 KiB
Dart
// 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.
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'clipboard_utils.dart';
|
|
import 'keyboard_utils.dart';
|
|
import 'process_text_utils.dart';
|
|
import 'semantics_tester.dart';
|
|
|
|
Offset textOffsetToPosition(RenderParagraph paragraph, int offset) {
|
|
const caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
|
|
final Offset localOffset =
|
|
paragraph.getOffsetForCaret(TextPosition(offset: offset), caret) +
|
|
Offset(0.0, paragraph.preferredLineHeight);
|
|
return paragraph.localToGlobal(localOffset) + const Offset(kIsWeb ? 1.0 : 0.0, -2.0);
|
|
}
|
|
|
|
Offset globalize(Offset point, RenderBox box) {
|
|
return box.localToGlobal(point);
|
|
}
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
final mockClipboard = MockClipboard();
|
|
|
|
setUp(() async {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
mockClipboard.handleMethodCall,
|
|
);
|
|
await Clipboard.setData(const ClipboardData(text: 'empty'));
|
|
});
|
|
|
|
tearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
);
|
|
});
|
|
|
|
Future<void> setAppLifecycleState(AppLifecycleState state) async {
|
|
final ByteData? message = const StringCodec().encodeMessage(state.toString());
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
'flutter/lifecycle',
|
|
message,
|
|
(ByteData? data) {},
|
|
);
|
|
}
|
|
|
|
group('SelectableRegion', () {
|
|
testWidgets('mouse selection single click sends correct events', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
const Offset(200.0, 200.0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pumpAndSettle();
|
|
renderSelectionSpy.events.clear();
|
|
|
|
await gesture.moveTo(const Offset(200.0, 100.0));
|
|
expect(renderSelectionSpy.events.length, 2);
|
|
expect(renderSelectionSpy.events[0].type, SelectionEventType.startEdgeUpdate);
|
|
final startEdge = renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent;
|
|
expect(startEdge.globalPosition, const Offset(200.0, 200.0));
|
|
expect(renderSelectionSpy.events[1].type, SelectionEventType.endEdgeUpdate);
|
|
var endEdge = renderSelectionSpy.events[1] as SelectionEdgeUpdateEvent;
|
|
expect(endEdge.globalPosition, const Offset(200.0, 100.0));
|
|
renderSelectionSpy.events.clear();
|
|
|
|
await gesture.moveTo(const Offset(100.0, 100.0));
|
|
expect(renderSelectionSpy.events.length, 1);
|
|
expect(renderSelectionSpy.events[0].type, SelectionEventType.endEdgeUpdate);
|
|
endEdge = renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent;
|
|
expect(endEdge.globalPosition, const Offset(100.0, 100.0));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('mouse double click sends select-word event', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
const Offset(200.0, 200.0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
renderSelectionSpy.events.clear();
|
|
await gesture.down(const Offset(200.0, 200.0));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
expect(renderSelectionSpy.events.length, 1);
|
|
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
|
|
final selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
|
|
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
|
|
});
|
|
|
|
testWidgets('touch double click sends select-word event', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
renderSelectionSpy.events.clear();
|
|
await gesture.down(const Offset(200.0, 200.0));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
expect(renderSelectionSpy.events.length, 1);
|
|
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
|
|
final selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
|
|
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
|
|
});
|
|
|
|
testWidgets('Does not crash when using Navigator pages', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/119776
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Navigator(
|
|
pages: <Page<void>>[
|
|
MaterialPage<void>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
const Text('How are you?'),
|
|
SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const SelectAllWidget(child: SizedBox(width: 100, height: 100)),
|
|
),
|
|
const Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
const MaterialPage<void>(child: Scaffold(body: Text('Foreground Page'))),
|
|
],
|
|
onPopPage: (_, _) => false,
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('can draw handles when they are at rect boundaries', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Column(
|
|
children: <Widget>[
|
|
const Text('How are you?'),
|
|
SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectAllWidget(key: spy, child: const SizedBox(width: 100, height: 100)),
|
|
),
|
|
const Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(spy)));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
final RenderSelectAll renderSpy = tester.renderObject<RenderSelectAll>(find.byKey(spy));
|
|
expect(renderSpy.startHandle, isNotNull);
|
|
expect(renderSpy.endHandle, isNotNull);
|
|
});
|
|
|
|
testWidgets('touch does not accept drag', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
|
|
addTearDown(gesture.removePointer);
|
|
await gesture.moveTo(const Offset(200.0, 100.0));
|
|
await gesture.up();
|
|
expect(
|
|
renderSelectionSpy.events.every((SelectionEvent element) => element is ClearSelectionEvent),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'tapping outside the selectable region dismisses selection',
|
|
(WidgetTester tester) async {
|
|
const text = 'Hello world';
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text(text),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// The selection only dismisses when unfocused if the app
|
|
// was currently active.
|
|
await setAppLifecycleState(AppLifecycleState.resumed);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(text), matching: find.byType(RichText)),
|
|
);
|
|
|
|
// Drag to select.
|
|
final Offset textTopLeft = tester.getTopLeft(find.text(text));
|
|
final Offset textBottomRight = tester.getBottomRight(find.text(text));
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textTopLeft,
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await gesture.moveTo(textBottomRight);
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(paragraph.selections, isNotEmpty);
|
|
|
|
// Tap just outside the top-left corner of the selectable region
|
|
// to dismiss the selection.
|
|
final Rect selectableRegionRect = tester.getRect(find.byType(SelectableRegion));
|
|
await tester.tapAt(selectableRegionRect.topLeft - const Offset(10.0, 10.0));
|
|
await tester.pump();
|
|
expect(paragraph.selections, isEmpty);
|
|
},
|
|
// [intended] Tap outside to dismiss the selection is only supported on web.
|
|
skip: !kIsWeb,
|
|
);
|
|
|
|
testWidgets('does not merge semantics node of the children', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
const Text('Line one'),
|
|
const Text('Line two'),
|
|
ElevatedButton(onPressed: () {}, child: const Text('Button')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: 'Line one', textDirection: TextDirection.ltr),
|
|
TestSemantics(label: 'Line two', textDirection: TextDirection.ltr),
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.isEnabled,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
|
label: 'Button',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
ignoreId: true,
|
|
),
|
|
);
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets(
|
|
'Horizontal PageView beats SelectionArea child touch drag gestures on iOS',
|
|
(WidgetTester tester) async {
|
|
final pageController = PageController();
|
|
const testValue = 'abc def ghi jkl mno pqr stu vwx yz';
|
|
addTearDown(pageController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: PageView(
|
|
controller: pageController,
|
|
children: <Widget>[
|
|
Center(
|
|
child: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text(testValue),
|
|
),
|
|
),
|
|
const SizedBox(height: 200.0, child: Center(child: Text('Page 2'))),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(testValue), matching: find.byType(RichText)),
|
|
);
|
|
final Offset gPos = textOffsetToPosition(paragraph, testValue.indexOf('g'));
|
|
final Offset pPos = textOffsetToPosition(paragraph, testValue.indexOf('p'));
|
|
|
|
// A double tap + drag should take precedence over parent drags.
|
|
final TestGesture gesture = await tester.startGesture(gPos);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(gPos);
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(pPos);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(
|
|
paragraph.selections[0],
|
|
TextSelection(
|
|
baseOffset: testValue.indexOf('g'),
|
|
extentOffset: testValue.indexOf('p') + 3,
|
|
),
|
|
);
|
|
|
|
expect(pageController.page, isNotNull);
|
|
expect(pageController.page, 0.0);
|
|
// A horizontal drag directly on the SelectableRegion should move the page
|
|
// view to the next page.
|
|
final Rect selectableTextRect = tester.getRect(find.byType(SelectableRegion));
|
|
await tester.dragFrom(
|
|
selectableTextRect.centerRight - const Offset(0.1, 0.0),
|
|
const Offset(-500.0, 0.0),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(pageController.page, isNotNull);
|
|
expect(pageController.page, 1.0);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'Vertical PageView beats SelectionArea child touch drag gestures',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/150897.
|
|
final pageController = PageController();
|
|
const testValue = 'abc def ghi jkl mno pqr stu vwx yz';
|
|
addTearDown(pageController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: PageView(
|
|
scrollDirection: Axis.vertical,
|
|
controller: pageController,
|
|
children: <Widget>[
|
|
Center(
|
|
child: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text(testValue),
|
|
),
|
|
),
|
|
const SizedBox(height: 200.0, child: Center(child: Text('Page 2'))),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(testValue), matching: find.byType(RichText)),
|
|
);
|
|
final Offset gPos = textOffsetToPosition(paragraph, testValue.indexOf('g'));
|
|
final Offset pPos = textOffsetToPosition(paragraph, testValue.indexOf('p'));
|
|
|
|
// A double tap + drag should take precedence over parent drags.
|
|
final TestGesture gesture = await tester.startGesture(gPos);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(gPos);
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(pPos);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(
|
|
paragraph.selections[0],
|
|
TextSelection(
|
|
baseOffset: testValue.indexOf('g'),
|
|
extentOffset: testValue.indexOf('p') + 3,
|
|
),
|
|
);
|
|
|
|
expect(pageController.page, isNotNull);
|
|
expect(pageController.page, 0.0);
|
|
// A vertical drag directly on the SelectableRegion should move the page
|
|
// view to the next page.
|
|
final Rect selectableTextRect = tester.getRect(find.byType(SelectableRegion));
|
|
// Simulate a pan by drag vertically first.
|
|
await gesture.down(selectableTextRect.center);
|
|
await tester.pump();
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(0.0, -200.0));
|
|
// Introduce horizontal movement.
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(5.0, -300.0));
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(-10.0, -400.0));
|
|
// Continue dragging vertically.
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(0.0, -500.0));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(pageController.page, isNotNull);
|
|
expect(pageController.page, 1.0);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
// [intended] Web does not support double tap + drag gestures on the tested platforms.
|
|
skip: kIsWeb,
|
|
);
|
|
|
|
testWidgets(
|
|
'Vertical PageView beats SelectionArea child touch drag gestures on iOS',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/150897.
|
|
final pageController = PageController();
|
|
const testValue = 'abc def ghi jkl mno pqr stu vwx yz';
|
|
addTearDown(pageController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: PageView(
|
|
scrollDirection: Axis.vertical,
|
|
controller: pageController,
|
|
children: <Widget>[
|
|
Center(
|
|
child: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text(testValue),
|
|
),
|
|
),
|
|
const SizedBox(height: 200.0, child: Center(child: Text('Page 2'))),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(testValue), matching: find.byType(RichText)),
|
|
);
|
|
final Offset gPos = textOffsetToPosition(paragraph, testValue.indexOf('g'));
|
|
final Offset pPos = textOffsetToPosition(paragraph, testValue.indexOf('p'));
|
|
|
|
// A double tap + drag should take precedence over parent drags.
|
|
final TestGesture gesture = await tester.startGesture(gPos);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(gPos);
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(pPos);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(
|
|
paragraph.selections[0],
|
|
TextSelection(
|
|
baseOffset: testValue.indexOf('g'),
|
|
extentOffset: testValue.indexOf('p') + 3,
|
|
),
|
|
);
|
|
|
|
expect(pageController.page, isNotNull);
|
|
expect(pageController.page, 0.0);
|
|
// A vertical drag directly on the SelectableRegion should move the page
|
|
// view to the next page.
|
|
final Rect selectableTextRect = tester.getRect(find.byType(SelectableRegion));
|
|
// Simulate a pan by drag vertically first.
|
|
await gesture.down(selectableTextRect.center);
|
|
await tester.pump();
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(0.0, -200.0));
|
|
// Introduce horizontal movement.
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(5.0, -300.0));
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(-10.0, -400.0));
|
|
// Continue dragging vertically.
|
|
await gesture.moveTo(selectableTextRect.center + const Offset(0.0, -500.0));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(pageController.page, isNotNull);
|
|
expect(pageController.page, 1.0);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets('mouse single-click selection collapses the selection', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
const Offset(200.0, 200.0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(renderSelectionSpy.events.length, 2);
|
|
expect(renderSelectionSpy.events[0], isA<SelectionEdgeUpdateEvent>());
|
|
expect(
|
|
(renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent).type,
|
|
SelectionEventType.startEdgeUpdate,
|
|
);
|
|
expect(renderSelectionSpy.events[1], isA<SelectionEdgeUpdateEvent>());
|
|
expect(
|
|
(renderSelectionSpy.events[1] as SelectionEdgeUpdateEvent).type,
|
|
SelectionEventType.endEdgeUpdate,
|
|
);
|
|
});
|
|
|
|
testWidgets('touch long press sends select-word event', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
renderSelectionSpy.events.clear();
|
|
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
expect(renderSelectionSpy.events.length, 1);
|
|
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
|
|
final selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
|
|
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
|
|
});
|
|
|
|
testWidgets(
|
|
'ending a drag on a selection handle does not show the context menu on mobile web',
|
|
(WidgetTester tester) async {
|
|
const text = 'Hello world, how are you today?';
|
|
final toolbarKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
return SizedBox(key: toolbarKey);
|
|
},
|
|
child: const Text(text),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(text), matching: find.byType(RichText)),
|
|
);
|
|
|
|
// Long press to select 'world'.
|
|
await tester.longPressAt(textOffsetToPosition(paragraph, 7));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify selection, handle visibility, and toolbar visibility.
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(paragraph.selections.length, 1);
|
|
expect(paragraph.selections.first, const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
final List<FadeTransition> transitions = find
|
|
.descendant(
|
|
of: find.byWidgetPredicate(
|
|
(Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay',
|
|
),
|
|
matching: find.byType(FadeTransition),
|
|
)
|
|
.evaluate()
|
|
.map((Element e) => e.widget)
|
|
.cast<FadeTransition>()
|
|
.toList();
|
|
expect(transitions.length, 2);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
// Drag start handle.
|
|
List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections.first);
|
|
expect(boxes, hasLength(1));
|
|
Offset handlePos = globalize(boxes.first.toRect().bottomLeft, paragraph);
|
|
TestGesture gesture = await tester.startGesture(handlePos);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Verify selection and toolbar visibility.
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(paragraph.selections.length, 1);
|
|
expect(paragraph.selections.first, const TextSelection(baseOffset: 1, extentOffset: 11));
|
|
|
|
// Drag end handle.
|
|
boxes = paragraph.getBoxesForSelection(paragraph.selections.first);
|
|
expect(boxes, hasLength(1));
|
|
handlePos = globalize(boxes.first.toRect().bottomRight, paragraph);
|
|
gesture = await tester.startGesture(handlePos);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 20));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Verify selection and toolbar visibility.
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(paragraph.selections.length, 1);
|
|
expect(paragraph.selections.first, const TextSelection(baseOffset: 1, extentOffset: 20));
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
skip: !kIsWeb, // [intended] This test verifies mobile web behavior.
|
|
);
|
|
|
|
testWidgets('touch long press and drag sends correct events', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
renderSelectionSpy.events.clear();
|
|
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(renderSelectionSpy.events.length, 1);
|
|
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
|
|
final selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
|
|
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
|
|
|
|
renderSelectionSpy.events.clear();
|
|
await gesture.moveTo(const Offset(200.0, 50.0));
|
|
await gesture.up();
|
|
expect(renderSelectionSpy.events.length, 1);
|
|
expect(renderSelectionSpy.events[0].type, SelectionEventType.endEdgeUpdate);
|
|
final edgeEvent = renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent;
|
|
expect(edgeEvent.globalPosition, const Offset(200.0, 50.0));
|
|
expect(edgeEvent.granularity, TextGranularity.word);
|
|
});
|
|
|
|
testWidgets('touch long press cancel does not send ClearSelectionEvent', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
renderSelectionSpy.events.clear();
|
|
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
|
|
|
|
addTearDown(gesture.removePointer);
|
|
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.cancel();
|
|
expect(
|
|
renderSelectionSpy.events.any((SelectionEvent element) => element is ClearSelectionEvent),
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
testWidgets('scrolling after the selection does not send ClearSelectionEvent', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/128765
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SizedBox(
|
|
height: 750,
|
|
child: SingleChildScrollView(
|
|
child: SizedBox(
|
|
height: 2000,
|
|
child: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
renderSelectionSpy.events.clear();
|
|
final TestGesture selectGesture = await tester.startGesture(const Offset(200.0, 200.0));
|
|
addTearDown(selectGesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await selectGesture.up();
|
|
expect(renderSelectionSpy.events.length, 1);
|
|
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
|
|
|
|
renderSelectionSpy.events.clear();
|
|
final TestGesture scrollGesture = await tester.startGesture(const Offset(250.0, 850.0));
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await scrollGesture.moveTo(Offset.zero);
|
|
await scrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(renderSelectionSpy.events.length, 0);
|
|
});
|
|
|
|
testWidgets('mouse long press does not send select-word event', (WidgetTester tester) async {
|
|
final spy = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionSpy(key: spy),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(
|
|
find.byKey(spy),
|
|
);
|
|
renderSelectionSpy.events.clear();
|
|
final TestGesture gesture = await tester.startGesture(
|
|
const Offset(200.0, 200.0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
expect(
|
|
renderSelectionSpy.events.every(
|
|
(SelectionEvent element) => element is SelectionEdgeUpdateEvent,
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
});
|
|
|
|
testWidgets('Can extend StaticSelectionContainerDelegate', (WidgetTester tester) async {
|
|
SelectedContent? content;
|
|
|
|
// Inserts a new line between selected content of children selectables.
|
|
final selectionDelegate = ColumnSelectionContainerDelegate();
|
|
|
|
addTearDown(selectionDelegate.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionContainer(
|
|
delegate: selectionDelegate,
|
|
child: const Center(
|
|
child: Column(children: <Widget>[Text('Hello World!'), Text('How are you!')]),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Hello World!'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you!'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture mouseGesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 4),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
|
|
expect(content, isNull);
|
|
addTearDown(mouseGesture.removePointer);
|
|
await tester.pump();
|
|
|
|
// Move selection to second paragraph.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph2, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'o World!\nHow are yo');
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
});
|
|
|
|
testWidgets(
|
|
'dragging handle or selecting word triggers haptic feedback on Android',
|
|
(WidgetTester tester) async {
|
|
final log = <MethodCall>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
|
MethodCall methodCall,
|
|
) async {
|
|
log.add(methodCall);
|
|
return null;
|
|
});
|
|
addTearDown(() {
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
mockClipboard.handleMethodCall,
|
|
);
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 6),
|
|
); // at the 'r'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
// `are` is selected.
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
expect(
|
|
log.last,
|
|
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
|
|
);
|
|
log.clear();
|
|
final List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections[0]);
|
|
expect(boxes.length, 1);
|
|
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph);
|
|
await gesture.down(handlePos);
|
|
final endPos = Offset(textOffsetToPosition(paragraph, 8).dx, handlePos.dy);
|
|
|
|
// Select 1 more character by dragging end handle to trigger feedback.
|
|
await gesture.moveTo(endPos);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 8));
|
|
// Only Android vibrate when dragging the handle.
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
expect(
|
|
log.last,
|
|
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
|
|
);
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
expect(log, isEmpty);
|
|
}
|
|
await gesture.up();
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
group('SelectionArea integration', () {
|
|
testWidgets(
|
|
'selection is not cleared when app loses focus on desktop',
|
|
(WidgetTester tester) async {
|
|
final focusNode = FocusNode();
|
|
final GlobalKey selectableKey = GlobalKey();
|
|
addTearDown(focusNode.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
key: selectableKey,
|
|
focusNode: focusNode,
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
await setAppLifecycleState(AppLifecycleState.resumed);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
expect(focusNode.hasFocus, isTrue);
|
|
|
|
// Setting the app lifecycle state to AppLifecycleState.inactive to simulate
|
|
// a lose of window focus.
|
|
await setAppLifecycleState(AppLifecycleState.inactive);
|
|
await tester.pumpAndSettle();
|
|
expect(focusNode.hasFocus, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
},
|
|
variant: TargetPlatformVariant.desktop(),
|
|
);
|
|
|
|
testWidgets(
|
|
'touch can select word-by-word on double tap drag on mobile platforms',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 8));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
// Start a new double-click drag.
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
|
|
await tester.pump(kDoubleTapTimeout);
|
|
|
|
// Double-click.
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
// Selecting across line should select to the end.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 11));
|
|
await gesture.up();
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
// [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
|
skip: kIsWeb,
|
|
);
|
|
|
|
testWidgets(
|
|
'touch can select multiple widgets on double tap drag on mobile platforms',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2));
|
|
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: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
|
|
await gesture.up();
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
// [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
|
skip: kIsWeb,
|
|
);
|
|
|
|
testWidgets(
|
|
'touch can select multiple widgets on double tap drag and return to origin word on mobile platforms',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2));
|
|
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: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should clear the selection on paragraph 3.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
// Should clear the selection on paragraph 2.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
|
|
await gesture.up();
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
// [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
|
skip: kIsWeb,
|
|
);
|
|
|
|
testWidgets(
|
|
'touch can reverse selection across multiple widgets on double tap drag on mobile platforms',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 10));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph3, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
|
|
await tester.pump();
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 4));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 5));
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 4));
|
|
|
|
await gesture.up();
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
// [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
|
skip: kIsWeb,
|
|
);
|
|
|
|
testWidgets(
|
|
'touch cannot triple tap or triple tap drag on Android and iOS',
|
|
(WidgetTester tester) async {
|
|
const longText =
|
|
'Hello world this is some long piece of text '
|
|
'that will represent a long paragraph, when triple clicking this block '
|
|
'of text all of it will be selected.\n'
|
|
'This will be the start of a new line. When triple clicking this block '
|
|
'of text all of it should be selected.';
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text(longText)),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(longText), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 155));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 170));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
|
|
|
|
// Start a new triple-click drag.
|
|
await gesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pumpAndSettle();
|
|
await gesture.up();
|
|
expect(paragraph.selections.isNotEmpty, isTrue);
|
|
expect(paragraph.selections.length, 1);
|
|
expect(paragraph.selections.first, const TextSelection.collapsed(offset: 151));
|
|
await tester.pump(kDoubleTapTimeout);
|
|
|
|
// Triple-click.
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 150, extentOffset: 257));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Reset selection.
|
|
await tester.tapAt(textOffsetToPosition(paragraph, 0));
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 0));
|
|
|
|
// Trying to triple-click with a touch gesture should not work.
|
|
final TestGesture touchGesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
);
|
|
addTearDown(touchGesture.removePointer);
|
|
await tester.pump();
|
|
await touchGesture.up();
|
|
await tester.pump();
|
|
|
|
await touchGesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await touchGesture.up();
|
|
await tester.pump();
|
|
|
|
await touchGesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await touchGesture.up();
|
|
await tester.pumpAndSettle();
|
|
// The selection is collapsed on Android because the max consecutive tap count
|
|
// on native Android is 2 when the pointer device kind is not precise like
|
|
// for a touch.
|
|
//
|
|
// On iOS the selection is maintained because the tap occurred on the active
|
|
// selection.
|
|
expect(
|
|
paragraph.selections[0],
|
|
defaultTargetPlatform == TargetPlatform.iOS
|
|
? const TextSelection(baseOffset: 0, extentOffset: 5)
|
|
: const TextSelection.collapsed(offset: 2),
|
|
);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.iOS,
|
|
}),
|
|
// [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
|
skip: kIsWeb,
|
|
);
|
|
|
|
testWidgets(
|
|
'touch cannot select word-by-word on double tap drag when on Android web',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
// Dragging should not change the selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
},
|
|
skip: !kIsWeb, // [intended] This test verifies web behavior.
|
|
);
|
|
|
|
testWidgets(
|
|
'touch can double tap + drag on iOS web',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
|
|
|
// A double tap should not change the selection.
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
|
|
|
// Dragging should change the selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 8));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
skip: !kIsWeb, // [intended] This test verifies web behavior.
|
|
);
|
|
|
|
testWidgets(
|
|
'touch cannot double tap on iOS web',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
|
|
|
// A double tap should not change the selection.
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
skip: !kIsWeb, // [intended] This test verifies web behavior.
|
|
);
|
|
|
|
testWidgets(
|
|
'RenderParagraph should invalidate cachedRect on window size change',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/155143.
|
|
addTearDown(tester.view.reset);
|
|
const testString = 'How are you doing today? Good, and you?';
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text(testString)),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.textContaining('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, testString.length));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
paragraph.selections[0],
|
|
const TextSelection(baseOffset: 2, extentOffset: testString.length),
|
|
);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Change the size of the window.
|
|
tester.view.physicalSize = const Size(800.0, 400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Start a new drag.
|
|
await gesture.down(textOffsetToPosition(paragraph, 0));
|
|
await tester.pumpAndSettle();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 0));
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Select to the end.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, testString.length));
|
|
await tester.pump();
|
|
expect(
|
|
paragraph.selections[0],
|
|
const TextSelection(baseOffset: 2, extentOffset: testString.length),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets('RenderParagraph should invalidate cached bounding boxes', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final outerText = UniqueKey();
|
|
addTearDown(tester.view.reset);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Scaffold(
|
|
body: Center(child: Text('How are you doing today? Good, and you?', key: outerText)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final SelectableRegionState state = tester.state<SelectableRegionState>(
|
|
find.byType(SelectableRegion),
|
|
);
|
|
|
|
// Double click to select word at position.
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 27),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 27));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Should select "Good".
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 25, extentOffset: 29));
|
|
|
|
// Change the size of the window.
|
|
tester.view.physicalSize = const Size(800.0, 400.0);
|
|
await tester.pumpAndSettle();
|
|
state.clearSelection();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(paragraph.selections.isEmpty, isTrue);
|
|
|
|
// Double click at the same position.
|
|
await gesture.down(textOffsetToPosition(paragraph, 27));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 27));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Should select "Good" again.
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 25, extentOffset: 29));
|
|
});
|
|
|
|
testWidgets('mouse can select single text on desktop platforms', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 6));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1));
|
|
|
|
// Start a new drag.
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
|
|
|
|
// Selecting across line should select to the end.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11));
|
|
|
|
await gesture.up();
|
|
}, variant: TargetPlatformVariant.desktop());
|
|
|
|
testWidgets('mouse can select single text on mobile platforms', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 6));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1));
|
|
|
|
// Start a new drag.
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 6));
|
|
|
|
// Selecting across line should select to the end.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11));
|
|
|
|
await gesture.up();
|
|
}, variant: TargetPlatformVariant.mobile());
|
|
|
|
testWidgets('mouse drag finalizes the selection', (WidgetTester tester) async {
|
|
SelectableRegionSelectionStatus? selectionStatus;
|
|
final GlobalKey textKey = GlobalKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(child: Text(key: textKey, 'How are you')),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(textKey.currentContext, isNotNull);
|
|
final ValueListenable<SelectableRegionSelectionStatus>? selectionStatusNotifier =
|
|
SelectableRegionSelectionStatusScope.maybeOf(textKey.currentContext!);
|
|
void onSelectionStatusChange() {
|
|
selectionStatus = selectionStatusNotifier?.value;
|
|
}
|
|
|
|
selectionStatusNotifier?.addListener(onSelectionStatusChange);
|
|
addTearDown(() {
|
|
selectionStatusNotifier?.removeListener(onSelectionStatusChange);
|
|
});
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
expect(selectionStatus, SelectableRegionSelectionStatus.changing);
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(paragraph.selections.length, 1);
|
|
expect(selectionStatus, SelectableRegionSelectionStatus.finalized);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets(
|
|
'touch drag does not finalize selection on mobile platforms',
|
|
(WidgetTester tester) async {
|
|
SelectableRegionSelectionStatus? selectionStatus;
|
|
final GlobalKey textKey = GlobalKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(child: Text(key: textKey, 'How are you')),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(textKey.currentContext, isNotNull);
|
|
final ValueListenable<SelectableRegionSelectionStatus>? selectionStatusNotifier =
|
|
SelectableRegionSelectionStatusScope.maybeOf(textKey.currentContext!);
|
|
void onSelectionStatusChange() {
|
|
selectionStatus = selectionStatusNotifier?.value;
|
|
}
|
|
|
|
selectionStatusNotifier?.addListener(onSelectionStatusChange);
|
|
addTearDown(() {
|
|
selectionStatusNotifier?.removeListener(onSelectionStatusChange);
|
|
});
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(paragraph.selections.length, 0);
|
|
expect(selectionStatus, isNull);
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
);
|
|
|
|
testWidgets('mouse can select word-by-word on double click drag', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 8));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
// Start a new double-click drag.
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
|
|
await tester.pump(kDoubleTapTimeout);
|
|
|
|
// Double-click.
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 5));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
// Selecting across line should select to the end.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 11));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('mouse can select multiple widgets on double click drag', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: 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: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets(
|
|
'mouse can select multiple widgets on double click drag and return to origin word',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: 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: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should clear the selection on paragraph 3.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
// Should clear the selection on paragraph 2.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
|
|
await gesture.up();
|
|
},
|
|
);
|
|
|
|
testWidgets('mouse can reverse selection across multiple widgets on double click drag', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 10),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph3, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
|
|
await tester.pump();
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 4));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 5));
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 4));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('mouse can select paragraph-by-paragraph on triple click drag', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const longText =
|
|
'Hello world this is some long piece of text '
|
|
'that will represent a long paragraph, when triple clicking this block '
|
|
'of text all of it will be selected.\n'
|
|
'This will be the start of a new line. When triple clicking this block '
|
|
'of text all of it should be selected.';
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text(longText)),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(longText), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 155));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 170));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
|
|
|
|
// Check backward selection.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
|
|
|
|
// Start a new triple-click drag.
|
|
await gesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pumpAndSettle();
|
|
await gesture.up();
|
|
expect(paragraph.selections.isNotEmpty, isTrue);
|
|
expect(paragraph.selections.length, 1);
|
|
expect(paragraph.selections.first, const TextSelection.collapsed(offset: 151));
|
|
await tester.pump(kDoubleTapTimeout);
|
|
|
|
// Triple-click.
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
await gesture.down(textOffsetToPosition(paragraph, 151));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 150, extentOffset: 257));
|
|
|
|
// Selecting across line should select to the end.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, -200.0));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 257, extentOffset: 0));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets(
|
|
'mouse can select multiple widgets on triple click drag when selecting inside a WidgetSpan',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text.rich(
|
|
WidgetSpan(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Text('Text widget A.'),
|
|
Text('Text widget B.'),
|
|
Text('Text widget C.'),
|
|
Text('Text widget D.'),
|
|
Text('Text widget E.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraphC = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('Text widget C.'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraphC, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraphC, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraphC, 2));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraphC.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraphC, 7));
|
|
await tester.pump();
|
|
expect(paragraphC.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
|
|
final RenderParagraph paragraphE = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('Text widget E.'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
final RenderParagraph paragraphD = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('Text widget D.'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraphE, 5));
|
|
// Should select line C-E.
|
|
expect(paragraphC.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraphD.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraphE.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
|
|
await gesture.up();
|
|
},
|
|
);
|
|
|
|
testWidgets('mouse can select multiple widgets on triple click drag', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?\nThis is the first text widget.'),
|
|
Text('Good, and you?\nThis is the second text widget.'),
|
|
Text('Fine, thank you.\nThis is the third text widget.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('first text widget'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph1, 2));
|
|
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: 13));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 14));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('second text widget'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select line 1 of text widget 2.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 15));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 16));
|
|
// Should select the rest of text widget 2.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('third text widget'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
// Should select line 1 of text widget 3.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 17));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 18));
|
|
// Should select the rest of text widget 3.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 43));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 47));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets(
|
|
'mouse can select multiple widgets on triple click drag and return to origin paragraph',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?\nThis is the first text widget.'),
|
|
Text('Good, and you?\nThis is the second text widget.'),
|
|
Text('Fine, thank you.\nThis is the third text widget.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('second text widget'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph2, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph2, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph2, 2));
|
|
await tester.pumpAndSettle();
|
|
// Should select line 1 of text widget 2.
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 15));
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('first text widget'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
|
|
// Should select line 2 of text widget 1.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 14));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 43, extentOffset: 13));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 15, extentOffset: 0));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('third text widget'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 5));
|
|
// Should select rest of text widget 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 43, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 15, extentOffset: 0));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 2));
|
|
// Should clear the selection on paragraph 1 and return to the origin paragraph.
|
|
expect(paragraph1.selections.isEmpty, true);
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 15, extentOffset: 0));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
// Should select line 1 of text widget 3.
|
|
expect(paragraph1.selections.isEmpty, true);
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 17));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 18));
|
|
// Should select line 2 of text widget 3.
|
|
expect(paragraph1.selections.isEmpty, true);
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 46));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 47));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should clear the selection on paragraph 3 and return to the origin paragraph.
|
|
expect(paragraph1.selections.isEmpty, true);
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 15));
|
|
expect(paragraph3.selections.isEmpty, true);
|
|
|
|
await gesture.up();
|
|
},
|
|
);
|
|
|
|
testWidgets('mouse can reverse selection across multiple widgets on triple click drag', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?\nThis is the first text widget.'),
|
|
Text('Good, and you?\nThis is the second text widget.'),
|
|
Text('Fine, thank you.\nThis is the third text widget.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('Fine, thank you.'),
|
|
matching: find.byType(RichText),
|
|
),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 18),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph3, 18));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph3, 18));
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 17, extentOffset: 47));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
|
|
await tester.pump();
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 47, extentOffset: 0));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.textContaining('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 47, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 46, extentOffset: 0));
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.textContaining('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 47, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 46, extentOffset: 0));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 43, extentOffset: 0));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('mouse can select multiple widgets', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets(
|
|
'mouse shift + click holds the selection start in place and moves the end',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 9),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections[0], const TextSelection.collapsed(offset: 9));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
await gesture.down(textOffsetToPosition(paragraph2, 5));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 9, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.down(textOffsetToPosition(paragraph3, 13));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 9, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 13));
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 9, extentOffset: 4));
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph1, 0));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 9, extentOffset: 0));
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
},
|
|
variant: TargetPlatformVariant.desktop(),
|
|
);
|
|
|
|
testWidgets(
|
|
'mouse shift + click collapses the selection when it has not been initialized',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 9),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections[0], const TextSelection.collapsed(offset: 9));
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
},
|
|
variant: TargetPlatformVariant.desktop(),
|
|
);
|
|
|
|
testWidgets('collapsing selection should clear selection of all other selectables', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections[0], const TextSelection.collapsed(offset: 2));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.down(textOffsetToPosition(paragraph2, 5));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph1.selections.isEmpty, isTrue);
|
|
expect(paragraph2.selections[0], const TextSelection.collapsed(offset: 5));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.down(textOffsetToPosition(paragraph3, 13));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(paragraph1.selections.isEmpty, isTrue);
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph3.selections[0], const TextSelection.collapsed(offset: 13));
|
|
});
|
|
|
|
testWidgets('mouse can work with disabled container', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
SelectionContainer.disabled(child: Text('Good, and you?')),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
// paragraph2 is in a disabled container.
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('mouse can reverse selection', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 10),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
|
|
await tester.pump();
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 10, extentOffset: 4));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 10, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 5));
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 10, extentOffset: 0));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 6));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets(
|
|
'long press selection overlay behavior on iOS and Android',
|
|
(WidgetTester tester) async {
|
|
// This test verifies that all platforms wait until long press end to
|
|
// show the context menu, and only Android waits until long press end to
|
|
// show the selection handles.
|
|
final isPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
|
|
var buttonTypes = <ContextMenuButtonType>{};
|
|
final toolbarKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
|
.toSet();
|
|
return SizedBox.shrink(key: toolbarKey);
|
|
},
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(buttonTypes.isEmpty, true);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await tester.pumpAndSettle();
|
|
|
|
// All platform except Android should show the selection handles when the
|
|
// long press starts.
|
|
List<FadeTransition> transitions = find
|
|
.descendant(
|
|
of: find.byWidgetPredicate(
|
|
(Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay',
|
|
),
|
|
matching: find.byType(FadeTransition),
|
|
)
|
|
.evaluate()
|
|
.map((Element e) => e.widget)
|
|
.cast<FadeTransition>()
|
|
.toList();
|
|
expect(transitions.length, isPlatformAndroid ? 0 : 2);
|
|
FadeTransition? left;
|
|
FadeTransition? right;
|
|
if (!isPlatformAndroid) {
|
|
left = transitions[0];
|
|
right = transitions[1];
|
|
expect(left.opacity.value, equals(1.0));
|
|
expect(right.opacity.value, equals(1.0));
|
|
}
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
|
await tester.pumpAndSettle();
|
|
transitions = find
|
|
.descendant(
|
|
of: find.byWidgetPredicate(
|
|
(Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay',
|
|
),
|
|
matching: find.byType(FadeTransition),
|
|
)
|
|
.evaluate()
|
|
.map((Element e) => e.widget)
|
|
.cast<FadeTransition>()
|
|
.toList();
|
|
// All platform except Android should show the selection handles while doing
|
|
// a long press drag.
|
|
expect(transitions.length, isPlatformAndroid ? 0 : 2);
|
|
if (!isPlatformAndroid) {
|
|
left = transitions[0];
|
|
right = transitions[1];
|
|
expect(left.opacity.value, equals(1.0));
|
|
expect(right.opacity.value, equals(1.0));
|
|
}
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
transitions = find
|
|
.descendant(
|
|
of: find.byWidgetPredicate(
|
|
(Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay',
|
|
),
|
|
matching: find.byType(FadeTransition),
|
|
)
|
|
.evaluate()
|
|
.map((Element e) => e.widget)
|
|
.cast<FadeTransition>()
|
|
.toList();
|
|
expect(transitions.length, 2);
|
|
left = transitions[0];
|
|
right = transitions[1];
|
|
|
|
// All platforms should show the selection handles and context menu when
|
|
// the long press ends.
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
expect(left.opacity.value, equals(1.0));
|
|
expect(right.opacity.value, equals(1.0));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.iOS,
|
|
}),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'single tap on the previous selection toggles the toolbar on iOS',
|
|
(WidgetTester tester) async {
|
|
var buttonTypes = <ContextMenuButtonType>{};
|
|
final toolbarKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
|
.toSet();
|
|
return SizedBox.shrink(key: toolbarKey);
|
|
},
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(buttonTypes.isEmpty, true);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Collapse selection.
|
|
await tester.tapAt(textOffsetToPosition(paragraph, 9));
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, isFalse);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'right-click mouse can select word at position on Apple platforms',
|
|
(WidgetTester tester) async {
|
|
var buttonTypes = <ContextMenuButtonType>{};
|
|
final toolbarKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
|
.toSet();
|
|
return SizedBox.shrink(key: toolbarKey);
|
|
},
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(buttonTypes.isEmpty, true);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture primaryMouseButtonGesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
addTearDown(primaryMouseButtonGesture.removePointer);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 9));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Collapse selection.
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pumpAndSettle();
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'right-click mouse on an active selection does not clear the selection in other selectables on Apple platforms',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/150268.
|
|
var buttonTypes = <ContextMenuButtonType>{};
|
|
final toolbarKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
|
.toSet();
|
|
return SizedBox.shrink(key: toolbarKey);
|
|
},
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(buttonTypes.isEmpty, true);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
final TestGesture secondaryMouseButtonGesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
addTearDown(secondaryMouseButtonGesture.removePointer);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 5));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(paragraph2.selections, isNotEmpty);
|
|
expect(paragraph3.selections, isNotEmpty);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
|
|
// Right-clicking on the active selection should retain the selection.
|
|
await secondaryMouseButtonGesture.down(textOffsetToPosition(paragraph2, 7));
|
|
await tester.pump();
|
|
await secondaryMouseButtonGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(paragraph.selections, isNotEmpty);
|
|
expect(paragraph2.selections, isNotEmpty);
|
|
expect(paragraph3.selections, isNotEmpty);
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'right-click mouse at the same position as previous right-click toggles the context menu on macOS',
|
|
(WidgetTester tester) async {
|
|
var buttonTypes = <ContextMenuButtonType>{};
|
|
final toolbarKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
|
.toSet();
|
|
return SizedBox.shrink(key: toolbarKey);
|
|
},
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(buttonTypes.isEmpty, true);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
final TestGesture primaryMouseButtonGesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(primaryMouseButtonGesture.removePointer);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Right-click at same position will toggle the context menu off.
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 9));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 9));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Right-click at same position will toggle the context menu off.
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Collapse selection.
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pumpAndSettle();
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'right-click mouse shows the context menu at position on Android, Fuchsia, and Windows',
|
|
(WidgetTester tester) async {
|
|
var buttonTypes = <ContextMenuButtonType>{};
|
|
final toolbarKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
|
.toSet();
|
|
return SizedBox.shrink(key: toolbarKey);
|
|
},
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(buttonTypes.isEmpty, true);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
final TestGesture primaryMouseButtonGesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(primaryMouseButtonGesture.removePointer);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
|
expect(buttonTypes.length, 1);
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 6));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes.length, 1);
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 9));
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(buttonTypes.length, 1);
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Collapse selection.
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
// Create an uncollapsed selection by dragging.
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pump();
|
|
|
|
// Right click on previous selection should not collapse the selection.
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Right click anywhere outside previous selection should collapse the
|
|
// selection.
|
|
await gesture.down(textOffsetToPosition(paragraph, 7));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Collapse selection.
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pumpAndSettle();
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.fuchsia,
|
|
TargetPlatform.windows,
|
|
}),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'right-click mouse toggles the context menu on Linux',
|
|
(WidgetTester tester) async {
|
|
var buttonTypes = <ContextMenuButtonType>{};
|
|
final toolbarKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
|
.toSet();
|
|
return SizedBox.shrink(key: toolbarKey);
|
|
},
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(buttonTypes.isEmpty, true);
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
final TestGesture primaryMouseButtonGesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(primaryMouseButtonGesture.removePointer);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
|
|
|
// Context menu toggled on.
|
|
expect(buttonTypes.length, 1);
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
|
|
|
// Context menu toggled off. Selection remains the same.
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 9));
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
|
|
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Context menu toggled on.
|
|
expect(buttonTypes.length, 1);
|
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Collapse selection.
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5));
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pump();
|
|
|
|
// Right click on previous selection should not collapse the selection.
|
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Right click anywhere outside previous selection should first toggle the context
|
|
// menu off.
|
|
await gesture.down(textOffsetToPosition(paragraph, 7));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
|
|
// Right click again should collapse the selection and toggle the context
|
|
// menu on.
|
|
await gesture.down(textOffsetToPosition(paragraph, 7));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7));
|
|
expect(find.byKey(toolbarKey), findsOneWidget);
|
|
|
|
// Collapse selection.
|
|
await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
|
|
await tester.pump();
|
|
await primaryMouseButtonGesture.up();
|
|
await tester.pumpAndSettle();
|
|
// Selection is collapsed.
|
|
expect(paragraph.selections.isEmpty, false);
|
|
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
|
|
expect(find.byKey(toolbarKey), findsNothing);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'can copy a selection made with the mouse',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Select from offset 2 of paragraph 1 to offset 6 of paragraph3.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
await gesture.up();
|
|
|
|
// keyboard copy.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyC, control: true),
|
|
);
|
|
|
|
final clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
|
expect(clipboardData['text'], 'w are you?Good, and you?Fine, ');
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.windows,
|
|
TargetPlatform.linux,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'does not override TextField keyboard shortcuts if the TextField is focused - non apple',
|
|
(WidgetTester tester) async {
|
|
final controller = TextEditingController(text: 'I am fine, thank you.');
|
|
addTearDown(controller.dispose);
|
|
final selectableRegionFocus = FocusNode();
|
|
addTearDown(selectableRegionFocus.dispose);
|
|
final textFieldFocus = FocusNode();
|
|
addTearDown(textFieldFocus.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: SelectableRegion(
|
|
focusNode: selectableRegionFocus,
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Column(
|
|
children: <Widget>[
|
|
const Text('How are you?'),
|
|
const Text('Good, and you?'),
|
|
TextField(controller: controller, focusNode: textFieldFocus),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
textFieldFocus.requestFocus();
|
|
await tester.pump();
|
|
|
|
// Make sure keyboard select all works on TextField.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyA, control: true),
|
|
);
|
|
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 21));
|
|
|
|
// Make sure no selection in SelectableRegion.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph1.selections.isEmpty, isTrue);
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
|
|
// Focus selectable region.
|
|
selectableRegionFocus.requestFocus();
|
|
await tester.pump();
|
|
|
|
// Reset controller selection once the TextField is unfocused.
|
|
controller.selection = const TextSelection.collapsed(offset: -1);
|
|
|
|
// Make sure keyboard select all will be handled by selectable region now.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyA, control: true),
|
|
);
|
|
expect(controller.selection, const TextSelection.collapsed(offset: -1));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.windows,
|
|
TargetPlatform.linux,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
skip: kIsWeb, // [intended] the web handles this on its own.
|
|
);
|
|
|
|
testWidgets(
|
|
'does not override TextField keyboard shortcuts if the TextField is focused - apple',
|
|
(WidgetTester tester) async {
|
|
final controller = TextEditingController(text: 'I am fine, thank you.');
|
|
addTearDown(controller.dispose);
|
|
final selectableRegionFocus = FocusNode();
|
|
addTearDown(selectableRegionFocus.dispose);
|
|
final textFieldFocus = FocusNode();
|
|
addTearDown(textFieldFocus.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: SelectableRegion(
|
|
focusNode: selectableRegionFocus,
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Column(
|
|
children: <Widget>[
|
|
const Text('How are you?'),
|
|
const Text('Good, and you?'),
|
|
TextField(controller: controller, focusNode: textFieldFocus),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
textFieldFocus.requestFocus();
|
|
await tester.pump();
|
|
|
|
// Make sure keyboard select all works on TextField.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyA, meta: true),
|
|
);
|
|
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 21));
|
|
|
|
// Make sure no selection in SelectableRegion.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph1.selections.isEmpty, isTrue);
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
|
|
// Focus selectable region.
|
|
selectableRegionFocus.requestFocus();
|
|
await tester.pump();
|
|
|
|
// Reset controller selection once the TextField is unfocused.
|
|
controller.selection = const TextSelection.collapsed(offset: -1);
|
|
|
|
// Make sure keyboard select all will be handled by selectable region now.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyA, meta: true),
|
|
);
|
|
expect(controller.selection, const TextSelection.collapsed(offset: -1));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
skip: kIsWeb, // [intended] the web handles this on its own.
|
|
);
|
|
|
|
testWidgets(
|
|
'select all',
|
|
(WidgetTester tester) async {
|
|
final focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
focusNode: focusNode,
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
focusNode.requestFocus();
|
|
|
|
// keyboard select all.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyA, control: true),
|
|
);
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 16));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.windows,
|
|
TargetPlatform.linux,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'mouse selection can handle widget span',
|
|
(WidgetTester tester) async {
|
|
final outerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(
|
|
child: Text.rich(
|
|
const TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'How are you?'),
|
|
WidgetSpan(child: Text('Good, and you?')),
|
|
TextSpan(text: 'Fine, thank you.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
|
|
await gesture.up();
|
|
|
|
// keyboard copy.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyC, control: true),
|
|
);
|
|
final clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
|
expect(clipboardData['text'], 'w are you?Good, and you?Fine');
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.windows,
|
|
TargetPlatform.linux,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'double click + drag mouse selection can handle widget span',
|
|
(WidgetTester tester) async {
|
|
final outerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(
|
|
child: Text.rich(
|
|
const TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'How are you?'),
|
|
WidgetSpan(child: Text('Good, and you?')),
|
|
TextSpan(text: 'Fine, thank you.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 0));
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
|
|
await gesture.up();
|
|
|
|
// keyboard copy.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyC, control: true),
|
|
);
|
|
final clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
|
expect(clipboardData['text'], 'How are you?Good, and you?Fine,');
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.windows,
|
|
TargetPlatform.linux,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'double click + drag mouse selection can handle widget span - multiline',
|
|
(WidgetTester tester) async {
|
|
final outerText = UniqueKey();
|
|
final innerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'How are you\n?'),
|
|
WidgetSpan(child: Text('Good, and you?', key: innerText)),
|
|
const TextSpan(text: 'Fine, thank you.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final RenderParagraph innerParagraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(innerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
await gesture.down(textOffsetToPosition(paragraph, 0));
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(innerParagraph, 2)); // on `Good`.
|
|
|
|
// Should not crash.
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.windows,
|
|
TargetPlatform.linux,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'select word event can select inline widget',
|
|
(WidgetTester tester) async {
|
|
final outerText = UniqueKey();
|
|
final innerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'How are\n you?'),
|
|
WidgetSpan(child: Text('Good, and you?', key: innerText)),
|
|
const TextSpan(text: 'Fine, thank you.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final RenderParagraph innerParagraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(innerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byKey(innerText)),
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Should select "and".
|
|
expect(paragraph.selections.isEmpty, isTrue);
|
|
expect(innerParagraph.selections[0], const TextSelection(baseOffset: 6, extentOffset: 9));
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'select word event should not crash when its position is at an unselectable inline element',
|
|
(WidgetTester tester) async {
|
|
final focusNode = FocusNode();
|
|
final flutterLogo = UniqueKey();
|
|
addTearDown(focusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Scaffold(
|
|
body: Center(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(
|
|
text:
|
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
),
|
|
WidgetSpan(child: FlutterLogo(key: flutterLogo)),
|
|
const TextSpan(text: 'Hello, world.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final Offset gestureOffset = tester.getCenter(find.byKey(flutterLogo).first);
|
|
|
|
// Right click on unselectable element.
|
|
final TestGesture gesture = await tester.startGesture(
|
|
gestureOffset,
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Should not crash.
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can select word when a selectables rect is completely inside of another selectables rect',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/127076.
|
|
final outerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Scaffold(
|
|
body: Center(
|
|
child: Text.rich(
|
|
const TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(
|
|
text:
|
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
),
|
|
WidgetSpan(child: Text('Some text in a WidgetSpan. ')),
|
|
TextSpan(text: 'Hello, world.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
|
|
// Right click to select word at position.
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 125),
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
// Should select "Hello".
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 124, extentOffset: 129));
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'can select word when selectable is broken up by an unselectable WidgetSpan',
|
|
(WidgetTester tester) async {
|
|
final outerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Scaffold(
|
|
body: Center(
|
|
child: Text.rich(
|
|
const TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(
|
|
text:
|
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
),
|
|
WidgetSpan(child: SizedBox.shrink()),
|
|
TextSpan(text: 'Hello, world.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
|
|
// Right click to select word at position.
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 125),
|
|
kind: PointerDeviceKind.mouse,
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
// Should select "Hello".
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 124, extentOffset: 129));
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'widget span is ignored if it does not contain text - non Apple',
|
|
(WidgetTester tester) async {
|
|
final outerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(
|
|
child: Text.rich(
|
|
const TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'How are you?'),
|
|
WidgetSpan(child: Placeholder()),
|
|
TextSpan(text: 'Fine, thank you.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
|
|
await gesture.up();
|
|
|
|
// keyboard copy.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyC, control: true),
|
|
);
|
|
final clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
|
expect(clipboardData['text'], 'w are you?Fine');
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.windows,
|
|
TargetPlatform.linux,
|
|
TargetPlatform.fuchsia,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'widget span is ignored if it does not contain text - Apple',
|
|
(WidgetTester tester) async {
|
|
final outerText = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Center(
|
|
child: Text.rich(
|
|
const TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'How are you?'),
|
|
WidgetSpan(child: Placeholder()),
|
|
TextSpan(text: 'Fine, thank you.'),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
|
|
await gesture.up();
|
|
|
|
// keyboard copy.
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.keyC, meta: true),
|
|
);
|
|
final clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
|
expect(clipboardData['text'], 'w are you?Fine');
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets('mouse can select across bidi text', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('جيد وانت؟', textDirection: TextDirection.rtl),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('جيد وانت؟'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
// Add a little offset to cross the boundary between paragraph 2 and 3.
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6) + const Offset(0, 1));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 9));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('long press and drag touch moves selection word by word', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 6),
|
|
); // at the 'r'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
// `are` is selected.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 7));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 9));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('can drag end handle when not covering entire screen', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/104620.
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Column(
|
|
children: <Widget>[
|
|
const Text('How are you?'),
|
|
SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text('Good, and you?'),
|
|
),
|
|
const Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph2, 7),
|
|
); // at the 'a'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 6, extentOffset: 9));
|
|
final List<TextBox> boxes = paragraph2.getBoxesForSelection(paragraph2.selections[0]);
|
|
expect(boxes.length, 1);
|
|
|
|
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph2);
|
|
await gesture.down(handlePos);
|
|
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph2, 11) + Offset(0, paragraph2.size.height / 2),
|
|
);
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('can drag start handle when not covering entire screen', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/104620.
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Column(
|
|
children: <Widget>[
|
|
const Text('How are you?'),
|
|
SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text('Good, and you?'),
|
|
),
|
|
const Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph2, 7),
|
|
); // at the 'a'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 6, extentOffset: 9));
|
|
final List<TextBox> boxes = paragraph2.getBoxesForSelection(paragraph2.selections[0]);
|
|
expect(boxes.length, 1);
|
|
|
|
final Offset handlePos = globalize(boxes[0].toRect().bottomLeft, paragraph2);
|
|
await gesture.down(handlePos);
|
|
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph2, 11) + Offset(0, paragraph2.size.height / 2),
|
|
);
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 11, extentOffset: 9));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('can drag start selection handle', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 7),
|
|
); // at the 'h'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
final List<TextBox> boxes = paragraph3.getBoxesForSelection(paragraph3.selections[0]);
|
|
expect(boxes.length, 1);
|
|
|
|
final Offset handlePos = globalize(boxes[0].toRect().bottomLeft, paragraph3);
|
|
await gesture.down(handlePos);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph2, 5) + Offset(0, paragraph2.size.height / 2),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 5, extentOffset: 14));
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph1, 6) + Offset(0, paragraph1.size.height / 2),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 6, extentOffset: 12));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('can drag start selection handle across end selection handle', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 7),
|
|
); // at the 'h'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
final List<TextBox> boxes = paragraph3.getBoxesForSelection(paragraph3.selections[0]);
|
|
expect(boxes.length, 1);
|
|
|
|
final Offset handlePos = globalize(boxes[0].toRect().bottomLeft, paragraph3);
|
|
await gesture.down(handlePos);
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph3, 14) + Offset(0, paragraph3.size.height / 2),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 14, extentOffset: 11));
|
|
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph3, 4) + Offset(0, paragraph3.size.height / 2),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 4, extentOffset: 11));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('can drag end selection handle across start selection handle', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 7),
|
|
); // at the 'h'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
final List<TextBox> boxes = paragraph3.getBoxesForSelection(paragraph3.selections[0]);
|
|
expect(boxes.length, 1);
|
|
|
|
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph3);
|
|
await gesture.down(handlePos);
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph3, 4) + Offset(0, paragraph3.size.height / 2),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 4));
|
|
|
|
await gesture.moveTo(
|
|
textOffsetToPosition(paragraph3, 12) + Offset(0, paragraph3.size.height / 2),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 12));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('can select all from toolbar', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 7),
|
|
); // at the 'h'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
expect(find.text('Select all'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('Select all'));
|
|
await tester.pump();
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 16));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
}, skip: kIsWeb); // [intended] Web uses its native context menu.
|
|
|
|
testWidgets('can copy from toolbar', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 7),
|
|
); // at the 'h'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
|
expect(find.text('Copy'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('Copy'));
|
|
await tester.pump();
|
|
|
|
// Selection should be cleared.
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph3.selections.isEmpty, isTrue);
|
|
expect(paragraph2.selections.isEmpty, isTrue);
|
|
expect(paragraph1.selections.isEmpty, isTrue);
|
|
|
|
final clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
|
expect(clipboardData['text'], 'thank');
|
|
}, skip: kIsWeb); // [intended] Web uses its native context menu.
|
|
|
|
testWidgets(
|
|
'can use keyboard to granularly extend selection - character',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// Ho[w ar]e you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 6);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are] you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 7);
|
|
|
|
for (var i = 0; i < 5; i += 1) {
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 8 + i);
|
|
}
|
|
|
|
for (var i = 0; i < 5; i += 1) {
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 11 - i);
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets('can use keyboard to granularly extend selection - word', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
final bool alt;
|
|
final bool control;
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
alt = false;
|
|
control = true;
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
alt = true;
|
|
control = false;
|
|
}
|
|
|
|
// Ho[w ar]e you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 6);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are] you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 7);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you]?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 11);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you?]
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you?
|
|
// Good], and you?
|
|
// Fine, thank you.
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 4);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you?
|
|
// ]Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 0);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are ]you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 8);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 0);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('can use keyboard to granularly extend selection - line', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
final bool alt;
|
|
final bool meta;
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
meta = false;
|
|
alt = true;
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
meta = true;
|
|
alt = false;
|
|
}
|
|
|
|
// Ho[w ar]e you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 6);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, meta: meta),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you?]
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, meta: meta),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you?
|
|
// Good, and you?]
|
|
// Fine, thank you.
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 14);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you?]
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 0);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta),
|
|
);
|
|
await tester.pump();
|
|
// [Ho]w are you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 0);
|
|
expect(paragraph1.selections[0].end, 2);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets(
|
|
'should not throw range error when selecting previous paragraph',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Select from offset 2 of paragraph3 to offset 6 of paragraph3.
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph3, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
final bool alt;
|
|
final bool meta;
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
meta = false;
|
|
alt = true;
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
meta = true;
|
|
alt = false;
|
|
}
|
|
|
|
// How are you?
|
|
// Good, and you?
|
|
// Fi[ne, ]thank you.
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 2);
|
|
expect(paragraph3.selections[0].end, 6);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta),
|
|
);
|
|
await tester.pump();
|
|
// How are you?
|
|
// Good, and you?
|
|
// [Fine, ]thank you.
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 6);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true),
|
|
);
|
|
await tester.pump();
|
|
// How are you?
|
|
// Good, and you[?
|
|
// Fine, ]thank you.
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 6);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 13);
|
|
expect(paragraph2.selections[0].end, 14);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets(
|
|
'can use keyboard to granularly extend selection - document',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
final bool alt;
|
|
final bool meta;
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
meta = false;
|
|
alt = true;
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
meta = true;
|
|
alt = false;
|
|
}
|
|
|
|
// Ho[w ar]e you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 6);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: meta, alt: alt),
|
|
);
|
|
await tester.pump();
|
|
// Ho[w are you?
|
|
// Good, and you?
|
|
// Fine, thank you.]
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 14);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 16);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: meta, alt: alt),
|
|
);
|
|
await tester.pump();
|
|
// [Ho]w are you?
|
|
// Good, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 0);
|
|
expect(paragraph1.selections[0].end, 2);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 0);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 0);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets('can use keyboard to directionally extend selection', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Select from offset 2 of paragraph2 to offset 6 of paragraph2.
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph2, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 6));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
// How are you?
|
|
// Go[od, ]and you?
|
|
// Fine, thank you.
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 2);
|
|
expect(paragraph2.selections[0].end, 6);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true),
|
|
);
|
|
await tester.pump();
|
|
// How are you?
|
|
// Go[od, and you?
|
|
// Fine, t]hank you.
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 2);
|
|
expect(paragraph2.selections[0].end, 14);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 7);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true),
|
|
);
|
|
await tester.pump();
|
|
// How are you?
|
|
// Go[od, and you?
|
|
// Fine, thank you.]
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 2);
|
|
expect(paragraph2.selections[0].end, 14);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 16);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true),
|
|
);
|
|
await tester.pump();
|
|
// How are you?
|
|
// Go[od, ]and you?
|
|
// Fine, thank you.
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 2);
|
|
expect(paragraph2.selections[0].end, 6);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 0);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true),
|
|
);
|
|
await tester.pump();
|
|
// How a[re you?
|
|
// Go]od, and you?
|
|
// Fine, thank you.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 5);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 2);
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true),
|
|
);
|
|
await tester.pump();
|
|
// [How are you?
|
|
// Go]od, and you?
|
|
// Fine, thank you.
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 0);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 2);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
group('magnifier', () {
|
|
late ValueNotifier<MagnifierInfo> magnifierInfo;
|
|
final Widget fakeMagnifier = Container(key: UniqueKey());
|
|
|
|
testWidgets('Can drag handles to show, unshow, and update magnifier', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const text = 'Monkeys and rabbits in my soup';
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
magnifierConfiguration: TextMagnifierConfiguration(
|
|
magnifierBuilder:
|
|
(
|
|
_,
|
|
MagnifierController controller,
|
|
ValueNotifier<MagnifierInfo> localMagnifierInfo,
|
|
) {
|
|
magnifierInfo = localMagnifierInfo;
|
|
return fakeMagnifier;
|
|
},
|
|
),
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text(text),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text(text), matching: find.byType(RichText)),
|
|
);
|
|
|
|
// Show the selection handles.
|
|
final TestGesture activateSelectionGesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, text.length ~/ 2),
|
|
);
|
|
addTearDown(activateSelectionGesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await activateSelectionGesture.up();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
|
|
// Drag the handle around so that the magnifier shows.
|
|
final TextBox selectionBox = paragraph
|
|
.getBoxesForSelection(paragraph.selections.first)
|
|
.first;
|
|
final Offset leftHandlePos = globalize(selectionBox.toRect().bottomLeft, paragraph);
|
|
final TestGesture gesture = await tester.startGesture(leftHandlePos);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, text.length - 2));
|
|
await tester.pump();
|
|
|
|
// Expect the magnifier to show and then store it's position.
|
|
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
|
|
final Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph, text.length));
|
|
await tester.pump();
|
|
|
|
// Expect the position the magnifier gets to have moved.
|
|
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));
|
|
|
|
// Lift the pointer and expect the magnifier to disappear.
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
|
});
|
|
});
|
|
});
|
|
|
|
testWidgets(
|
|
'toolbar is hidden on Android and iOS when orientation changes',
|
|
(WidgetTester tester) async {
|
|
addTearDown(tester.view.reset);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 6),
|
|
); // at the 'r'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
// `are` is selected.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
await tester.pumpAndSettle();
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
// Text selection toolbar has appeared.
|
|
expect(find.text('Copy'), findsOneWidget);
|
|
|
|
// Hide the toolbar by changing orientation.
|
|
tester.view.physicalSize = const Size(1800.0, 2400.0);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Copy'), findsNothing);
|
|
|
|
// Handles should be hidden as well on Android
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(CompositedTransformFollower),
|
|
matching: find.byType(Padding),
|
|
),
|
|
defaultTargetPlatform == TargetPlatform.android ? findsNothing : findsNWidgets(2),
|
|
);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.android,
|
|
}),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/121053.
|
|
testWidgets(
|
|
'Ensure SelectionArea does not affect the layout of its children',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: <Widget>[
|
|
SelectionArea(child: Text('row 1')),
|
|
Text('row 2'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final double xOffset1 = tester.getTopLeft(find.text('row 1')).dx;
|
|
final double xOffset2 = tester.getTopLeft(find.text('row 2')).dx;
|
|
expect(xOffset1, xOffset2);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets(
|
|
'the selection behavior when clicking `Copy` item in mobile platforms',
|
|
(WidgetTester tester) async {
|
|
var buttonItems = <ContextMenuButtonItem>[];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonItems = selectableRegionState.contextMenuButtonItems;
|
|
return const SizedBox.shrink();
|
|
},
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await tester.longPressAt(textOffsetToPosition(paragraph1, 6)); // at the 'r'
|
|
await tester.pump(kLongPressTimeout);
|
|
// `are` is selected.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
// Press `Copy` item.
|
|
expect(buttonItems[0].type, ContextMenuButtonType.copy);
|
|
buttonItems[0].onPressed?.call();
|
|
|
|
final SelectableRegionState regionState = tester.state<SelectableRegionState>(
|
|
find.byType(SelectableRegion),
|
|
);
|
|
|
|
// In Android copy should clear the selection.
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
expect(regionState.selectionOverlay, isNull);
|
|
expect(regionState.selectionOverlay?.startHandleLayerLink, isNull);
|
|
expect(regionState.selectionOverlay?.endHandleLayerLink, isNull);
|
|
case TargetPlatform.iOS:
|
|
expect(regionState.selectionOverlay, isNotNull);
|
|
expect(regionState.selectionOverlay?.startHandleLayerLink, isNotNull);
|
|
expect(regionState.selectionOverlay?.endHandleLayerLink, isNotNull);
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
// Test doesn't run these platforms.
|
|
break;
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'the handles do not disappear when clicking `Select all` item in mobile platforms',
|
|
(WidgetTester tester) async {
|
|
var buttonItems = <ContextMenuButtonItem>[];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonItems = selectableRegionState.contextMenuButtonItems;
|
|
return const SizedBox.shrink();
|
|
},
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await tester.longPressAt(textOffsetToPosition(paragraph1, 6)); // at the 'r'
|
|
await tester.pump(kLongPressTimeout);
|
|
// `are` is selected.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
late ContextMenuButtonItem selectAllButton;
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
// On Android, the select all button is after the share button.
|
|
expect(buttonItems[2].type, ContextMenuButtonType.selectAll);
|
|
selectAllButton = buttonItems[2];
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
expect(buttonItems[1].type, ContextMenuButtonType.selectAll);
|
|
selectAllButton = buttonItems[1];
|
|
}
|
|
|
|
// Press `Select All` item.
|
|
selectAllButton.onPressed?.call();
|
|
|
|
final SelectableRegionState regionState = tester.state<SelectableRegionState>(
|
|
find.byType(SelectableRegion),
|
|
);
|
|
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.fuchsia:
|
|
expect(regionState.selectionOverlay, isNotNull);
|
|
expect(regionState.selectionOverlay?.startHandleLayerLink, isNotNull);
|
|
expect(regionState.selectionOverlay?.endHandleLayerLink, isNotNull);
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
// Test doesn't run these platforms.
|
|
break;
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant.mobile(),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'Selection behavior when clicking the `Share` button on Android',
|
|
(WidgetTester tester) async {
|
|
var buttonItems = <ContextMenuButtonItem>[];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonItems = selectableRegionState.contextMenuButtonItems;
|
|
return const SizedBox.shrink();
|
|
},
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
await tester.longPressAt(textOffsetToPosition(paragraph, 6)); // at the 'r'
|
|
await tester.pump(kLongPressTimeout);
|
|
|
|
// `are` is selected.
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
String? lastShare;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(MethodCall methodCall) async {
|
|
if (methodCall.method == 'Share.invoke') {
|
|
expect(methodCall.arguments, isA<String>());
|
|
lastShare = methodCall.arguments as String;
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
),
|
|
);
|
|
|
|
final SelectableRegionState regionState = tester.state<SelectableRegionState>(
|
|
find.byType(SelectableRegion),
|
|
);
|
|
|
|
// Press the `Share` button.
|
|
expect(buttonItems[1].type, ContextMenuButtonType.share);
|
|
buttonItems[1].onPressed?.call();
|
|
expect(lastShare, 'are');
|
|
// On Android, share should clear the selection.
|
|
expect(regionState.selectionOverlay, isNull);
|
|
expect(regionState.selectionOverlay?.startHandleLayerLink, isNull);
|
|
expect(regionState.selectionOverlay?.endHandleLayerLink, isNull);
|
|
},
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets(
|
|
'builds the correct button items',
|
|
(WidgetTester tester) async {
|
|
var buttonItems = <ContextMenuButtonItem>[];
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonItems = selectableRegionState.contextMenuButtonItems;
|
|
return const SizedBox.shrink();
|
|
},
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 6),
|
|
); // at the 'r'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
// `are` is selected.
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
// On Android, the share button is before the select all button.
|
|
expect(buttonItems.length, 3);
|
|
expect(buttonItems[0].type, ContextMenuButtonType.copy);
|
|
expect(buttonItems[1].type, ContextMenuButtonType.share);
|
|
expect(buttonItems[2].type, ContextMenuButtonType.selectAll);
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
expect(buttonItems.length, 2);
|
|
expect(buttonItems[0].type, ContextMenuButtonType.copy);
|
|
expect(buttonItems[1].type, ContextMenuButtonType.selectAll);
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets('can clear selection through SelectableRegionState', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final SelectableRegionState state = tester.state<SelectableRegionState>(
|
|
find.byType(SelectableRegion),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: 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: 3));
|
|
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
|
await tester.pump();
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
|
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
|
// Should select the rest of paragraph 1.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
|
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
|
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
|
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Clear selection programmatically.
|
|
state.clearSelection();
|
|
expect(paragraph1.selections, isEmpty);
|
|
expect(paragraph2.selections, isEmpty);
|
|
expect(paragraph3.selections, isEmpty);
|
|
});
|
|
|
|
testWidgets(
|
|
'Text processing actions are added to the toolbar',
|
|
(WidgetTester tester) async {
|
|
final mockProcessTextHandler = MockProcessTextHandler();
|
|
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.processText,
|
|
mockProcessTextHandler.handleMethodCall,
|
|
);
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.processText,
|
|
null,
|
|
),
|
|
);
|
|
|
|
var buttonLabels = <String?>{};
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
buttonLabels = selectableRegionState.contextMenuButtonItems
|
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.label)
|
|
.toSet();
|
|
return const SizedBox.shrink();
|
|
},
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 6),
|
|
); // at the 'r'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
// `are` is selected.
|
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// The text processing actions are available on Android only.
|
|
final areTextActionsSupported = defaultTargetPlatform == TargetPlatform.android;
|
|
expect(buttonLabels.contains(fakeAction1Label), areTextActionsSupported);
|
|
expect(buttonLabels.contains(fakeAction2Label), areTextActionsSupported);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
skip: kIsWeb, // [intended] Web uses its native context menu.
|
|
);
|
|
|
|
testWidgets('SelectionListener onSelectionChanged is accurate with WidgetSpans', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final dataModel = <String>['Hello world, ', 'how are you today.'];
|
|
final selectionNotifier = SelectionListenerNotifier();
|
|
addTearDown(selectionNotifier.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionListener(
|
|
selectionNotifier: selectionNotifier,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Text.rich(
|
|
TextSpan(
|
|
text: dataModel[0],
|
|
children: <InlineSpan>[WidgetSpan(child: Text(dataModel[1]))],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(
|
|
of: find.textContaining('Hello world'),
|
|
matching: find.byType(RichText).first,
|
|
),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('how are you today.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture mouseGesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
|
|
addTearDown(mouseGesture.removePointer);
|
|
await tester.pump();
|
|
|
|
SelectedContentRange? selectedRange;
|
|
|
|
// Selection on paragraph1.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph1, 1));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 0);
|
|
expect(selectedRange.endOffset, 1);
|
|
|
|
// Selection on paragraph1.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph1, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 0);
|
|
expect(selectedRange.endOffset, 10);
|
|
|
|
// Selection on paragraph1 and paragraph2.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph2, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 0);
|
|
expect(selectedRange.endOffset, 23);
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 0);
|
|
expect(selectedRange.endOffset, 23);
|
|
|
|
// Collapsed selection.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph2, 3));
|
|
await tester.pump();
|
|
await mouseGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(selectionNotifier.selection.status, SelectionStatus.collapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 16);
|
|
expect(selectedRange.endOffset, 16);
|
|
|
|
// Backwards selection.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph2, 4));
|
|
await tester.pump();
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph1, 0));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 17);
|
|
expect(selectedRange.endOffset, 0);
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 17);
|
|
expect(selectedRange.endOffset, 0);
|
|
|
|
// Collapsed selection.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph1, 0));
|
|
await tester.pumpAndSettle();
|
|
await mouseGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(selectionNotifier.selection.status, SelectionStatus.collapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 0);
|
|
expect(selectedRange.endOffset, 0);
|
|
});
|
|
|
|
testWidgets('onSelectionChanged SelectedContentRange is accurate', (WidgetTester tester) async {
|
|
final dataModel = <String>['How are you?', 'Good, and you?', 'Fine, thank you.'];
|
|
final selectionNotifier = SelectionListenerNotifier();
|
|
SelectedContentRange? selectedRange;
|
|
addTearDown(selectionNotifier.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: SelectionListener(
|
|
selectionNotifier: selectionNotifier,
|
|
child: Column(
|
|
children: <Widget>[Text(dataModel[0]), Text(dataModel[1]), Text(dataModel[2])],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture mouseGesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 4),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
|
|
addTearDown(mouseGesture.removePointer);
|
|
await tester.pump();
|
|
|
|
// Selection on paragraph1.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph1, 7));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 4);
|
|
expect(selectedRange.endOffset, 7);
|
|
|
|
// Selection on paragraph1.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph1, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 4);
|
|
expect(selectedRange.endOffset, 10);
|
|
|
|
// Selection on paragraph1 and paragraph2.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph2, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 4);
|
|
expect(selectedRange.endOffset, 22);
|
|
|
|
// Selection on paragraph1, paragraph2, and paragraph3.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph3, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 4);
|
|
expect(selectedRange.endOffset, 36);
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 4);
|
|
expect(selectedRange.endOffset, 36);
|
|
|
|
// Collapsed selection.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph1, 3));
|
|
await tester.pump();
|
|
await mouseGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(selectionNotifier.selection.status, SelectionStatus.collapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 3);
|
|
expect(selectedRange.endOffset, 3);
|
|
|
|
// Backwards selection.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph3, 4));
|
|
await tester.pump();
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph1, 0));
|
|
await tester.pumpAndSettle();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 30);
|
|
expect(selectedRange.endOffset, 0);
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
expect(selectionNotifier.selection.status, SelectionStatus.uncollapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 30);
|
|
expect(selectedRange.endOffset, 0);
|
|
|
|
// Collapsed selection.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph1, 0));
|
|
await tester.pumpAndSettle();
|
|
await mouseGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(selectionNotifier.selection.status, SelectionStatus.collapsed);
|
|
selectedRange = selectionNotifier.selection.range;
|
|
expect(selectedRange, isNotNull);
|
|
expect(selectedRange!.startOffset, 0);
|
|
expect(selectedRange.endOffset, 0);
|
|
});
|
|
|
|
testWidgets('onSelectionChange is called when the selection changes through gestures', (
|
|
WidgetTester tester,
|
|
) async {
|
|
SelectedContent? content;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture mouseGesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph, 4),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
final TestGesture touchGesture = await tester.createGesture();
|
|
|
|
expect(content, isNull);
|
|
addTearDown(mouseGesture.removePointer);
|
|
addTearDown(touchGesture.removePointer);
|
|
await tester.pump();
|
|
|
|
// Called on drag.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 7));
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'are');
|
|
|
|
// Updates on drag.
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'are yo');
|
|
|
|
// Called on drag end.
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'are yo');
|
|
|
|
// Backwards selection.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph, 3));
|
|
await tester.pump();
|
|
await mouseGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, '');
|
|
|
|
await mouseGesture.down(textOffsetToPosition(paragraph, 3));
|
|
await tester.pump();
|
|
|
|
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 0));
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'How');
|
|
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'How');
|
|
|
|
// Called on double tap.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph, 6));
|
|
await tester.pump();
|
|
await mouseGesture.up();
|
|
await tester.pump();
|
|
await mouseGesture.down(textOffsetToPosition(paragraph, 6));
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'are');
|
|
await mouseGesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Called on tap.
|
|
await mouseGesture.down(textOffsetToPosition(paragraph, 0));
|
|
await tester.pumpAndSettle();
|
|
await mouseGesture.up();
|
|
await tester.pumpAndSettle(kDoubleTapTimeout);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, '');
|
|
|
|
// With touch gestures.
|
|
|
|
// Called on long press start.
|
|
await touchGesture.down(textOffsetToPosition(paragraph, 0));
|
|
await tester.pumpAndSettle(kLongPressTimeout);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'How');
|
|
|
|
// Called on long press update.
|
|
await touchGesture.moveTo(textOffsetToPosition(paragraph, 5));
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'How are');
|
|
|
|
// Called on long press end.
|
|
await touchGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'How are');
|
|
|
|
// Long press to select 'you'.
|
|
await touchGesture.down(textOffsetToPosition(paragraph, 9));
|
|
await tester.pumpAndSettle(kLongPressTimeout);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'you');
|
|
await touchGesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Called while moving selection handles.
|
|
final List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections[0]);
|
|
expect(boxes.length, 1);
|
|
final Offset startHandlePos = globalize(boxes[0].toRect().bottomLeft, paragraph);
|
|
final Offset endHandlePos = globalize(boxes[0].toRect().bottomRight, paragraph);
|
|
final startPos = Offset(textOffsetToPosition(paragraph, 4).dx, startHandlePos.dy);
|
|
final endPos = Offset(textOffsetToPosition(paragraph, 6).dx, endHandlePos.dy);
|
|
|
|
// Start handle.
|
|
await touchGesture.down(startHandlePos);
|
|
await touchGesture.moveTo(startPos);
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'are you');
|
|
await touchGesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// End handle.
|
|
await touchGesture.down(endHandlePos);
|
|
await touchGesture.moveTo(endPos);
|
|
await tester.pumpAndSettle();
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'ar');
|
|
await touchGesture.up();
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('onSelectionChange is called when the selection changes through keyboard actions', (
|
|
WidgetTester tester,
|
|
) async {
|
|
SelectedContent? content;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Text('How are you?'),
|
|
Text('Good, and you?'),
|
|
Text('Fine, thank you.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(content, isNull);
|
|
await tester.pump();
|
|
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)),
|
|
);
|
|
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 2),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 6);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w ar');
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 7);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are');
|
|
|
|
for (var i = 0; i < 5; i += 1) {
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 8 + i);
|
|
expect(content, isNotNull);
|
|
}
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are you?');
|
|
|
|
for (var i = 0; i < 5; i += 1) {
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 11 - i);
|
|
expect(content, isNotNull);
|
|
}
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are');
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 8);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are you?Good, an');
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 14);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 9);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are you?Good, and you?Fine, tha');
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 14);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(paragraph3.selections[0].start, 0);
|
|
expect(paragraph3.selections[0].end, 16);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are you?Good, and you?Fine, thank you.');
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 12);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph2.selections[0].start, 0);
|
|
expect(paragraph2.selections[0].end, 8);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are you?Good, an');
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 2);
|
|
expect(paragraph1.selections[0].end, 7);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'w are');
|
|
|
|
await sendKeyCombination(
|
|
tester,
|
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true),
|
|
);
|
|
await tester.pump();
|
|
expect(paragraph1.selections.length, 1);
|
|
expect(paragraph1.selections[0].start, 0);
|
|
expect(paragraph1.selections[0].end, 2);
|
|
expect(paragraph2.selections.length, 1);
|
|
expect(paragraph3.selections.length, 1);
|
|
expect(content, isNotNull);
|
|
expect(content!.plainText, 'Ho');
|
|
});
|
|
|
|
group('BrowserContextMenu', () {
|
|
setUp(() async {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.contextMenu,
|
|
(MethodCall call) {
|
|
// Just complete successfully, so that BrowserContextMenu thinks that
|
|
// the engine successfully received its call.
|
|
return Future<void>.value();
|
|
},
|
|
);
|
|
await BrowserContextMenu.disableContextMenu();
|
|
});
|
|
|
|
tearDown(() async {
|
|
await BrowserContextMenu.enableContextMenu();
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.contextMenu,
|
|
null,
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'web can show flutter context menu when the browser context menu is disabled',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
onSelectionChanged: (SelectedContent? selectedContent) {},
|
|
selectionControls: materialTextSelectionControls,
|
|
child: const Center(child: Text('How are you')),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final SelectableRegionState state = tester.state<SelectableRegionState>(
|
|
find.byType(SelectableRegion),
|
|
);
|
|
expect(find.text('Copy'), findsNothing);
|
|
|
|
state.selectAll(SelectionChangedCause.toolbar);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Copy'), findsOneWidget);
|
|
|
|
state.hideToolbar();
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Copy'), findsNothing);
|
|
},
|
|
skip: !kIsWeb, // [intended] This test verifies web behavior.
|
|
);
|
|
|
|
testWidgets(
|
|
'uses contextMenuBuilder by default on Android and iOS web',
|
|
(WidgetTester tester) async {
|
|
final contextMenu = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder:
|
|
(BuildContext context, SelectableRegionState selectableRegionState) {
|
|
return SizedBox.shrink(key: contextMenu);
|
|
},
|
|
child: const Text('How are you?'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(contextMenu), findsNothing);
|
|
|
|
// Show the toolbar by longpressing.
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 6),
|
|
); // at the 'r'
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
// `are` is selected.
|
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(contextMenu), findsOneWidget);
|
|
},
|
|
// TODO(Renzo-Olivares): Remove this test when the web context menu
|
|
// for Android and iOS is re-enabled.
|
|
// See: https://github.com/flutter/flutter/issues/177123.
|
|
// [intended] Android and iOS use the flutter rendered menu on the web.
|
|
skip:
|
|
!kIsWeb ||
|
|
!<TargetPlatform>{
|
|
TargetPlatform.android,
|
|
TargetPlatform.iOS,
|
|
}.contains(defaultTargetPlatform),
|
|
);
|
|
});
|
|
|
|
testWidgets('Multiple selectables on a single line should be in screen order', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/127942.
|
|
final outerText = UniqueKey();
|
|
const textStyle = TextStyle(fontSize: 10);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Scaffold(
|
|
body: Center(
|
|
child: Text.rich(
|
|
const TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'Hello my name is ', style: textStyle),
|
|
WidgetSpan(
|
|
child: Text('Dash', style: textStyle),
|
|
alignment: PlaceholderAlignment.middle,
|
|
),
|
|
TextSpan(text: '.', style: textStyle),
|
|
],
|
|
),
|
|
key: outerText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(
|
|
find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first,
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
textOffsetToPosition(paragraph1, 0),
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
addTearDown(gesture.removePointer);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
|
|
// Select all.
|
|
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
|
|
|
|
// keyboard copy.
|
|
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
|
|
|
|
final clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
|
expect(clipboardData['text'], 'Hello my name is Dash.');
|
|
});
|
|
}
|
|
|
|
class ColumnSelectionContainerDelegate extends StaticSelectionContainerDelegate {
|
|
/// Copies the selected contents of all [Selectable]s, separating their
|
|
/// contents with a new line.
|
|
@override
|
|
SelectedContent? getSelectedContent() {
|
|
final selections = <SelectedContent>[
|
|
for (final Selectable selectable in selectables)
|
|
if (selectable.getSelectedContent() case final SelectedContent data) data,
|
|
];
|
|
if (selections.isEmpty) {
|
|
return null;
|
|
}
|
|
return SelectedContent(
|
|
plainText: selections
|
|
.map((SelectedContent selectedContent) => selectedContent.plainText)
|
|
.join('\n'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SelectionSpy extends LeafRenderObjectWidget {
|
|
const SelectionSpy({super.key});
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return RenderSelectionSpy(SelectionContainer.maybeOf(context));
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {}
|
|
}
|
|
|
|
class RenderSelectionSpy extends RenderProxyBox with Selectable, SelectionRegistrant {
|
|
RenderSelectionSpy(SelectionRegistrar? registrar) {
|
|
this.registrar = registrar;
|
|
}
|
|
|
|
final Set<VoidCallback> listeners = <VoidCallback>{};
|
|
List<SelectionEvent> events = <SelectionEvent>[];
|
|
|
|
@override
|
|
List<Rect> get boundingBoxes => <Rect>[paintBounds];
|
|
|
|
@override
|
|
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
|
|
|
|
@override
|
|
void performLayout() => size = computeDryLayout(constraints);
|
|
|
|
@override
|
|
void addListener(VoidCallback listener) => listeners.add(listener);
|
|
|
|
@override
|
|
void removeListener(VoidCallback listener) => listeners.remove(listener);
|
|
|
|
@override
|
|
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
|
|
events.add(event);
|
|
return SelectionResult.end;
|
|
}
|
|
|
|
@override
|
|
SelectedContent? getSelectedContent() {
|
|
return const SelectedContent(plainText: 'content');
|
|
}
|
|
|
|
@override
|
|
SelectedContentRange? getSelection() {
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
int get contentLength => 1;
|
|
|
|
@override
|
|
final SelectionGeometry value = const SelectionGeometry(
|
|
hasContent: true,
|
|
status: SelectionStatus.uncollapsed,
|
|
startSelectionPoint: SelectionPoint(
|
|
localPosition: Offset.zero,
|
|
lineHeight: 0.0,
|
|
handleType: TextSelectionHandleType.left,
|
|
),
|
|
endSelectionPoint: SelectionPoint(
|
|
localPosition: Offset.zero,
|
|
lineHeight: 0.0,
|
|
handleType: TextSelectionHandleType.left,
|
|
),
|
|
);
|
|
|
|
@override
|
|
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {}
|
|
}
|
|
|
|
class SelectAllWidget extends SingleChildRenderObjectWidget {
|
|
const SelectAllWidget({super.key, super.child});
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return RenderSelectAll(SelectionContainer.maybeOf(context));
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {}
|
|
}
|
|
|
|
class RenderSelectAll extends RenderProxyBox with Selectable, SelectionRegistrant {
|
|
RenderSelectAll(SelectionRegistrar? registrar) {
|
|
this.registrar = registrar;
|
|
}
|
|
|
|
@override
|
|
List<Rect> get boundingBoxes => <Rect>[paintBounds];
|
|
|
|
final Set<VoidCallback> listeners = <VoidCallback>{};
|
|
LayerLink? startHandle;
|
|
LayerLink? endHandle;
|
|
|
|
@override
|
|
void addListener(VoidCallback listener) => listeners.add(listener);
|
|
|
|
@override
|
|
void removeListener(VoidCallback listener) => listeners.remove(listener);
|
|
|
|
@override
|
|
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
|
|
value = SelectionGeometry(
|
|
hasContent: true,
|
|
status: SelectionStatus.uncollapsed,
|
|
startSelectionPoint: SelectionPoint(
|
|
localPosition: Offset(0, size.height),
|
|
lineHeight: 0.0,
|
|
handleType: TextSelectionHandleType.left,
|
|
),
|
|
endSelectionPoint: SelectionPoint(
|
|
localPosition: Offset(size.width, size.height),
|
|
lineHeight: 0.0,
|
|
handleType: TextSelectionHandleType.left,
|
|
),
|
|
);
|
|
return SelectionResult.end;
|
|
}
|
|
|
|
@override
|
|
SelectedContent? getSelectedContent() {
|
|
return const SelectedContent(plainText: 'content');
|
|
}
|
|
|
|
@override
|
|
SelectedContentRange? getSelection() {
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
int get contentLength => 1;
|
|
|
|
@override
|
|
SelectionGeometry get value => _value;
|
|
SelectionGeometry _value = const SelectionGeometry(
|
|
hasContent: true,
|
|
status: SelectionStatus.uncollapsed,
|
|
startSelectionPoint: SelectionPoint(
|
|
localPosition: Offset.zero,
|
|
lineHeight: 0.0,
|
|
handleType: TextSelectionHandleType.left,
|
|
),
|
|
endSelectionPoint: SelectionPoint(
|
|
localPosition: Offset.zero,
|
|
lineHeight: 0.0,
|
|
handleType: TextSelectionHandleType.left,
|
|
),
|
|
);
|
|
set value(SelectionGeometry other) {
|
|
if (other == _value) {
|
|
return;
|
|
}
|
|
_value = other;
|
|
for (final VoidCallback callback in listeners) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
|
|
this.startHandle = startHandle;
|
|
this.endHandle = endHandle;
|
|
}
|
|
}
|