fix[widget_inspector]: Widget Inspector Directionality & Move button tooltip. (#169425)

Fix Widget Inspector buttons direction & arrow button tooltip text on
when the App locale is RTL:

Before:

![before_1](https://github.com/user-attachments/assets/c0cd3f65-d524-4e49-a946-4736a4f3890a)

![before_2](https://github.com/user-attachments/assets/3c8d2f4a-f2be-4c12-a2a0-696736f2b2e9)

After:

![after_en1](https://github.com/user-attachments/assets/617293f6-d047-4fc0-8e3d-1de9608b48c6)

![after_en2](https://github.com/user-attachments/assets/4bff323c-3814-489f-bd90-d213b71192f1)

![after_ar1](https://github.com/user-attachments/assets/50f3d78b-d403-4d31-b6c9-4a5ad25da7cd)

![after_ar2](https://github.com/user-attachments/assets/601dcfdf-44d8-4bb3-9696-cf87ddaff911)


## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- 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

---------

Co-authored-by: Kenzie Davisson <43759233+kenzieschmoll@users.noreply.github.com>
This commit is contained in:
Muhammad Kamel 2025-06-03 18:18:24 +03:00 committed by GitHub
parent 03dbf1a99c
commit ec17f9b297
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 112 additions and 34 deletions

View File

@ -550,12 +550,12 @@ class _CupertinoAppState extends State<CupertinoApp> {
BuildContext context, {
required VoidCallback onPressed,
required String semanticsLabel,
bool isLeftAligned = true,
bool usesDefaultAlignment = true,
}) {
return _CupertinoInspectorButton.iconOnly(
onPressed: onPressed,
semanticsLabel: semanticsLabel,
icon: isLeftAligned ? CupertinoIcons.arrow_right : CupertinoIcons.arrow_left,
icon: usesDefaultAlignment ? CupertinoIcons.arrow_right : CupertinoIcons.arrow_left,
);
}

View File

@ -951,12 +951,12 @@ class _MaterialAppState extends State<MaterialApp> {
BuildContext context, {
required VoidCallback onPressed,
required String semanticsLabel,
bool isLeftAligned = true,
bool usesDefaultAlignment = true,
}) {
return _MaterialInspectorButton.iconOnly(
onPressed: onPressed,
semanticsLabel: semanticsLabel,
icon: isLeftAligned ? Icons.arrow_right : Icons.arrow_left,
icon: usesDefaultAlignment ? Icons.arrow_right : Icons.arrow_left,
isDarkTheme: _isDarkTheme(context),
);
}

View File

@ -55,7 +55,7 @@ typedef MoveExitWidgetSelectionButtonBuilder =
BuildContext context, {
required VoidCallback onPressed,
required String semanticsLabel,
bool isLeftAligned,
bool usesDefaultAlignment,
});
/// Signature for the builder callback used by
@ -3737,7 +3737,11 @@ class _WidgetInspectorButtonGroupState extends State<_WidgetInspectorButtonGroup
String? _tooltipMessage;
bool _leftAligned = true;
/// Indicates whether the button is using the default alignment based on text direction.
///
/// For LTR, the default alignment is on the left.
/// For RTL, the default alignment is on the right.
bool _usesDefaultAlignment = true;
ValueNotifier<bool> get _selectionOnTapEnabled =>
WidgetsBinding.instance.debugWidgetInspectorSelectionOnTapEnabled;
@ -3749,7 +3753,11 @@ class _WidgetInspectorButtonGroupState extends State<_WidgetInspectorButtonGroup
return null;
}
final String buttonLabel = 'Move to the ${_leftAligned ? 'right' : 'left'}';
final TextDirection textDirection = Directionality.of(context);
final String buttonLabel =
'Move to the ${_usesDefaultAlignment == (textDirection == TextDirection.ltr) ? 'right' : 'left'}';
return _WidgetInspectorButton(
button: buttonBuilder(
context,
@ -3758,7 +3766,7 @@ class _WidgetInspectorButtonGroupState extends State<_WidgetInspectorButtonGroup
_onTooltipHidden();
},
semanticsLabel: buttonLabel,
isLeftAligned: _leftAligned,
usesDefaultAlignment: _usesDefaultAlignment,
),
onTooltipVisible: () {
_changeTooltipMessage(buttonLabel);
@ -3819,24 +3827,25 @@ class _WidgetInspectorButtonGroupState extends State<_WidgetInspectorButtonGroup
painter: _ExitWidgetSelectionTooltipPainter(
tooltipMessage: _tooltipMessage,
buttonKey: _exitWidgetSelectionButtonKey,
isLeftAligned: _leftAligned,
usesDefaultAlignment: _usesDefaultAlignment,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (_leftAligned) selectionModeButtons,
if (_usesDefaultAlignment) selectionModeButtons,
if (_moveExitWidgetSelectionButton != null) _moveExitWidgetSelectionButton!,
if (!_leftAligned) selectionModeButtons,
if (!_usesDefaultAlignment) selectionModeButtons,
],
),
],
);
return Positioned(
left: _leftAligned ? _kExitWidgetSelectionButtonMargin : null,
right: _leftAligned ? null : _kExitWidgetSelectionButtonMargin,
return Positioned.directional(
textDirection: Directionality.of(context),
start: _usesDefaultAlignment ? _kExitWidgetSelectionButtonMargin : null,
end: _usesDefaultAlignment ? null : _kExitWidgetSelectionButtonMargin,
bottom: _kExitWidgetSelectionButtonMargin,
child: buttonGroup,
);
@ -3868,7 +3877,7 @@ class _WidgetInspectorButtonGroupState extends State<_WidgetInspectorButtonGroup
void _changeButtonGroupAlignment() {
if (mounted) {
setState(() {
_leftAligned = !_leftAligned;
_usesDefaultAlignment = !_usesDefaultAlignment;
});
}
}
@ -3974,12 +3983,12 @@ class _ExitWidgetSelectionTooltipPainter extends CustomPainter {
_ExitWidgetSelectionTooltipPainter({
required this.tooltipMessage,
required this.buttonKey,
required this.isLeftAligned,
required this.usesDefaultAlignment,
});
final String? tooltipMessage;
final GlobalKey buttonKey;
final bool isLeftAligned;
final bool usesDefaultAlignment;
@override
void paint(Canvas canvas, Size size) {
@ -4021,7 +4030,7 @@ class _ExitWidgetSelectionTooltipPainter extends CustomPainter {
final double tooltipHeight = textHeight + (tooltipPadding * 2);
final double tooltipXOffset =
isLeftAligned ? 0 - buttonWidth : 0 - (tooltipWidth - buttonWidth);
usesDefaultAlignment ? 0 - buttonWidth : 0 - (tooltipWidth - buttonWidth);
final double tooltipYOffset = 0 - tooltipHeight - tooltipSpacing;
// Draw tooltip background.

View File

@ -996,11 +996,9 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
);
testWidgets(
'WidgetInspector Move Exit Selection Mode button to the right / left',
'[LTR] WidgetInspector Move Exit Selection Mode button to the right then left',
(WidgetTester tester) async {
// Enable widget selection mode.
WidgetInspectorService.instance.isSelectMode = true;
final GlobalKey inspectorKey = GlobalKey();
setupDefaultPubRootDirectory(service);
@ -1023,12 +1021,12 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
BuildContext context, {
required VoidCallback onPressed,
required String semanticsLabel,
bool isLeftAligned = true,
bool usesDefaultAlignment = true,
}) {
return Material(
child: ElevatedButton(
onPressed: onPressed,
child: Text(isLeftAligned ? 'MOVE RIGHT' : 'MOVE LEFT'),
child: Text(usesDefaultAlignment ? 'MOVE RIGHT' : 'MOVE LEFT'),
),
);
}
@ -1050,33 +1048,104 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
),
);
// Initially the exit select button is on the left.
final Finder exitButton = buttonFinder('EXIT SELECT MODE');
expect(exitButton, findsOneWidget);
final Finder moveRightButton = buttonFinder('MOVE RIGHT');
expect(moveRightButton, findsOneWidget);
final double initialExitButtonX = tester.getCenter(exitButton).dx;
// Move the button to the right.
await tester.tap(moveRightButton);
await tester.pump();
// Verify the button is now on the right.
expect(moveRightButton, findsNothing);
final Finder moveLeftButton = buttonFinder('MOVE LEFT');
expect(moveLeftButton, findsOneWidget);
final double exitButtonXAfterMovingRight = tester.getCenter(exitButton).dx;
expect(initialExitButtonX, lessThan(exitButtonXAfterMovingRight));
final double movedExitButtonX = tester.getCenter(exitButton).dx;
expect(initialExitButtonX, lessThan(movedExitButtonX), reason: 'LTR: should move right');
// Move the button to the left again.
await tester.tap(moveLeftButton);
await tester.pump();
// Verify the button is in its original position.
expect(moveLeftButton, findsNothing);
final double finalExitButtonX = tester.getCenter(exitButton).dx;
expect(finalExitButtonX, equals(initialExitButtonX));
},
// [intended] Test requires --track-widget-creation flag.
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(),
);
testWidgets(
'[RTL] WidgetInspector Move Exit Selection Mode button to the left then right',
(WidgetTester tester) async {
WidgetInspectorService.instance.isSelectMode = true;
final GlobalKey inspectorKey = GlobalKey();
setupDefaultPubRootDirectory(service);
Widget exitWidgetSelectionButtonBuilder(
BuildContext context, {
required VoidCallback onPressed,
required String semanticsLabel,
required GlobalKey key,
}) {
return Material(
child: ElevatedButton(
onPressed: onPressed,
key: key,
child: const Text('EXIT SELECT MODE'),
),
);
}
Widget moveWidgetSelectionButtonBuilder(
BuildContext context, {
required VoidCallback onPressed,
required String semanticsLabel,
bool usesDefaultAlignment = true,
}) {
return Material(
child: ElevatedButton(
onPressed: onPressed,
child: Text(usesDefaultAlignment ? 'MOVE RIGHT' : 'MOVE LEFT'),
),
);
}
Finder buttonFinder(String buttonText) {
return find.ancestor(of: find.text(buttonText), matching: find.byType(ElevatedButton));
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: WidgetInspector(
key: inspectorKey,
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
moveExitWidgetSelectionButtonBuilder: moveWidgetSelectionButtonBuilder,
tapBehaviorButtonBuilder: null,
child: const Text('APP'),
),
),
);
final Finder exitButton = buttonFinder('EXIT SELECT MODE');
expect(exitButton, findsOneWidget);
final Finder moveRightButton = buttonFinder('MOVE RIGHT');
expect(moveRightButton, findsOneWidget);
final double exitButtonXAfterMovingLeft = tester.getCenter(exitButton).dx;
expect(exitButtonXAfterMovingLeft, equals(initialExitButtonX));
final double initialExitButtonX = tester.getCenter(exitButton).dx;
await tester.tap(moveRightButton);
await tester.pump();
final Finder moveLeftButton = buttonFinder('MOVE LEFT');
expect(moveLeftButton, findsOneWidget);
final double movedExitButtonX = tester.getCenter(exitButton).dx;
expect(initialExitButtonX, greaterThan(movedExitButtonX), reason: 'RTL: should move left');
await tester.tap(moveLeftButton);
await tester.pump();
final double finalExitButtonX = tester.getCenter(exitButton).dx;
expect(finalExitButtonX, equals(initialExitButtonX));
},
// [intended] Test requires --track-widget-creation flag.
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(),