mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-08 18:20:12 +08:00
feat: cross row select (#2437)
* feat(wip): cross row select * fix patch Signed-off-by: dom <githubaccount56556@proton.me> * update Signed-off-by: dom <githubaccount56556@proton.me> --------- Co-authored-by: dom <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
204715266f
commit
b540647222
249
lib/scripts/null_safety_for_selectable_region.patch
Normal file
249
lib/scripts/null_safety_for_selectable_region.patch
Normal file
@@ -0,0 +1,249 @@
|
||||
diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||
index 59de8bae20b..5a6923f3a6a 100644
|
||||
--- a/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||
+++ b/packages/flutter/lib/src/widgets/selectable_region.dart
|
||||
@@ -29,8 +29,10 @@ import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'magnifier.dart';
|
||||
import 'media_query.dart';
|
||||
+import 'notification_listener.dart';
|
||||
import 'overlay.dart';
|
||||
import 'platform_selectable_region_context_menu.dart';
|
||||
+import 'scroll_notification.dart';
|
||||
import 'selection_container.dart';
|
||||
import 'tap_region.dart';
|
||||
import 'text_editing_intents.dart';
|
||||
@@ -1775,12 +1777,12 @@ class SelectableRegionState extends State<SelectableRegion>
|
||||
|
||||
/// The line height at the start of the current selection.
|
||||
double get startGlyphHeight {
|
||||
- return _selectionDelegate.value.startSelectionPoint!.lineHeight;
|
||||
+ return _selectionDelegate.value.startSelectionPoint?.lineHeight ?? 0;
|
||||
}
|
||||
|
||||
/// The line height at the end of the current selection.
|
||||
double get endGlyphHeight {
|
||||
- return _selectionDelegate.value.endSelectionPoint!.lineHeight;
|
||||
+ return _selectionDelegate.value.endSelectionPoint?.lineHeight ?? 0;
|
||||
}
|
||||
|
||||
/// Returns the local coordinates of the endpoints of the current selection.
|
||||
@@ -1946,7 +1948,7 @@ class SelectableRegionState extends State<SelectableRegion>
|
||||
if (_webContextMenuEnabled) {
|
||||
result = PlatformSelectableRegionContextMenu(child: result);
|
||||
}
|
||||
- return TapRegion(
|
||||
+ final tapRegion = TapRegion(
|
||||
groupId: SelectableRegion,
|
||||
onTapOutside: (PointerDownEvent event) {
|
||||
// To match the native web behavior, this selectable region is
|
||||
@@ -1976,6 +1978,22 @@ class SelectableRegionState extends State<SelectableRegion>
|
||||
),
|
||||
),
|
||||
);
|
||||
+ return NotificationListener<ScrollEndNotification>(
|
||||
+ onNotification: (notification) {
|
||||
+ _selectionDelegate.layoutDidChange();
|
||||
+ _updateSelectedContentIfNeeded();
|
||||
+ final SelectedContent? lastSelection = _lastSelectedContent;
|
||||
+ if (lastSelection != null && lastSelection.plainText.isNotEmpty) {
|
||||
+ _showHandles();
|
||||
+ final SelectionOverlay? selectionOverlay = _selectionOverlay;
|
||||
+ if (selectionOverlay == null || !selectionOverlay.toolbarIsVisible) {
|
||||
+ _showToolbar();
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+ },
|
||||
+ child: tapRegion,
|
||||
+ );
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/packages/flutter/test/widgets/selectable_region_test.dart b/packages/flutter/test/widgets/selectable_region_test.dart
|
||||
index f7bd4071c8a..a82a9448120 100644
|
||||
--- a/packages/flutter/test/widgets/selectable_region_test.dart
|
||||
+++ b/packages/flutter/test/widgets/selectable_region_test.dart
|
||||
@@ -841,6 +841,125 @@ void main() {
|
||||
expect(renderSelectionSpy.events.length, 0);
|
||||
});
|
||||
|
||||
+ testWidgets(
|
||||
+ 'SelectionArea + ListView: select across multiple lines and scroll does not crash',
|
||||
+ (WidgetTester tester) async {
|
||||
+ // Regression test: when selection spans across ListView items and user scrolls,
|
||||
+ // startSelectionPoint/endSelectionPoint may be null (off-screen). Verifies that
|
||||
+ // startGlyphHeight/endGlyphHeight return 0 and do not crash.
|
||||
+ await tester.pumpWidget(
|
||||
+ MaterialApp(
|
||||
+ home: SelectableRegion(
|
||||
+ selectionControls: materialTextSelectionControls,
|
||||
+ child: SizedBox(
|
||||
+ height: 400,
|
||||
+ child: ListView(
|
||||
+ children: List<Widget>.generate(
|
||||
+ 15,
|
||||
+ (int i) => Text(
|
||||
+ 'Line $i: Selectable text content for testing',
|
||||
+ style: const TextStyle(fontSize: 20),
|
||||
+ ),
|
||||
+ ),
|
||||
+ ),
|
||||
+ ),
|
||||
+ ),
|
||||
+ ),
|
||||
+ );
|
||||
+ await tester.pumpAndSettle();
|
||||
+
|
||||
+ final RenderParagraph firstParagraph = tester.renderObject<RenderParagraph>(
|
||||
+ find.descendant(
|
||||
+ of: find.text('Line 0: Selectable text content for testing'),
|
||||
+ matching: find.byType(RichText),
|
||||
+ ),
|
||||
+ );
|
||||
+ final RenderParagraph thirdParagraph = tester.renderObject<RenderParagraph>(
|
||||
+ find.descendant(
|
||||
+ of: find.text('Line 2: Selectable text content for testing'),
|
||||
+ matching: find.byType(RichText),
|
||||
+ ),
|
||||
+ );
|
||||
+
|
||||
+ // Select from first line to third line (cross-line selection).
|
||||
+ final TestGesture selectGesture = await tester.startGesture(
|
||||
+ textOffsetToPosition(firstParagraph, 2),
|
||||
+ kind: PointerDeviceKind.mouse,
|
||||
+ );
|
||||
+ addTearDown(selectGesture.removePointer);
|
||||
+ await tester.pump();
|
||||
+ await selectGesture.moveTo(textOffsetToPosition(thirdParagraph, 15));
|
||||
+ await tester.pump();
|
||||
+ await selectGesture.up();
|
||||
+ await tester.pumpAndSettle();
|
||||
+
|
||||
+ // Verify selection exists.
|
||||
+ expect(firstParagraph.selections.isNotEmpty, isTrue);
|
||||
+ expect(thirdParagraph.selections.isNotEmpty, isTrue);
|
||||
+
|
||||
+ // Simulate scroll - this may cause selection points to become null when off-screen.
|
||||
+ await tester.drag(find.byType(ListView), const Offset(0, -150));
|
||||
+ await tester.pumpAndSettle();
|
||||
+
|
||||
+ // Should not throw - startGlyphHeight/endGlyphHeight return 0 when points are null.
|
||||
+ expect(tester.takeException(), isNull);
|
||||
+ },
|
||||
+ skip: kIsWeb, // [intended] Web uses its native context menu.
|
||||
+ );
|
||||
+
|
||||
+ testWidgets(
|
||||
+ 'startGlyphHeight and endGlyphHeight return 0 when startSelectionPoint/endSelectionPoint are null',
|
||||
+ (WidgetTester tester) async {
|
||||
+ // Directly tests the fix: when SelectionGeometry has null start/end points
|
||||
+ // (e.g. selection off-screen in scrollable), startGlyphHeight and
|
||||
+ // endGlyphHeight must not crash and must return 0.
|
||||
+ final delegateKey = GlobalKey<NullSelectionPointDelegateWidgetState>();
|
||||
+
|
||||
+ await tester.pumpWidget(
|
||||
+ MaterialApp(
|
||||
+ home: SelectableRegion(
|
||||
+ selectionControls: materialTextSelectionControls,
|
||||
+ child: NullSelectionPointDelegateWidget(
|
||||
+ key: delegateKey,
|
||||
+ child: const Column(
|
||||
+ children: <Widget>[Text('First'), Text('Second'), Text('Third')],
|
||||
+ ),
|
||||
+ ),
|
||||
+ ),
|
||||
+ ),
|
||||
+ );
|
||||
+ await tester.pumpAndSettle();
|
||||
+
|
||||
+ final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
|
||||
+ find.descendant(of: find.text('Second'), matching: find.byType(RichText)),
|
||||
+ );
|
||||
+ final TestGesture gesture = await tester.startGesture(
|
||||
+ textOffsetToPosition(paragraph, 2),
|
||||
+ kind: PointerDeviceKind.mouse,
|
||||
+ );
|
||||
+ addTearDown(gesture.removePointer);
|
||||
+ await gesture.moveTo(textOffsetToPosition(paragraph, 5));
|
||||
+ await gesture.up();
|
||||
+ await tester.pumpAndSettle();
|
||||
+
|
||||
+ // Switch delegate to return null selection points (simulates off-screen scenario).
|
||||
+ final NullSelectionPointDelegate delegate = delegateKey.currentState!.delegate;
|
||||
+ delegate.useNullSelectionPoints = true;
|
||||
+ delegate.notifyListeners();
|
||||
+ await tester.pump();
|
||||
+
|
||||
+ final SelectableRegionState state = tester.state<SelectableRegionState>(
|
||||
+ find.byType(SelectableRegion),
|
||||
+ );
|
||||
+ // Access the getters - should not throw, must return 0 when points are null.
|
||||
+ expect(() => state.startGlyphHeight, returnsNormally);
|
||||
+ expect(() => state.endGlyphHeight, returnsNormally);
|
||||
+ expect(state.startGlyphHeight, 0);
|
||||
+ expect(state.endGlyphHeight, 0);
|
||||
+ },
|
||||
+ skip: kIsWeb, // [intended] Web uses its native context menu.
|
||||
+ );
|
||||
+
|
||||
testWidgets('mouse long press does not send select-word event', (WidgetTester tester) async {
|
||||
final spy = UniqueKey();
|
||||
|
||||
@@ -6690,6 +6809,56 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
+/// A [SelectionContainerDelegate] that can return [SelectionGeometry] with null
|
||||
+/// [startSelectionPoint] and [endSelectionPoint] to test off-screen selection handling.
|
||||
+class NullSelectionPointDelegate extends StaticSelectionContainerDelegate {
|
||||
+ bool useNullSelectionPoints = false;
|
||||
+
|
||||
+ @override
|
||||
+ SelectionGeometry get value {
|
||||
+ final SelectionGeometry geometry = super.value;
|
||||
+ if (useNullSelectionPoints && geometry.hasSelection) {
|
||||
+ return SelectionGeometry(
|
||||
+ status: geometry.status,
|
||||
+ hasContent: geometry.hasContent,
|
||||
+ selectionRects: geometry.selectionRects,
|
||||
+ );
|
||||
+ }
|
||||
+ return geometry;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/// Widget that holds [NullSelectionPointDelegate] for testing.
|
||||
+class NullSelectionPointDelegateWidget extends StatefulWidget {
|
||||
+ const NullSelectionPointDelegateWidget({super.key, required this.child});
|
||||
+
|
||||
+ final Widget child;
|
||||
+
|
||||
+ @override
|
||||
+ State<NullSelectionPointDelegateWidget> createState() => NullSelectionPointDelegateWidgetState();
|
||||
+}
|
||||
+
|
||||
+class NullSelectionPointDelegateWidgetState extends State<NullSelectionPointDelegateWidget> {
|
||||
+ late final NullSelectionPointDelegate delegate;
|
||||
+
|
||||
+ @override
|
||||
+ void initState() {
|
||||
+ super.initState();
|
||||
+ delegate = NullSelectionPointDelegate();
|
||||
+ }
|
||||
+
|
||||
+ @override
|
||||
+ void dispose() {
|
||||
+ delegate.dispose();
|
||||
+ super.dispose();
|
||||
+ }
|
||||
+
|
||||
+ @override
|
||||
+ Widget build(BuildContext context) {
|
||||
+ return SelectionContainer(delegate: delegate, child: widget.child);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
class ColumnSelectionContainerDelegate extends StaticSelectionContainerDelegate {
|
||||
/// Copies the selected contents of all [Selectable]s, separating their
|
||||
/// contents with a new line.
|
||||
Reference in New Issue
Block a user