Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Property Editor] Add a tooltip for the property's default value and documentation #9058

Merged
merged 8 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ class EditableArgument with Serializable {
this.value,
this.defaultValue,
this.displayValue,
this.documentation,
this.errorText,
});

Expand All @@ -545,6 +546,7 @@ class EditableArgument with Serializable {
value: map[Field.value],
defaultValue: map[Field.defaultValue],
displayValue: map[Field.displayValue] as String?,
documentation: map[Field.documentation] as String?,
errorText: map[Field.errorText] as String?,
);

Expand Down Expand Up @@ -595,6 +597,9 @@ class EditableArgument with Serializable {
/// as the value field, for example an expression or named constant.
final String? displayValue;

/// Documentation about the widget argument.
final String? documentation;

final String? errorText;

String get valueDisplay => displayValue ?? currentValue.toString();
Expand All @@ -615,6 +620,7 @@ class EditableArgument with Serializable {
Field.isEditable: isEditable,
Field.options: options,
Field.displayValue: displayValue,
Field.documentation: documentation,
Field.errorText: errorText,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ class PropertyEditorController extends DisposableController
_editableWidgetData;
final _editableWidgetData = ValueNotifier<EditableWidgetData?>(null);

List<EditableProperty> get allProperties =>
_editableWidgetData.value?.properties ?? [];
String? get widgetName => _editableWidgetData.value?.name;
String? get widgetDocumentation => _editableWidgetData.value?.documentation;
String? get fileUri => _editableWidgetData.value?.fileUri;

ValueListenable<bool> get shouldReconnect => _shouldReconnect;
final _shouldReconnect = ValueNotifier<bool>(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ mixin _PropertyInputMixin<T extends StatefulWidget, U> on State<T> {

Widget inputLabel(EditableProperty property, {required ThemeData theme}) {
// Flutter scales down the label font size by 75%, therefore we need to
// increase the size to make it glegible.
// increase the size to make it legible.
final fixedFontStyle = theme.fixedFontStyle.copyWith(
fontSize: defaultFontSize + 1,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class EditableProperty extends EditableArgument {
isEditable: argument.isEditable,
options: argument.options,
displayValue: argument.displayValue,
documentation: argument.documentation,
errorText: argument.errorText,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,25 @@ class PropertyEditorView extends StatelessWidget {
);
}

final (:properties, :name, :documentation, :fileUri) =
editableWidgetData;
final fileUri = controller.fileUri;
if (fileUri != null && !fileUri.endsWith('.dart')) {
return const CenteredMessage(
message: 'No Dart code found at the current cursor location.',
);
}

final filteredProperties = values.fourth as List<EditableProperty>;
final widgetName = controller.widgetName;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (name != null)
if (widgetName != null)
_WidgetNameAndDocumentation(
name: name,
documentation: documentation,
name: widgetName,
documentation: controller.widgetDocumentation,
),
properties.isEmpty
? _NoEditablePropertiesMessage(name: name)
controller.allProperties.isEmpty
? _NoEditablePropertiesMessage(name: controller.widgetName)
: _PropertiesList(
controller: controller,
editableProperties: filteredProperties,
Expand Down Expand Up @@ -122,6 +122,7 @@ class _PropertiesListState extends State<_PropertiesList> {
_EditablePropertyItem(
property: property,
editProperty: widget.controller.editArgument,
widgetDocumentation: widget.controller.widgetDocumentation,
),
].joinWith(const PaddedDivider.noPadding()),
);
Expand All @@ -132,10 +133,12 @@ class _EditablePropertyItem extends StatelessWidget {
const _EditablePropertyItem({
required this.property,
required this.editProperty,
required this.widgetDocumentation,
});

final EditableProperty property;
final EditArgumentFunction editProperty;
final String? widgetDocumentation;

@override
Widget build(BuildContext context) {
Expand All @@ -146,9 +149,25 @@ class _EditablePropertyItem extends StatelessWidget {
flex: 3,
child: Padding(
padding: const EdgeInsets.all(_PropertiesList.defaultItemPadding),
child: _PropertyInput(
property: property,
editProperty: editProperty,
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(
bottom: largeSpacing,
right: densePadding,
),
child: _InfoTooltip(
property: property,
widgetDocumentation: widgetDocumentation,
),
),
Expanded(
child: _PropertyInput(
property: property,
editProperty: editProperty,
),
),
],
),
),
),
Expand Down Expand Up @@ -244,6 +263,76 @@ class _PropertyLabels extends StatelessWidget {
width >= _widthForFullLabels ? labelText : labelText[0].toUpperCase();
}

class _InfoTooltip extends StatelessWidget {
const _InfoTooltip({
required this.property,
required this.widgetDocumentation,
});

final EditableProperty property;
final String? widgetDocumentation;

@override
Widget build(BuildContext context) {
return DevToolsTooltip(
richMessage: _infoMessage(context),
child: Icon(size: defaultIconSize, Icons.info_outline),
);
}

TextSpan _infoMessage(BuildContext context) {
final theme = Theme.of(context);
final textColor = theme.colorScheme.tooltipTextColor;
final regularFontStyle = theme.regularTextStyle.copyWith(color: textColor);
final boldFontStyle = theme.boldTextStyle.copyWith(color: textColor);
final fixedFontStyle = theme.fixedFontStyle.copyWith(color: textColor);

final propertyNameSpans = [
TextSpan(
text: '${property.displayType} ',
style: fixedFontStyle.copyWith(fontSize: largeFontSize),
),
TextSpan(
text: property.name,
style: fixedFontStyle.copyWith(
fontSize: largeFontSize,
fontWeight: FontWeight.bold,
),
),
];

final defaultValueSpans =
property.hasDefault
? [
TextSpan(text: '\n\nDefault value: ', style: boldFontStyle),
TextSpan(
text: property.defaultValue.toString(),
style: fixedFontStyle,
),
]
: [
TextSpan(text: '\n\nDefault value:\n', style: boldFontStyle),
TextSpan(text: property.name, style: fixedFontStyle),
TextSpan(text: ' has no default value.', style: regularFontStyle),
];

final spans = [...propertyNameSpans, ...defaultValueSpans];

final documentation = property.documentation;
if (documentation != null && documentation != widgetDocumentation) {
spans.addAll([
TextSpan(text: '\n\nDocumentation:\n', style: boldFontStyle),
...DartDocConverter(documentation).toTextSpans(
regularFontStyle: regularFontStyle,
fixedFontStyle: fixedFontStyle,
),
]);
}

return TextSpan(children: spans);
}
}

class _PropertyInput extends StatelessWidget {
const _PropertyInput({required this.property, required this.editProperty});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,18 @@ class DartDocConverter {
return Text.rich(TextSpan(children: children));
}

@visibleForTesting
List<TextSpan> toTextSpans({
required TextStyle regularFontStyle,
required TextStyle fixedFontStyle,
}) {
final text = _removeTemplateIndicators(dartDocText);

final children = <TextSpan>[];
int currentIndex = 0;

while (currentIndex < dartDocText.length) {
final openBracketIndex = dartDocText.indexOf('[', currentIndex);
final openBacktickIndex = dartDocText.indexOf('`', currentIndex);
while (currentIndex < text.length) {
final openBracketIndex = text.indexOf('[', currentIndex);
final openBacktickIndex = text.indexOf('`', currentIndex);

int nextSpecialCharIndex = -1;
bool isLink = false;
Expand All @@ -53,46 +54,85 @@ class DartDocConverter {
if (nextSpecialCharIndex == -1) {
// No more special characters, add the remaining text.
children.add(
TextSpan(
text: dartDocText.substring(currentIndex),
style: regularFontStyle,
),
TextSpan(text: text.substring(currentIndex), style: regularFontStyle),
);
break;
}

// Add text before the special character.
children.add(
TextSpan(
text: dartDocText.substring(currentIndex, nextSpecialCharIndex),
text: text.substring(currentIndex, nextSpecialCharIndex),
style: regularFontStyle,
),
);

final closeIndex = dartDocText.indexOf(
final closeIndex = text.indexOf(
isLink ? ']' : '`',
isLink ? nextSpecialCharIndex : nextSpecialCharIndex + 1,
);
if (closeIndex == -1) {
// Treat unmatched brackets/backticks as regular text.
children.add(
TextSpan(
text: dartDocText.substring(nextSpecialCharIndex),
text: text.substring(nextSpecialCharIndex),
style: regularFontStyle,
),
);
currentIndex = dartDocText.length; // Effectively break the loop.
currentIndex = text.length; // Effectively break the loop.
} else {
final content = dartDocText.substring(
nextSpecialCharIndex + 1,
closeIndex,
);
final content = text.substring(nextSpecialCharIndex + 1, closeIndex);
children.add(TextSpan(text: content, style: fixedFontStyle));
currentIndex = closeIndex + 1;
}
}
return children;
}

/// Removes @template and @endtemplate indicators from the [input].
String _removeTemplateIndicators(String input) {
const templateStart = '{@template';
const templateEnd = '{@endtemplate';
const closingCurlyBrace = '}';
const newLine = '\n';
String result = '';
int currentIndex = 0;

while (currentIndex < input.length) {
final startTemplateIndex = input.indexOf(templateStart, currentIndex);
final endTemplateIndex = input.indexOf(templateEnd, currentIndex);

int templateIndex;
if (startTemplateIndex != -1 && endTemplateIndex != -1) {
templateIndex =
(startTemplateIndex < endTemplateIndex)
? startTemplateIndex
: endTemplateIndex;
} else if (startTemplateIndex != -1) {
templateIndex = startTemplateIndex;
} else if (endTemplateIndex != -1) {
templateIndex = endTemplateIndex;
} else {
result += input.substring(currentIndex);
break;
}

result += input.substring(currentIndex, templateIndex);

final closingIndex = input.indexOf(closingCurlyBrace, templateIndex);
if (closingIndex == -1) {
result += input.substring(templateIndex);
break;
}
final closingChars =
input.substring(closingIndex).startsWith('$closingCurlyBrace$newLine')
? '$closingCurlyBrace$newLine'
: closingCurlyBrace;
currentIndex = closingIndex + closingChars.length;
}

return result;
}
}

/// Workaround to force reload the Property Editor when it disconnects.
Expand Down
Loading