mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
6726 lines
258 KiB
Dart
6726 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;
|
|
}
|
|
}
|