Skip to content

Commit f4fbff8

Browse files
authored
[Property Editor] Better messages when panel is empty (#9076)
1 parent 367b770 commit f4fbff8

3 files changed

Lines changed: 288 additions & 94 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'package:devtools_app_shared/ui.dart';
6+
import 'package:flutter/material.dart';
7+
8+
import '../../../shared/ui/colors.dart';
9+
10+
class HowToUseMessage extends StatelessWidget {
11+
const HowToUseMessage({super.key});
12+
13+
static const _lightHighlighterColor = Colors.yellow;
14+
static const _darkHighlighterColor = Color.fromARGB(168, 191, 17, 196);
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
final theme = Theme.of(context);
19+
final colorScheme = theme.colorScheme;
20+
final fixedFontStyle = theme.fixedFontStyle;
21+
TextSpan colorA(String text) => _coloredSpan(
22+
text,
23+
style: fixedFontStyle,
24+
color: colorScheme.declarationsSyntaxColor,
25+
);
26+
TextSpan colorB(String text) => _coloredSpan(
27+
text,
28+
style: fixedFontStyle,
29+
color: colorScheme.modifierSyntaxColor,
30+
);
31+
TextSpan colorC(String text) => _coloredSpan(
32+
text,
33+
style: fixedFontStyle,
34+
color: colorScheme.variableSyntaxColor,
35+
);
36+
TextSpan colorD(String text) => _coloredSpan(
37+
text,
38+
style: fixedFontStyle,
39+
color: colorScheme.controlFlowSyntaxColor,
40+
);
41+
TextSpan colorE(String text) => _coloredSpan(
42+
text,
43+
style: fixedFontStyle,
44+
color: colorScheme.stringSyntaxColor,
45+
);
46+
TextSpan colorF(String text) => _coloredSpan(
47+
text,
48+
style: fixedFontStyle,
49+
color: colorScheme.functionSyntaxColor,
50+
);
51+
TextSpan colorG(String text) => _coloredSpan(
52+
text,
53+
style: fixedFontStyle,
54+
color: colorScheme.numericConstantSyntaxColor,
55+
);
56+
TextSpan highlight(TextSpan original) => _highlight(
57+
original,
58+
highlighterColor:
59+
theme.isDarkTheme ? _darkHighlighterColor : _lightHighlighterColor,
60+
);
61+
62+
return Text.rich(
63+
TextSpan(
64+
children: [
65+
const TextSpan(text: '\nPlease move your cursor anywhere inside a '),
66+
TextSpan(
67+
text: 'Flutter widget constructor invocation',
68+
style: theme.boldTextStyle,
69+
),
70+
const TextSpan(text: ' to view and edit its properties.\n\n'),
71+
const TextSpan(
72+
text:
73+
'For example, the highlighted code below is a constructor invocation of a ',
74+
),
75+
TextSpan(
76+
text: 'Text',
77+
style: Theme.of(
78+
context,
79+
).fixedFontStyle.copyWith(color: colorScheme.primary),
80+
),
81+
const TextSpan(text: ' widget:\n\n'),
82+
colorA('@override\n'),
83+
colorB('Widget '),
84+
colorG('build'),
85+
colorC('('),
86+
colorB('BuildContext '),
87+
colorA('context'),
88+
colorC(') '),
89+
colorC('{\n'),
90+
colorD(' return '),
91+
highlight(colorB('Text')),
92+
highlight(colorC('(\n')),
93+
highlight(colorE(' "Hello World!"')),
94+
highlight(colorF(',\n')),
95+
highlight(colorA(' overflow')),
96+
highlight(colorF(': ')),
97+
highlight(colorB('TextOveflow')),
98+
highlight(colorF('.')),
99+
highlight(colorG('clip')),
100+
highlight(colorF(',\n')),
101+
highlight(colorC(' )')),
102+
highlight(colorF(';\n')),
103+
colorC('}'),
104+
],
105+
),
106+
);
107+
}
108+
109+
TextSpan _coloredSpan(
110+
String text, {
111+
required TextStyle style,
112+
required Color color,
113+
}) => TextSpan(text: text, style: style.copyWith(color: color));
114+
115+
TextSpan _highlight(TextSpan original, {required Color highlighterColor}) =>
116+
TextSpan(
117+
text: original.text,
118+
style: original.style!.copyWith(backgroundColor: highlighterColor),
119+
);
120+
}
121+
122+
class NoDartCodeMessage extends StatelessWidget {
123+
const NoDartCodeMessage({super.key});
124+
125+
@override
126+
Widget build(BuildContext context) {
127+
return Text(
128+
'No Dart code found at the current cursor location.',
129+
style: Theme.of(context).textTheme.bodyLarge,
130+
);
131+
}
132+
}
133+
134+
class NoMatchingPropertiesMessage extends StatelessWidget {
135+
const NoMatchingPropertiesMessage({super.key});
136+
137+
@override
138+
Widget build(BuildContext context) {
139+
return const Text('No properties matching the current filter.');
140+
}
141+
}
142+
143+
class NoWidgetAtLocationMessage extends StatelessWidget {
144+
const NoWidgetAtLocationMessage({super.key});
145+
146+
@override
147+
Widget build(BuildContext context) {
148+
return Text(
149+
'No Flutter widget found at the current cursor location.',
150+
style: Theme.of(context).textTheme.bodyLarge,
151+
);
152+
}
153+
}
154+
155+
class WelcomeMessage extends StatelessWidget {
156+
const WelcomeMessage({super.key});
157+
158+
@override
159+
Widget build(BuildContext context) {
160+
return Text(
161+
'👋 Welcome to the Flutter Property Editor!',
162+
style: Theme.of(context).textTheme.bodyLarge,
163+
);
164+
}
165+
}

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart

