Skip to content

Commit 8f62e34

Browse files
authored
[Focus] Add run key command to dump the focus tree (flutter#123473)
[Focus] Add run key command to dump the focus tree
1 parent 89da046 commit 8f62e34

File tree

13 files changed

+160
-2
lines changed

13 files changed

+160
-2
lines changed

dev/integration_tests/flutter_gallery/test/smoke_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
8181
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
8282
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep());
8383
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? '');
84+
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
8485

8586
// Scroll the demo around a bit more.
8687
await tester.flingFrom(const Offset(400.0, 300.0), const Offset(0.0, 400.0), 1000.0);

packages/flutter/lib/src/rendering/service_extensions.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ enum RenderingServiceExtensions {
6868
/// registered.
6969
debugDumpLayerTree,
7070

71-
7271
/// Name of service extension that, when called, will toggle whether all
7372
/// clipping effects from the layer tree will be ignored.
7473
///

packages/flutter/lib/src/widgets/binding.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
386386
},
387387
);
388388

389+
registerServiceExtension(
390+
name: WidgetsServiceExtensions.debugDumpFocusTree.name,
391+
callback: (Map<String, String> parameters) async {
392+
final String data = focusManager.toStringDeep();
393+
return <String, Object>{
394+
'data': data,
395+
};
396+
},
397+
);
398+
389399
if (!kIsWeb) {
390400
registerBoolServiceExtension(
391401
name: WidgetsServiceExtensions.showPerformanceOverlay.name,

packages/flutter/lib/src/widgets/service_extensions.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ enum WidgetsServiceExtensions {
2020
/// registered.
2121
debugDumpApp,
2222

23+
/// Name of service extension that, when called, will output a string
24+
/// representation of the focus tree to the console.
25+
///
26+
/// See also:
27+
///
28+
/// * [WidgetsBinding.initServiceExtensions], where the service extension is
29+
/// registered.
30+
debugDumpFocusTree,
31+
2332
/// Name of service extension that, when called, will overlay a performance
2433
/// graph on top of this app.
2534
///

packages/flutter/test/foundation/service_extensions_test.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ void main() {
177177
// framework, excluding any that are for the widget inspector
178178
// (see widget_inspector_test.dart for tests of the ext.flutter.inspector
179179
// service extensions).
180-
const int serviceExtensionCount = 37;
180+
const int serviceExtensionCount = 38;
181181

182182
expect(binding.extensions.length, serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions);
183183

@@ -218,6 +218,19 @@ void main() {
218218
});
219219
});
220220

221+
test('Service extensions - debugDumpFocusTree', () async {
222+
final Map<String, dynamic> result = await binding.testExtension(WidgetsServiceExtensions.debugDumpFocusTree.name, <String, String>{});
223+
224+
expect(result, <String, dynamic>{
225+
'data': matches(
226+
r'^'
227+
r'FocusManager#[0-9a-f]{5}\n'
228+
r' └─rootScope: FocusScopeNode#[0-9a-f]{5}\(Root Focus Scope\)\n'
229+
r'$',
230+
),
231+
});
232+
});
233+
221234
test('Service extensions - debugDumpRenderTree', () async {
222235
await binding.doFrame();
223236
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpRenderTree.name, <String, String>{});

packages/flutter_tools/lib/src/base/command_help.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ class CommandHelp {
9797
'Detach (terminate "flutter run" but leave application running).',
9898
);
9999

100+
late final CommandHelpOption f = _makeOption(
101+
'f',
102+
'Dump focus tree to the console.',
103+
'debugDumpFocusTree',
104+
);
105+
100106
late final CommandHelpOption g = _makeOption(
101107
'g',
102108
'Run source code generators.'

packages/flutter_tools/lib/src/resident_runner.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,22 @@ abstract class ResidentHandlers {
752752
return true;
753753
}
754754

755+
Future<bool> debugDumpFocusTree() async {
756+
if (!supportsServiceProtocol || !isRunningDebug) {
757+
return false;
758+
}
759+
for (final FlutterDevice? device in flutterDevices) {
760+
final List<FlutterView> views = await device!.vmService!.getFlutterViews();
761+
for (final FlutterView view in views) {
762+
final String data = await device.vmService!.flutterDebugDumpFocusTree(
763+
isolateId: view.uiIsolate!.id!,
764+
);
765+
logger.printStatus(data);
766+
}
767+
}
768+
return true;
769+
}
770+
755771
/// Dump the application's current semantics tree to the terminal.
756772
///
757773
/// If semantics are not enabled, nothing is returned.
@@ -1521,6 +1537,7 @@ abstract class ResidentRunner extends ResidentHandlers {
15211537
commandHelp.t.print();
15221538
if (isRunningDebug) {
15231539
commandHelp.L.print();
1540+
commandHelp.f.print();
15241541
commandHelp.S.print();
15251542
commandHelp.U.print();
15261543
commandHelp.i.print();
@@ -1706,6 +1723,8 @@ class TerminalHandler {
17061723
case 'D':
17071724
await residentRunner.detach();
17081725
return true;
1726+
case 'f':
1727+
return residentRunner.debugDumpFocusTree();
17091728
case 'g':
17101729
await residentRunner.runSourceGenerators();
17111730
return true;

packages/flutter_tools/lib/src/vmservice.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,16 @@ class FlutterVmService {
643643
return response?['data']?.toString() ?? '';
644644
}
645645

646+
Future<String> flutterDebugDumpFocusTree({
647+
required String isolateId,
648+
}) async {
649+
final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
650+
'ext.flutter.debugDumpFocusTree',
651+
isolateId: isolateId,
652+
);
653+
return response?['data']?.toString() ?? '';
654+
}
655+
646656
Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({
647657
required String isolateId,
648658
}) async {

packages/flutter_tools/test/general.shard/base/command_help_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ void _testMessageLength({
6060
expect(commandHelp.b.toString().length, lessThanOrEqualTo(expectedWidth));
6161
expect(commandHelp.c.toString().length, lessThanOrEqualTo(expectedWidth));
6262
expect(commandHelp.d.toString().length, lessThanOrEqualTo(expectedWidth));
63+
expect(commandHelp.f.toString().length, lessThanOrEqualTo(expectedWidth));
6364
expect(commandHelp.g.toString().length, lessThanOrEqualTo(expectedWidth));
6465
expect(commandHelp.hWithDetails.toString().length, lessThanOrEqualTo(expectedWidth));
6566
expect(commandHelp.hWithoutDetails.toString().length, lessThanOrEqualTo(expectedWidth));
@@ -137,6 +138,7 @@ void main() {
137138
expect(commandHelp.U.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m'));
138139
expect(commandHelp.a.toString(), endsWith('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m\x1B[22m'));
139140
expect(commandHelp.b.toString(), endsWith('\x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m'));
141+
expect(commandHelp.f.toString(), endsWith('\x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m'));
140142
expect(commandHelp.i.toString(), endsWith('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m\x1B[22m'));
141143
expect(commandHelp.o.toString(), endsWith('\x1B[90m(defaultTargetPlatform)\x1B[39m\x1B[22m'));
142144
expect(commandHelp.p.toString(), endsWith('\x1B[90m(debugPaintSizeEnabled)\x1B[39m\x1B[22m'));
@@ -193,6 +195,7 @@ void main() {
193195
expect(commandHelp.b.toString(), equals('\x1B[1mb\x1B[22m Toggle platform brightness (dark and light mode). \x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m'));
194196
expect(commandHelp.c.toString(), equals('\x1B[1mc\x1B[22m Clear the screen'));
195197
expect(commandHelp.d.toString(), equals('\x1B[1md\x1B[22m Detach (terminate "flutter run" but leave application running).'));
198+
expect(commandHelp.f.toString(), equals('\x1B[1mf\x1B[22m Dump focus tree to the console. \x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m'));
196199
expect(commandHelp.g.toString(), equals('\x1B[1mg\x1B[22m Run source code generators.'));
197200
expect(commandHelp.hWithDetails.toString(), equals('\x1B[1mh\x1B[22m Repeat this help message.'));
198201
expect(commandHelp.hWithoutDetails.toString(), equals('\x1B[1mh\x1B[22m List all available interactive commands.'));

packages/flutter_tools/test/general.shard/resident_runner_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,7 @@ flutter:
14551455
commandHelp.w,
14561456
commandHelp.t,
14571457
commandHelp.L,
1458+
commandHelp.f,
14581459
commandHelp.S,
14591460
commandHelp.U,
14601461
commandHelp.i,

0 commit comments

Comments
 (0)