Skip to content

Commit e4435ba

Browse files
bkonyimboetger
authored andcommitted
[ Widget Preview ] Respond to IDE navigation events and show previews from the currently focused script (flutter#174466)
This change allows for the widget previewer to react to `activeLocationChanged` events sent over the `Editor` DTD service to automatically filter the set of visible previews based on the currently selected file in the IDE. This functionality can be turned on or off depending on whether or not the developer wants to see all previews in the project at once. This change also includes some minor refactoring and UI changes to move the widget preview environment controls to a reserved area at the bottom of the preview window. **Demo:** https://github.com/user-attachments/assets/c3b93826-8437-4655-8264-6beed6651fe7
1 parent b68d190 commit e4435ba

28 files changed

+878
-329
lines changed

packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ typedef PreviewDependencyGraph = Map<PreviewPath, LibraryPreviewNode>;
3535
class _PreviewVisitor extends RecursiveAstVisitor<void> {
3636
_PreviewVisitor({required LibraryElement2 lib})
3737
: packageName = lib.uri.scheme == 'package' ? lib.uri.pathSegments.first : null,
38-
_context = lib.session.analysisContext;
38+
_context = lib.session.analysisContext,
39+
_currentScriptUri = null;
3940

4041
late final String? packageName;
4142

@@ -46,6 +47,12 @@ class _PreviewVisitor extends RecursiveAstVisitor<void> {
4647
ConstructorDeclaration? _currentConstructor;
4748
MethodDeclaration? _currentMethod;
4849

50+
Uri? _currentScriptUri;
51+
52+
void findPreviewsInResolvedUnitResult(ResolvedUnitResult unit) {
53+
_scopedVisitChildren(unit.unit, (_) => _currentScriptUri = unit.file.toUri());
54+
}
55+
4956
/// Handles previews defined on top-level functions.
5057
@override
5158
void visitFunctionDeclaration(FunctionDeclaration node) {
@@ -104,6 +111,7 @@ class _PreviewVisitor extends RecursiveAstVisitor<void> {
104111
if (returnType.isWidget || returnType.isWidgetBuilder) {
105112
previewEntries.add(
106113
PreviewDetails(
114+
scriptUri: _currentScriptUri!,
107115
packageName: packageName,
108116
functionName: _currentFunction!.name.toString(),
109117
isBuilder: returnType.isWidgetBuilder,
@@ -118,6 +126,7 @@ class _PreviewVisitor extends RecursiveAstVisitor<void> {
118126
final Token? name = _currentConstructor!.name;
119127
previewEntries.add(
120128
PreviewDetails(
129+
scriptUri: _currentScriptUri!,
121130
packageName: packageName,
122131
functionName: '$returnType${name == null ? '' : '.$name'}',
123132
isBuilder: false,
@@ -132,6 +141,7 @@ class _PreviewVisitor extends RecursiveAstVisitor<void> {
132141
final parentClass = _currentMethod!.parent! as ClassDeclaration;
133142
previewEntries.add(
134143
PreviewDetails(
144+
scriptUri: _currentScriptUri!,
135145
packageName: packageName,
136146
functionName: '${parentClass.name}.${_currentMethod!.name}',
137147
isBuilder: returnType.isWidgetBuilder,
@@ -210,9 +220,7 @@ final class LibraryPreviewNode {
210220
void findPreviews({required ResolvedLibraryResult lib}) {
211221
// Iterate over the compilation unit's AST to find previews.
212222
final visitor = _PreviewVisitor(lib: lib.element);
213-
for (final ResolvedUnitResult libUnit in lib.units) {
214-
libUnit.unit.visitChildren(visitor);
215-
}
223+
lib.units.forEach(visitor.findPreviewsInResolvedUnitResult);
216224
previews
217225
..clear()
218226
..addAll(visitor.previewEntries);

packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ class PreviewCodeGenerator {
190190
return cb.refer(_kWidgetPreviewClass, _kWidgetPreviewLibraryUri).newInstance(
191191
<cb.Expression>[],
192192
<String, cb.Expression>{
193+
PreviewDetails.kScriptUri: cb.literalString(preview.scriptUri.toString()),
193194
// TODO(bkonyi): try to display the preview name, even if the preview can't be displayed.
194195
if (!libraryDetails.dependencyHasErrors &&
195196
!libraryDetails.hasErrors) ...<String, cb.Expression>{

packages/flutter_tools/lib/src/widget_preview/preview_details.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:analyzer/dart/constant/value.dart';
77
/// Contains details related to a single preview instance.
88
final class PreviewDetails {
99
PreviewDetails({
10+
required this.scriptUri,
1011
required this.packageName,
1112
required this.functionName,
1213
required this.isBuilder,
@@ -19,6 +20,7 @@ final class PreviewDetails {
1920
brightness = previewAnnotation.getField(kBrightness)!,
2021
localizations = previewAnnotation.getField(kLocalizations)!;
2122

23+
static const kScriptUri = 'scriptUri';
2224
static const kPackageName = 'packageName';
2325
static const kName = 'name';
2426
static const kSize = 'size';
@@ -28,6 +30,9 @@ final class PreviewDetails {
2830
static const kBrightness = 'brightness';
2931
static const kLocalizations = 'localizations';
3032

33+
/// The file:// URI pointing to the script in which the preview is defined.
34+
final Uri scriptUri;
35+
3136
/// The name of the package in which the preview was defined.
3237
///
3338
/// For example, if this preview is defined in 'package:foo/src/bar.dart', this

packages/flutter_tools/templates/template_manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@
348348
"templates/widget_preview_scaffold/lib/main.dart.tmpl",
349349
"templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl",
350350
"templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl",
351+
"templates/widget_preview_scaffold/lib/src/widget_preview_scaffold_controller.dart.tmpl",
351352
"templates/widget_preview_scaffold/lib/src/controls.dart.tmpl",
352353
"templates/widget_preview_scaffold/lib/src/dtd/dtd_services.dart.tmpl",
353354
"templates/widget_preview_scaffold/lib/src/dtd/editor_service.dart.tmpl",

packages/flutter_tools/templates/widget_preview_scaffold/lib/src/controls.dart.tmpl

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/material.dart';
6-
import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart';
6+
import 'widget_preview_scaffold_controller.dart';
77

88
class _WidgetPreviewIconButton extends StatelessWidget {
99
const _WidgetPreviewIconButton({
@@ -97,6 +97,85 @@ class ZoomControls extends StatelessWidget {
9797
}
9898
}
9999

100+
class _ControlDecorator extends StatelessWidget {
101+
const _ControlDecorator({required this.child});
102+
103+
final Widget child;
104+
105+
@override
106+
Widget build(BuildContext context) {
107+
return Container(
108+
padding: EdgeInsets.all(8.0),
109+
decoration: BoxDecoration(
110+
color: Colors.grey[300],
111+
borderRadius: BorderRadius.circular(8.0),
112+
),
113+
child: child,
114+
);
115+
}
116+
}
117+
118+
/// Allows for controlling the grid vs layout view in the preview environment.
119+
class LayoutTypeSelector extends StatelessWidget {
120+
const LayoutTypeSelector({super.key, required this.controller});
121+
122+
final WidgetPreviewScaffoldController controller;
123+
124+
@override
125+
Widget build(BuildContext context) {
126+
return _ControlDecorator(
127+
child: ValueListenableBuilder<LayoutType>(
128+
valueListenable: controller.layoutTypeListenable,
129+
builder: (context, selectedLayout, _) {
130+
return Row(
131+
children: [
132+
IconButton(
133+
onPressed: () => controller.layoutType = LayoutType.gridView,
134+
icon: Icon(Icons.grid_on),
135+
color: selectedLayout == LayoutType.gridView
136+
? Colors.blue
137+
: Colors.black,
138+
),
139+
IconButton(
140+
onPressed: () => controller.layoutType = LayoutType.listView,
141+
icon: Icon(Icons.view_list),
142+
color: selectedLayout == LayoutType.listView
143+
? Colors.blue
144+
: Colors.black,
145+
),
146+
],
147+
);
148+
},
149+
),
150+
);
151+
}
152+
}
153+
154+
/// A toggle button that enables / disables filtering previews by the currently
155+
/// selected source file.
156+
class FilterBySelectedFileToggle extends StatelessWidget {
157+
const FilterBySelectedFileToggle({super.key, required this.controller});
158+
159+
final WidgetPreviewScaffoldController controller;
160+
161+
@override
162+
Widget build(BuildContext context) {
163+
return _ControlDecorator(
164+
child: ValueListenableBuilder(
165+
valueListenable: controller.filterBySelectedFileListenable,
166+
builder: (context, value, child) {
167+
return IconButton(
168+
onPressed: controller.toggleFilterBySelectedFile,
169+
icon: Icon(Icons.file_open),
170+
color: value ? Colors.blue : Colors.black,
171+
tooltip: 'Filter previews by selected file',
172+
);
173+
},
174+
),
175+
);
176+
}
177+
}
178+
100179
/// A button that triggers a "soft" restart of a previewed widget.
101180
///
102181
/// A soft restart removes the previewed widget from the widget tree for a frame before
@@ -129,16 +208,18 @@ class SoftRestartButton extends StatelessWidget {
129208
/// A button that triggers a restart of the widget previewer through a hot restart request made
130209
/// through DTD.
131210
class WidgetPreviewerRestartButton extends StatelessWidget {
132-
const WidgetPreviewerRestartButton({super.key, required this.dtdServices});
211+
const WidgetPreviewerRestartButton({super.key, required this.controller});
133212

134-
final WidgetPreviewScaffoldDtdServices dtdServices;
213+
final WidgetPreviewScaffoldController controller;
135214

136215
@override
137216
Widget build(BuildContext context) {
138-
return IconButton.outlined(
139-
tooltip: 'Restart the Widget Previewer',
140-
onPressed: () => dtdServices.hotRestartPreviewer(),
141-
icon: Icon(Icons.restart_alt),
217+
return _ControlDecorator(
218+
child: IconButton(
219+
tooltip: 'Restart the Widget Previewer',
220+
onPressed: controller.dtdServices.hotRestartPreviewer,
221+
icon: Icon(Icons.restart_alt),
222+
),
142223
);
143224
}
144225
}

packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_services.dart.tmpl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import 'dart:async';
66

77
import 'package:dtd/dtd.dart';
8-
import 'package:widget_preview_scaffold/src/dtd/editor_service.dart';
8+
import 'editor_service.dart';
99

1010
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
1111
class WidgetPreviewScaffoldDtdServices with DtdEditorService {
@@ -42,6 +42,13 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
4242
await initializeEditorService();
4343
}
4444

45+
/// Disposes the DTD connection.
46+
@override
47+
Future<void> dispose() async {
48+
super.dispose();
49+
await dtd.close();
50+
}
51+
4552
Future<DTDResponse> _call(
4653
String methodName, {
4754
Map<String, Object?>? params,

packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/editor_service.dart.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ mixin DtdEditorService {
4040
});
4141
await dtd.streamListen(kEditorService);
4242
}
43+
44+
@mustCallSuper
45+
void dispose() {
46+
_selectedSourceFile.dispose();
47+
_editorTheme.dispose();
48+
}
4349
}
4450

4551
// TODO(bkonyi): much of the following code is copied from the DevTools codebase. We should publish

packages/flutter_tools/templates/widget_preview_scaffold/lib/src/utils.dart.tmpl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,44 @@ class VerticalSpacer extends StatelessWidget {
3131
return const SizedBox(height: 10);
3232
}
3333
}
34+
35+
/// A basic horizontal spacer.
36+
class HorizontalSpacer extends StatelessWidget {
37+
/// Creates a basic vertical spacer.
38+
const HorizontalSpacer({super.key});
39+
40+
@override
41+
Widget build(BuildContext context) {
42+
return const SizedBox(width: 10);
43+
}
44+
}
45+
46+
/// A widget that explicitly responds to hot reload events.
47+
///
48+
/// Hot reload will always result in [reassemble] being called.
49+
class HotReloadListener extends StatefulWidget {
50+
const HotReloadListener({
51+
super.key,
52+
required this.onHotReload,
53+
required this.child,
54+
});
55+
56+
final VoidCallback onHotReload;
57+
final Widget child;
58+
59+
@override
60+
HotReloadListenerState createState() => HotReloadListenerState();
61+
}
62+
63+
class HotReloadListenerState extends State<HotReloadListener> {
64+
@override
65+
void reassemble() {
66+
super.reassemble();
67+
widget.onHotReload();
68+
}
69+
70+
@override
71+
Widget build(BuildContext context) {
72+
return widget.child;
73+
}
74+
}

packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,12 @@ import 'package:flutter/widgets.dart';
88

99
/// Wraps a [Widget], initializing various state and properties to allow for
1010
/// previewing of the [Widget] in the widget previewer.
11-
///
12-
/// WARNING: This interface is not stable and **will change**.
13-
///
14-
/// See also:
15-
///
16-
/// * [Preview], an annotation class used to mark functions returning widget
17-
/// previews.
18-
// TODO(bkonyi): link to actual documentation when available.
1911
class WidgetPreview {
2012
/// Wraps [builder] in a [WidgetPreview] instance that applies some set of
2113
/// properties.
2214
const WidgetPreview({
2315
required this.builder,
16+
required this.scriptUri,
2417
this.packageName,
2518
this.name,
2619
this.size,
@@ -30,6 +23,11 @@ class WidgetPreview {
3023
this.localizations,
3124
});
3225

26+
/// The absolute file:// URI pointing to the script containing this preview.
27+
///
28+
/// This matches the URI format sent by IDEs for active location change events.
29+
final String scriptUri;
30+
3331
/// The name of the package in which a preview was defined.
3432
///
3533
/// For example, if a preview is defined in 'package:foo/src/bar.dart', this

0 commit comments

Comments
 (0)