Lines changed: 81 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '../../../shared/ui/common_widgets.dart';
1111
import '../../../shared/ui/filter.dart';
1212
import 'property_editor_controller.dart';
1313
import 'property_editor_inputs.dart';
14+
import 'property_editor_messages.dart';
1415
import 'property_editor_types.dart';
1516
import 'utils/utils.dart';
1617

@@ -26,7 +27,6 @@ class PropertyEditorView extends StatelessWidget {
2627
controller.editorClient.editArgumentMethodName,
2728
controller.editorClient.editableArgumentsMethodName,
2829
controller.editableWidgetData,
29-
controller.filteredData,
3030
],
3131
builder: (_, values, _) {
3232
final editArgumentMethodName = values.first as String?;
@@ -38,56 +38,56 @@ class PropertyEditorView extends StatelessWidget {
3838
}
3939

4040
final editableWidgetData = values.third as EditableWidgetData?;
41-
if (editableWidgetData == null) {
42-
final introSentence =
43-
controller.waitingForFirstEvent
44-
? '👋 Welcome to the Flutter Property Editor!'
45-
: 'No Flutter widget found at the current cursor location.';
46-
const howToUseSentence =
47-
'Please move your cursor to a Flutter widget constructor invocation to view its properties.';
48-
return CenteredMessage(
49-
message: '$introSentence\n\n$howToUseSentence',
50-
);
51-
}
52-
53-
final fileUri = controller.fileUri;
54-
if (fileUri != null && !fileUri.endsWith('.dart')) {
55-
return const CenteredMessage(
56-
message: 'No Dart code found at the current cursor location.',
57-
);
58-
}
59-
60-
final filteredProperties = values.fourth as List<EditableProperty>;
61-
final widgetName = controller.widgetName;
62-
return Column(
63-
crossAxisAlignment: CrossAxisAlignment.start,
64-
children: [
65-
if (widgetName != null)
66-
_WidgetNameAndDocumentation(
67-
name: widgetName,
68-
documentation: controller.widgetDocumentation,
69-
),
70-
controller.allProperties.isEmpty
71-
? _NoEditablePropertiesMessage(name: controller.widgetName)
72-
: _PropertiesList(
73-
controller: controller,
74-
editableProperties: filteredProperties,
75-
),
76-
],
41+
return SelectionArea(
42+
child: Column(
43+
crossAxisAlignment: CrossAxisAlignment.start,
44+
children: _propertyEditorContents(editableWidgetData),
45+
),
7746
);
7847
},
7948
);
8049
}
50+
51+
List<Widget> _propertyEditorContents(EditableWidgetData? editableWidgetData) {
52+
if (editableWidgetData == null) {
53+
final introSentence =
54+
controller.waitingForFirstEvent
55+
? const WelcomeMessage()
56+
: const NoWidgetAtLocationMessage();
57+
return [introSentence, const HowToUseMessage()];
58+
}
59+
60+
final (:properties, :name, :documentation, :fileUri) = editableWidgetData;
61+
if (fileUri != null && !fileUri.endsWith('.dart')) {
62+
return [const NoDartCodeMessage(), const HowToUseMessage()];
63+
}
64+
65+
final contents = <Widget>[];
66+
if (name != null) {
67+
contents.add(
68+
_WidgetNameAndDocumentation(name: name, documentation: documentation),
69+
);
70+
}
71+
if (properties.isEmpty) {
72+
if (name != null) {
73+
contents.add(_NoEditablePropertiesMessage(name: name));
74+
} else {
75+
contents.addAll([
76+
const NoWidgetAtLocationMessage(),
77+
const HowToUseMessage(),
78+
]);
79+
}
80+
} else {
81+
contents.add(_PropertiesList(controller: controller));
82+
}
83+
return contents;
84+
}
8185
}
8286

