mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-29 15:00:14 +08:00
661 lines
28 KiB
Diff
661 lines
28 KiB
Diff
diff --git a/packages/flutter/lib/src/painting/inline_span.dart b/packages/flutter/lib/src/painting/inline_span.dart
|
|
index 0c4a0be6177..77baa589699 100644
|
|
--- a/packages/flutter/lib/src/painting/inline_span.dart
|
|
+++ b/packages/flutter/lib/src/painting/inline_span.dart
|
|
@@ -315,6 +315,21 @@ abstract class InlineSpan extends DiagnosticableTree {
|
|
return buffer.toString();
|
|
}
|
|
|
|
+ (String, Map<int, String>) toPlainTextV2({
|
|
+ bool includeSemanticsLabels = true,
|
|
+ bool includePlaceholders = true,
|
|
+ }) {
|
|
+ final buffer = StringBuffer();
|
|
+ final map = <int, String>{};
|
|
+ computeToPlainTextV2(
|
|
+ buffer,
|
|
+ map,
|
|
+ includeSemanticsLabels: includeSemanticsLabels,
|
|
+ includePlaceholders: includePlaceholders,
|
|
+ );
|
|
+ return (buffer.toString(), map);
|
|
+ }
|
|
+
|
|
/// Flattens the [InlineSpan] tree to a list of
|
|
/// [InlineSpanSemanticsInformation] objects.
|
|
///
|
|
@@ -358,6 +373,14 @@ abstract class InlineSpan extends DiagnosticableTree {
|
|
bool includePlaceholders = true,
|
|
});
|
|
|
|
+ @protected
|
|
+ void computeToPlainTextV2(
|
|
+ StringBuffer buffer,
|
|
+ Map<int, String> map, {
|
|
+ bool includeSemanticsLabels = true,
|
|
+ bool includePlaceholders = true,
|
|
+ });
|
|
+
|
|
/// Returns the UTF-16 code unit at the given `index` in the flattened string.
|
|
///
|
|
/// This only accounts for the [TextSpan.text] values and ignores [PlaceholderSpan]s.
|
|
diff --git a/packages/flutter/lib/src/painting/placeholder_span.dart b/packages/flutter/lib/src/painting/placeholder_span.dart
|
|
index 110ff860310..7ad808d05bc 100644
|
|
--- a/packages/flutter/lib/src/painting/placeholder_span.dart
|
|
+++ b/packages/flutter/lib/src/painting/placeholder_span.dart
|
|
@@ -45,6 +45,7 @@ abstract class PlaceholderSpan extends InlineSpan {
|
|
this.alignment = ui.PlaceholderAlignment.bottom,
|
|
this.baseline,
|
|
super.style,
|
|
+ this.rawText,
|
|
});
|
|
|
|
/// The unicode character to represent a placeholder.
|
|
@@ -61,6 +62,8 @@ abstract class PlaceholderSpan extends InlineSpan {
|
|
/// This is ignored when using other alignment modes.
|
|
final TextBaseline? baseline;
|
|
|
|
+ final String? rawText;
|
|
+
|
|
/// [PlaceholderSpan]s are flattened to a `0xFFFC` object replacement character in the
|
|
/// plain text representation when `includePlaceholders` is true.
|
|
@override
|
|
@@ -74,6 +77,21 @@ abstract class PlaceholderSpan extends InlineSpan {
|
|
}
|
|
}
|
|
|
|
+ @override
|
|
+ void computeToPlainTextV2(
|
|
+ StringBuffer buffer,
|
|
+ Map<int, String> map, {
|
|
+ bool includeSemanticsLabels = true,
|
|
+ bool includePlaceholders = true,
|
|
+ }) {
|
|
+ if (includePlaceholders) {
|
|
+ if (rawText != null) {
|
|
+ map[buffer.length] = rawText!;
|
|
+ }
|
|
+ buffer.writeCharCode(placeholderCodeUnit);
|
|
+ }
|
|
+ }
|
|
+
|
|
@override
|
|
void computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector) {
|
|
collector.add(InlineSpanSemanticsInformation.placeholder);
|
|
diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart
|
|
index 09fba7fc4fb..57ee469358e 100644
|
|
--- a/packages/flutter/lib/src/painting/text_span.dart
|
|
+++ b/packages/flutter/lib/src/painting/text_span.dart
|
|
@@ -398,6 +398,31 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
|
|
}
|
|
}
|
|
|
|
+ @override
|
|
+ void computeToPlainTextV2(
|
|
+ StringBuffer buffer,
|
|
+ Map<int, String> map, {
|
|
+ bool includeSemanticsLabels = true,
|
|
+ bool includePlaceholders = true,
|
|
+ }) {
|
|
+ assert(debugAssertIsValid());
|
|
+ if (semanticsLabel != null && includeSemanticsLabels) {
|
|
+ buffer.write(semanticsLabel);
|
|
+ } else if (text != null) {
|
|
+ buffer.write(text);
|
|
+ }
|
|
+ if (children != null) {
|
|
+ for (final InlineSpan child in children!) {
|
|
+ child.computeToPlainTextV2(
|
|
+ buffer,
|
|
+ map,
|
|
+ includeSemanticsLabels: includeSemanticsLabels,
|
|
+ includePlaceholders: includePlaceholders,
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
@override
|
|
void computeSemanticsInformation(
|
|
List<InlineSpanSemanticsInformation> collector, {
|
|
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
|
|
index 97a41dafb8d..266dd6f0283 100644
|
|
--- a/packages/flutter/lib/src/rendering/paragraph.dart
|
|
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
|
|
@@ -502,27 +502,17 @@ class RenderParagraph extends RenderBox
|
|
}
|
|
|
|
List<_SelectableFragment> _getSelectableFragments() {
|
|
- final String plainText = text.toPlainText(includeSemanticsLabels: false);
|
|
- final result = <_SelectableFragment>[];
|
|
- var start = 0;
|
|
- while (start < plainText.length) {
|
|
- int end = plainText.indexOf(_placeholderCharacter, start);
|
|
- if (start != end) {
|
|
- if (end == -1) {
|
|
- end = plainText.length;
|
|
- }
|
|
- result.add(
|
|
- _SelectableFragment(
|
|
- paragraph: this,
|
|
- range: TextRange(start: start, end: end),
|
|
- fullText: plainText,
|
|
- ),
|
|
- );
|
|
- start = end;
|
|
- }
|
|
- start += 1;
|
|
- }
|
|
- return result;
|
|
+ final (String plainText, Map<int, String> placeHolder) = text.toPlainTextV2(
|
|
+ includeSemanticsLabels: false,
|
|
+ );
|
|
+ return [
|
|
+ _SelectableFragment(
|
|
+ paragraph: this,
|
|
+ range: TextRange(start: 0, end: plainText.length),
|
|
+ fullText: plainText,
|
|
+ placeholder: placeHolder,
|
|
+ ),
|
|
+ ];
|
|
}
|
|
|
|
/// Determines whether the given [Selectable] was created by this
|
|
@@ -1467,7 +1457,7 @@ class RenderParagraph extends RenderBox
|
|
class _SelectableFragment
|
|
with Selectable, Diagnosticable, ChangeNotifier
|
|
implements TextLayoutMetrics {
|
|
- _SelectableFragment({required this.paragraph, required this.fullText, required this.range})
|
|
+ _SelectableFragment({required this.paragraph, required this.fullText, required this.range, required this.placeholder})
|
|
: assert(range.isValid && !range.isCollapsed && range.isNormalized) {
|
|
if (kFlutterMemoryAllocationsEnabled) {
|
|
ChangeNotifier.maybeDispatchObjectCreation(this);
|
|
@@ -1478,6 +1468,7 @@ class _SelectableFragment
|
|
final TextRange range;
|
|
final RenderParagraph paragraph;
|
|
final String fullText;
|
|
+ final Map<int,String> placeholder;
|
|
|
|
TextPosition? _textSelectionStart;
|
|
TextPosition? _textSelectionEnd;
|
|
@@ -1509,18 +1500,36 @@ class _SelectableFragment
|
|
final int selectionStart = _textSelectionStart!.offset;
|
|
final int selectionEnd = _textSelectionEnd!.offset;
|
|
final bool isReversed = selectionStart > selectionEnd;
|
|
- final Offset startOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(
|
|
- TextPosition(offset: selectionStart),
|
|
- );
|
|
- final Offset endOffsetInParagraphCoordinates = selectionStart == selectionEnd
|
|
- ? startOffsetInParagraphCoordinates
|
|
- : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
|
|
+
|
|
final flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
|
|
final selection = TextSelection(baseOffset: selectionStart, extentOffset: selectionEnd);
|
|
+ final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(selection, boxHeightStyle: .max);
|
|
final selectionRects = <Rect>[];
|
|
- for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
|
|
+ for (final textBox in boxes) {
|
|
selectionRects.add(textBox.toRect());
|
|
}
|
|
+
|
|
+ final Offset startOffsetInParagraphCoordinates;
|
|
+ final Offset endOffsetInParagraphCoordinates;
|
|
+ if (boxes.isEmpty) {
|
|
+ final Offset offset =
|
|
+ paragraph._textPainter.getOffsetForCaret(selection.extent, Rect.zero) +
|
|
+ Offset(0, paragraph._textPainter.getFullHeightForCaret(selection.extent, Rect.zero));
|
|
+ startOffsetInParagraphCoordinates = endOffsetInParagraphCoordinates = Offset(
|
|
+ clampDouble(offset.dx, 0, paragraph._textPainter.width),
|
|
+ clampDouble(offset.dy, 0, paragraph._textPainter.height),
|
|
+ );
|
|
+ } else {
|
|
+ startOffsetInParagraphCoordinates = Offset(
|
|
+ clampDouble(boxes.first.start, 0, paragraph._textPainter.width),
|
|
+ clampDouble(boxes.first.bottom, 0, paragraph._textPainter.height),
|
|
+ );
|
|
+ endOffsetInParagraphCoordinates = Offset(
|
|
+ clampDouble(boxes.last.end, 0, paragraph._textPainter.width),
|
|
+ clampDouble(boxes.last.bottom, 0, paragraph._textPainter.height),
|
|
+ );
|
|
+ }
|
|
+
|
|
final selectionCollapsed = selectionStart == selectionEnd;
|
|
final (
|
|
TextSelectionHandleType startSelectionHandleType,
|
|
@@ -1564,12 +1573,14 @@ class _SelectableFragment
|
|
result = _updateSelectionEdge(
|
|
edgeUpdate.globalPosition,
|
|
isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
|
|
+ isSingle: edgeUpdate.isSingle,
|
|
);
|
|
case TextGranularity.word:
|
|
result = _updateSelectionEdgeByTextBoundary(
|
|
edgeUpdate.globalPosition,
|
|
isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
|
|
getTextBoundary: _getWordBoundaryAtPosition,
|
|
+ isSingle: edgeUpdate.isSingle,
|
|
);
|
|
case TextGranularity.paragraph:
|
|
result = _updateSelectionEdgeByMultiSelectableTextBoundary(
|
|
@@ -1626,9 +1637,33 @@ class _SelectableFragment
|
|
if (_textSelectionStart == null || _textSelectionEnd == null) {
|
|
return null;
|
|
}
|
|
- final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
|
+ int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
|
final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
|
- return SelectedContent(plainText: fullText.substring(start, end));
|
|
+ final String selectedText;
|
|
+ if (placeholder.isEmpty) {
|
|
+ selectedText = fullText.substring(start, end);
|
|
+ } else {
|
|
+ final buffer = StringBuffer();
|
|
+ for (final e in placeholder.entries) {
|
|
+ final i = e.key;
|
|
+ if (i < start) {
|
|
+ continue;
|
|
+ }
|
|
+ if (i >= end) {
|
|
+ break;
|
|
+ }
|
|
+ if (i != start) {
|
|
+ buffer.write(fullText.substring(start, i));
|
|
+ }
|
|
+ buffer.write(e.value);
|
|
+ start = i + 1;
|
|
+ }
|
|
+ if (start != end) {
|
|
+ buffer.write(fullText.substring(start, end));
|
|
+ }
|
|
+ selectedText = buffer.toString();
|
|
+ }
|
|
+ return SelectedContent(plainText: selectedText);
|
|
}
|
|
|
|
@override
|
|
@@ -1848,6 +1883,7 @@ class _SelectableFragment
|
|
Offset globalPosition, {
|
|
required bool isEnd,
|
|
required _TextBoundaryAtPosition getTextBoundary,
|
|
+ bool isSingle = false,
|
|
}) {
|
|
// When the start/end edges are swapped, i.e. the start is after the end, and
|
|
// the scrollable synthesizes an event for the opposite edge, this will potentially
|
|
@@ -1866,6 +1902,7 @@ class _SelectableFragment
|
|
_rect,
|
|
localPosition,
|
|
direction: paragraph.textDirection,
|
|
+ isSingle: isSingle,
|
|
);
|
|
|
|
final TextPosition position = paragraph.getPositionForOffset(adjustedOffset);
|
|
@@ -1920,7 +1957,7 @@ class _SelectableFragment
|
|
return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
|
|
}
|
|
|
|
- SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd}) {
|
|
+ SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd, bool isSingle = false}) {
|
|
_setSelectionPosition(null, isEnd: isEnd);
|
|
final Matrix4 transform = paragraph.getTransformTo(null);
|
|
transform.invert();
|
|
@@ -1932,11 +1969,21 @@ class _SelectableFragment
|
|
_rect,
|
|
localPosition,
|
|
direction: paragraph.textDirection,
|
|
+ isSingle: isSingle,
|
|
);
|
|
|
|
final TextPosition position = _clampTextPosition(
|
|
paragraph.getPositionForOffset(adjustedOffset),
|
|
);
|
|
+ // if (rawText != null) {
|
|
+ // position = _clampTextPosition(
|
|
+ // adjustedOffset.dx * 2 > _rect.left + _rect.right
|
|
+ // ? TextPosition(offset: range.end)
|
|
+ // : TextPosition(offset: range.start),
|
|
+ // );
|
|
+ // } else {
|
|
+ // position = _clampTextPosition(paragraph.getPositionForOffset(adjustedOffset));
|
|
+ // }
|
|
_setSelectionPosition(position, isEnd: isEnd);
|
|
if (position.offset == range.end) {
|
|
return SelectionResult.next;
|
|
@@ -3568,7 +3615,10 @@ class _SelectableFragment
|
|
final selectionPaint = Paint()
|
|
..style = PaintingStyle.fill
|
|
..color = paragraph.selectionColor!;
|
|
- for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
|
|
+ for (final TextBox textBox in paragraph.getBoxesForSelection(
|
|
+ selection,
|
|
+ boxHeightStyle: ui.BoxHeightStyle.max,
|
|
+ )) {
|
|
context.canvas.drawRect(textBox.toRect().shift(offset), selectionPaint);
|
|
}
|
|
}
|
|
diff --git a/packages/flutter/lib/src/rendering/selection.dart b/packages/flutter/lib/src/rendering/selection.dart
|
|
index a813e141dc4..e5ef2103645 100644
|
|
--- a/packages/flutter/lib/src/rendering/selection.dart
|
|
+++ b/packages/flutter/lib/src/rendering/selection.dart
|
|
@@ -6,6 +6,8 @@
|
|
/// @docImport 'package:flutter/material.dart';
|
|
library;
|
|
|
|
+import 'dart:math' as math;
|
|
+
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:vector_math/vector_math_64.dart';
|
|
|
|
@@ -114,6 +116,8 @@ abstract class SelectionHandler implements ValueListenable<SelectionGeometry> {
|
|
|
|
/// The length of the content in this object.
|
|
int get contentLength;
|
|
+
|
|
+ Object? get separator;
|
|
}
|
|
|
|
/// This class stores the range information of the selection under a [Selectable]
|
|
@@ -236,6 +240,8 @@ class SelectedContent with Diagnosticable {
|
|
/// See also:
|
|
/// * [SelectableRegion], which provides an overview of selection system.
|
|
mixin Selectable implements SelectionHandler {
|
|
+ Object? get separator => null;
|
|
+
|
|
/// {@macro flutter.rendering.RenderObject.getTransformTo}
|
|
Matrix4 getTransformTo(RenderObject? ancestor);
|
|
|
|
@@ -356,10 +362,19 @@ abstract final class SelectionUtils {
|
|
Rect targetRect,
|
|
Offset point, {
|
|
TextDirection direction = TextDirection.ltr,
|
|
+ bool isSingle = false,
|
|
}) {
|
|
if (targetRect.contains(point)) {
|
|
return point;
|
|
}
|
|
+
|
|
+ if (isSingle || (point.dy >= targetRect.top && point.dy <= targetRect.bottom)) {
|
|
+ return Offset(
|
|
+ math.min(point.dx, targetRect.right),
|
|
+ math.min(point.dy, targetRect.bottom - .1),
|
|
+ );
|
|
+ }
|
|
+
|
|
if (point.dy <= targetRect.top ||
|
|
point.dy <= targetRect.bottom && point.dx <= targetRect.left) {
|
|
// Area 1
|
|
@@ -520,7 +535,7 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
|
|
///
|
|
/// The [granularity] contains the granularity which the selection edge should move by.
|
|
/// This value defaults to [TextGranularity.character].
|
|
- const SelectionEdgeUpdateEvent.forStart({
|
|
+ SelectionEdgeUpdateEvent.forStart({
|
|
required this.globalPosition,
|
|
TextGranularity? granularity,
|
|
}) : granularity = granularity ?? TextGranularity.character,
|
|
@@ -532,7 +547,7 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
|
|
///
|
|
/// The [granularity] contains the granularity which the selection edge should move by.
|
|
/// This value defaults to [TextGranularity.character].
|
|
- const SelectionEdgeUpdateEvent.forEnd({
|
|
+ SelectionEdgeUpdateEvent.forEnd({
|
|
required this.globalPosition,
|
|
TextGranularity? granularity,
|
|
}) : granularity = granularity ?? TextGranularity.character,
|
|
@@ -547,6 +562,8 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
|
|
///
|
|
/// Defaults to [TextGranularity.character].
|
|
final TextGranularity granularity;
|
|
+
|
|
+ bool isSingle = false;
|
|
}
|
|
|
|
/// Extends the start or end of the selection by a given [TextGranularity].
|
|
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
|
|
index 9df2d633f16..54d30883245 100644
|
|
--- a/packages/flutter/lib/src/widgets/scrollable.dart
|
|
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
|
|
@@ -1177,6 +1177,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
|
|
_position.addListener(_scheduleLayoutChange);
|
|
}
|
|
|
|
+ @override
|
|
+ Object separator = '\n';
|
|
+
|
|
// Pointer drag is a single point, it should not have a size.
|
|
static const double _kDefaultDragTargetSize = 0;
|
|
|
|
diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart
|
|
index 59de8bae20b..9ce4810aa68 100644
|
|
--- a/packages/flutter/lib/src/widgets/selectable_region.dart
|
|
+++ b/packages/flutter/lib/src/widgets/selectable_region.dart
|
|
@@ -401,9 +401,11 @@ class SelectableRegionState extends State<SelectableRegion>
|
|
final LayerLink _startHandleLayerLink = LayerLink();
|
|
final LayerLink _endHandleLayerLink = LayerLink();
|
|
final LayerLink _toolbarLayerLink = LayerLink();
|
|
- final StaticSelectionContainerDelegate _selectionDelegate = StaticSelectionContainerDelegate();
|
|
+ final StaticSelectionContainerDelegate _selectionDelegate = StaticSelectionContainerDelegate(separator: '\n');
|
|
+ StaticSelectionContainerDelegate get selectionDelegate => _selectionDelegate;
|
|
// there should only ever be one selectable, which is the SelectionContainer.
|
|
Selectable? _selectable;
|
|
+ Selectable? get selectable => _selectable;
|
|
|
|
bool get _hasSelectionOverlayGeometry =>
|
|
_selectionDelegate.value.startSelectionPoint != null ||
|
|
@@ -599,48 +601,32 @@ class SelectableRegionState extends State<SelectableRegion>
|
|
//
|
|
// This method should be used in all instances when details.consecutiveTapCount
|
|
// would be used.
|
|
- int _getEffectiveConsecutiveTapCount(int rawCount) {
|
|
- var maxConsecutiveTap = 3;
|
|
+ static int _getEffectiveConsecutiveTapCount(int rawCount) {
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
- if (_lastPointerDeviceKind != null && _lastPointerDeviceKind != PointerDeviceKind.mouse) {
|
|
- // When the pointer device kind is not precise like a mouse, native
|
|
- // Android resets the tap count at 2. For example, this is so the
|
|
- // selection can collapse on the third tap.
|
|
- maxConsecutiveTap = 2;
|
|
- }
|
|
- // From observation, these platforms reset their tap count to 0 when
|
|
- // the number of consecutive taps exceeds the max consecutive tap supported.
|
|
- // For example on native Android, when going past a triple click,
|
|
- // on the fourth click the selection is moved to the precise click
|
|
- // position, on the fifth click the word at the position is selected, and
|
|
- // on the sixth click the paragraph at the position is selected.
|
|
- return rawCount <= maxConsecutiveTap
|
|
- ? rawCount
|
|
- : (rawCount % maxConsecutiveTap == 0
|
|
- ? maxConsecutiveTap
|
|
- : rawCount % maxConsecutiveTap);
|
|
case TargetPlatform.linux:
|
|
- // From observation, these platforms reset their tap count to 0 when
|
|
- // the number of consecutive taps exceeds the max consecutive tap supported.
|
|
- // For example on Debian Linux with GTK, when going past a triple click,
|
|
- // on the fourth click the selection is moved to the precise click
|
|
- // position, on the fifth click the word at the position is selected, and
|
|
- // on the sixth click the paragraph at the position is selected.
|
|
- return rawCount <= maxConsecutiveTap
|
|
- ? rawCount
|
|
- : (rawCount % maxConsecutiveTap == 0
|
|
- ? maxConsecutiveTap
|
|
- : rawCount % maxConsecutiveTap);
|
|
+ // From observation, these platform's reset their tap count to 0 when
|
|
+ // the number of consecutive taps exceeds 3. For example on Debian Linux
|
|
+ // with GTK, when going past a triple click, on the fourth click the
|
|
+ // selection is moved to the precise click position, on the fifth click
|
|
+ // the word at the position is selected, and on the sixth click the
|
|
+ // paragraph at the position is selected.
|
|
+ return rawCount <= 3 ? rawCount : (rawCount % 3 == 0 ? 3 : rawCount % 3);
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
+ // From observation, these platform's either hold their tap count at 3.
|
|
+ // For example on macOS, when going past a triple click, the selection
|
|
+ // should be retained at the paragraph that was first selected on triple
|
|
+ // click.
|
|
+ return min(rawCount, 3);
|
|
case TargetPlatform.windows:
|
|
- // From observation, these platforms hold their tap count at the max
|
|
- // consecutive tap supported. For example on macOS, when going past a triple
|
|
- // click, the selection should be retained at the paragraph that was first
|
|
- // selected on triple click.
|
|
- return min(rawCount, maxConsecutiveTap);
|
|
+ // From observation, this platform's consecutive tap actions alternate
|
|
+ // between double click and triple click actions. For example, after a
|
|
+ // triple click has selected a paragraph, on the next click the word at
|
|
+ // the clicked position will be selected, and on the next click the
|
|
+ // paragraph at the position is selected.
|
|
+ return rawCount < 2 ? rawCount : 2 + rawCount % 2;
|
|
}
|
|
}
|
|
|
|
@@ -782,22 +768,8 @@ class SelectableRegionState extends State<SelectableRegion>
|
|
_selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
|
|
}
|
|
case 3:
|
|
- switch (defaultTargetPlatform) {
|
|
- case TargetPlatform.android:
|
|
- case TargetPlatform.fuchsia:
|
|
- case TargetPlatform.iOS:
|
|
- if (details.kind != null && _isPrecisePointerDevice(details.kind!)) {
|
|
- // Triple tap on static text is only supported on mobile
|
|
- // platforms using a precise pointer device.
|
|
- _selectParagraphAt(offset: details.globalPosition);
|
|
- _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
|
|
- }
|
|
- case TargetPlatform.macOS:
|
|
- case TargetPlatform.linux:
|
|
- case TargetPlatform.windows:
|
|
- _selectParagraphAt(offset: details.globalPosition);
|
|
- _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
|
|
- }
|
|
+ _selectParagraphAt(offset: details.globalPosition);
|
|
+ _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing;
|
|
}
|
|
_updateSelectedContentIfNeeded();
|
|
}
|
|
@@ -1049,10 +1021,10 @@ class SelectableRegionState extends State<SelectableRegion>
|
|
case TargetPlatform.windows:
|
|
// If _lastSecondaryTapDownPosition is within the current selection then
|
|
// keep the current selection, if not then collapse it.
|
|
- final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(
|
|
- globalPosition: details.globalPosition,
|
|
- );
|
|
- if (lastSecondaryTapDownPositionWasOnActiveSelection) {
|
|
+ // final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(
|
|
+ // globalPosition: details.globalPosition,
|
|
+ // );
|
|
+ if (_selectionDelegate.value.selectionRects.isNotEmpty) {
|
|
// Restore _lastSecondaryTapDownPosition since it may be cleared if a user
|
|
// accesses contextMenuAnchors.
|
|
_lastSecondaryTapDownPosition = details.globalPosition;
|
|
@@ -1957,6 +1929,8 @@ class SelectableRegionState extends State<SelectableRegion>
|
|
// the region on non-web platforms.
|
|
if (kIsWeb) {
|
|
_focusNode.unfocus();
|
|
+ } else {
|
|
+ clearSelection();
|
|
}
|
|
},
|
|
child: CompositedTransformTarget(
|
|
@@ -2087,6 +2061,11 @@ class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovemen
|
|
/// * [MultiSelectableSelectionContainerDelegate], for the class that provides
|
|
/// the main implementation details of this [SelectionContainerDelegate].
|
|
class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
|
|
+ StaticSelectionContainerDelegate({this.separator});
|
|
+
|
|
+ @override
|
|
+ final Object? separator;
|
|
+
|
|
/// The set of [Selectable]s that have received start events.
|
|
final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};
|
|
|
|
@@ -2271,6 +2250,18 @@ class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainer
|
|
super.dispose();
|
|
}
|
|
|
|
+ void handleSelectionEdgeUpdateEvent(SelectionEdgeUpdateEvent event) {
|
|
+ event.isSingle = isSingle;
|
|
+ }
|
|
+
|
|
+ @override
|
|
+ SelectionResult dispatchSelectionEvent(SelectionEvent event) {
|
|
+ if (event is SelectionEdgeUpdateEvent) {
|
|
+ handleSelectionEdgeUpdateEvent(event);
|
|
+ }
|
|
+ return super.dispatchSelectionEvent(event);
|
|
+ }
|
|
+
|
|
@override
|
|
SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
|
|
switch (event.type) {
|
|
@@ -2367,6 +2358,8 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|
/// Gets the list of [Selectable]s this delegate is managing.
|
|
List<Selectable> selectables = <Selectable>[];
|
|
|
|
+ bool get isSingle => selectables.length == 1;
|
|
+
|
|
/// The number of additional pixels added to the selection handle drawable
|
|
/// area.
|
|
///
|
|
@@ -2822,8 +2815,17 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|
return null;
|
|
}
|
|
final buffer = StringBuffer();
|
|
- for (final selection in selections) {
|
|
- buffer.write(selection.plainText);
|
|
+ if (separator != null) {
|
|
+ for (final (i, selection) in selections.indexed) {
|
|
+ if (i != 0) {
|
|
+ buffer.write(separator);
|
|
+ }
|
|
+ buffer.write(selection.plainText);
|
|
+ }
|
|
+ } else {
|
|
+ for (final selection in selections) {
|
|
+ buffer.write(selection.plainText);
|
|
+ }
|
|
}
|
|
return SelectedContent(plainText: buffer.toString());
|
|
}
|
|
diff --git a/packages/flutter/lib/src/widgets/selection_container.dart b/packages/flutter/lib/src/widgets/selection_container.dart
|
|
index 5d5a2386a7a..92e7d1eb70a 100644
|
|
--- a/packages/flutter/lib/src/widgets/selection_container.dart
|
|
+++ b/packages/flutter/lib/src/widgets/selection_container.dart
|
|
@@ -270,6 +270,8 @@ class SelectionRegistrarScope extends InheritedWidget {
|
|
/// This delegate needs to implement [SelectionRegistrar] to register
|
|
/// [Selectable]s in the [SelectionContainer] subtree.
|
|
abstract class SelectionContainerDelegate implements SelectionHandler, SelectionRegistrar {
|
|
+ Object? get separator => null;
|
|
+
|
|
BuildContext? _selectionContainerContext;
|
|
|
|
/// Gets the paint transform from the [Selectable] child to
|
|
diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart
|
|
index a3aa5fde862..8fbf6a1b16d 100644
|
|
--- a/packages/flutter/lib/src/widgets/text.dart
|
|
+++ b/packages/flutter/lib/src/widgets/text.dart
|
|
@@ -992,6 +992,9 @@ class _SelectableTextContainerDelegate extends StaticSelectionContainerDelegate
|
|
final GlobalKey _textKey;
|
|
RenderParagraph get paragraph => _textKey.currentContext!.findRenderObject()! as RenderParagraph;
|
|
|
|
+ @override
|
|
+ void handleSelectionEdgeUpdateEvent(SelectionEdgeUpdateEvent event) {}
|
|
+
|
|
@override
|
|
SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
|
|
final SelectionResult result = _handleSelectParagraph(event);
|
|
diff --git a/packages/flutter/lib/src/widgets/widget_span.dart b/packages/flutter/lib/src/widgets/widget_span.dart
|
|
index 09da3b76525..4a83efb0392 100644
|
|
--- a/packages/flutter/lib/src/widgets/widget_span.dart
|
|
+++ b/packages/flutter/lib/src/widgets/widget_span.dart
|
|
@@ -78,7 +78,7 @@ class WidgetSpan extends PlaceholderSpan {
|
|
///
|
|
/// A [TextStyle] may be provided with the [style] property, but only the
|
|
/// decoration, foreground, background, and spacing options will be used.
|
|
- const WidgetSpan({required this.child, super.alignment, super.baseline, super.style})
|
|
+ const WidgetSpan({required this.child, super.alignment, super.baseline, super.style, super.rawText})
|
|
: assert(
|
|
baseline != null ||
|
|
!(identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
|