8387
class _PropertiesList extends StatefulWidget {
84-
const _PropertiesList({
85-
required this.controller,
86-
required this.editableProperties,
87-
});
88+
const _PropertiesList({required this.controller});
8889

8990
final PropertyEditorController controller;
90-
final List<EditableProperty> editableProperties;
9191

9292
static const defaultItemPadding = borderPadding;
9393
static const denseItemPadding = defaultItemPadding / 2;
@@ -113,18 +113,22 @@ class _PropertiesListState extends State<_PropertiesList> {
113113

114114
@override
115115
Widget build(BuildContext context) {
116-
return Column(
117-
children: <Widget>[
118-
_FilterControls(controller: widget.controller),
119-
if (widget.editableProperties.isEmpty)
120-
const _NoMatchingPropertiesMessage(),
121-
for (final property in widget.editableProperties)
122-
_EditablePropertyItem(
123-
property: property,
124-
editProperty: widget.controller.editArgument,
125-
widgetDocumentation: widget.controller.widgetDocumentation,
126-
),
127-
].joinWith(const PaddedDivider.noPadding()),
116+
return ValueListenableBuilder(
117+
valueListenable: widget.controller.filteredData,
118+
builder: (context, properties, _) {
119+
return Column(
120+
children: <Widget>[
121+
_FilterControls(controller: widget.controller),
122+
if (properties.isEmpty) const NoMatchingPropertiesMessage(),
123+
for (final property in properties)
124+
_EditablePropertyItem(
125+
property: property,
126+
editProperty: widget.controller.editArgument,
127+
widgetDocumentation: widget.controller.widgetDocumentation,
128+
),
129+
].joinWith(const PaddedDivider.noPadding()),
130+
);
131+
},
128132
);
129133
}
130134
}
@@ -436,15 +440,6 @@ class _NoEditablePropertiesMessage extends StatelessWidget {
436440
}
437441
}
438442

439-
class _NoMatchingPropertiesMessage extends StatelessWidget {
440-
const _NoMatchingPropertiesMessage();
441-
442-
@override
443-
Widget build(BuildContext context) {
444-
return const Text('No properties matching the current filter.');
445-
}
446-
}
447-
448443
class _WidgetNameAndDocumentation extends StatelessWidget {
449444
const _WidgetNameAndDocumentation({required this.name, this.documentation});
450445

@@ -453,34 +448,32 @@ class _WidgetNameAndDocumentation extends StatelessWidget {
453448

454449
@override
455450
Widget build(BuildContext context) {
456-
return SelectionArea(
457-
child: Column(
458-
mainAxisSize: MainAxisSize.min,
459-
children: [
460-
Container(
461-
alignment: Alignment.centerLeft,
462-
padding: const EdgeInsets.only(bottom: denseSpacing),
463-
child: Text(
464-
name,
465-
style: Theme.of(context).fixedFontStyle.copyWith(
466-
fontWeight: FontWeight.bold,
467-
fontSize: defaultFontSize + 1,
468-
),
451+
return Column(
452+
mainAxisSize: MainAxisSize.min,
453+
children: [
454+
Container(
455+
alignment: Alignment.centerLeft,
456+
padding: const EdgeInsets.only(bottom: denseSpacing),
457+
child: Text(
458+
name,
459+
style: Theme.of(context).fixedFontStyle.copyWith(
460+
fontWeight: FontWeight.bold,
461+
fontSize: defaultFontSize + 1,
469462
),
470463
),
471-
Row(
472-
children: [
473-
Expanded(
474-
child: _ExpandableWidgetDocumentation(
475-
documentation:
476-
documentation ?? 'Creates ${addIndefiniteArticle(name)}.',
477-
),
464+
),
465+
Row(
466+
children: [
467+
Expanded(
468+
child: _ExpandableWidgetDocumentation(
469+
documentation:
470+
documentation ?? 'Creates ${addIndefiniteArticle(name)}.',
478471
),
479-
],
480-
),
481-
const PaddedDivider.noPadding(),
482-
],
483-
),
472+
),
473+
],
474+
),
475+
const PaddedDivider.noPadding(),
476+
],
484477
);
485478
}
486479
}

0 commit comments

Comments
 (0)