Skip to content
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
@@ -0,0 +1,165 @@
// Copyright 2025 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';

import '../../../shared/ui/colors.dart';

class HowToUseMessage extends StatelessWidget {
const HowToUseMessage({super.key});

static const _lightHighlighterColor = Colors.yellow;
static const _darkHighlighterColor = Color.fromARGB(168, 191, 17, 196);

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final fixedFontStyle = theme.fixedFontStyle;
TextSpan colorA(String text) => _coloredSpan(
text,
style: fixedFontStyle,
color: colorScheme.declarationsSyntaxColor,
);
TextSpan colorB(String text) => _coloredSpan(
text,
style: fixedFontStyle,
color: colorScheme.modifierSyntaxColor,
);
TextSpan colorC(String text) => _coloredSpan(
text,
style: fixedFontStyle,
color: colorScheme.variableSyntaxColor,
);
TextSpan colorD(String text) => _coloredSpan(
text,
style: fixedFontStyle,
color: colorScheme.controlFlowSyntaxColor,
);
TextSpan colorE(String text) => _coloredSpan(
text,
style: fixedFontStyle,
color: colorScheme.stringSyntaxColor,
);
TextSpan colorF(String text) => _coloredSpan(
text,
style: fixedFontStyle,
color: colorScheme.functionSyntaxColor,
);
TextSpan colorG(String text) => _coloredSpan(
text,
style: fixedFontStyle,
color: colorScheme.numericConstantSyntaxColor,
);
TextSpan highlight(TextSpan original) => _highlight(
original,
highlighterColor:
theme.isDarkTheme ? _darkHighlighterColor : _lightHighlighterColor,
);

return Text.rich(
TextSpan(
children: [
const TextSpan(text: '\nPlease move your cursor anywhere inside a '),
TextSpan(
text: 'Flutter widget constructor invocation',
style: theme.boldTextStyle,
),
const TextSpan(text: ' to view and edit its properties.\n\n'),
const TextSpan(
text:
'For example, the highlighted code below is a constructor invocation of a ',
),
TextSpan(
text: 'Text',
style: Theme.of(
context,
).fixedFontStyle.copyWith(color: colorScheme.primary),
),
const TextSpan(text: ' widget:\n\n'),
colorA('@override\n'),
colorB('Widget '),
colorG('build'),
colorC('('),
colorB('BuildContext '),
colorA('context'),
colorC(') '),
colorC('{\n'),
colorD(' return '),
highlight(colorB('Text')),
highlight(colorC('(\n')),
highlight(colorE(' "Hello World!"')),
highlight(colorF(',\n')),
highlight(colorA(' overflow')),
highlight(colorF(': ')),
highlight(colorB('TextOveflow')),
highlight(colorF('.')),
highlight(colorG('clip')),
highlight(colorF(',\n')),
highlight(colorC(' )')),
highlight(colorF(';\n')),
colorC('}'),
],
),
);
}

TextSpan _coloredSpan(
String text, {
required TextStyle style,
required Color color,
}) => TextSpan(text: text, style: style.copyWith(color: color));

TextSpan _highlight(TextSpan original, {required Color highlighterColor}) =>
TextSpan(
text: original.text,
style: original.style!.copyWith(backgroundColor: highlighterColor),
);
}

class NoDartCodeMessage extends StatelessWidget {
const NoDartCodeMessage({super.key});

@override
Widget build(BuildContext context) {
return Text(
'No Dart code found at the current cursor location.',
style: Theme.of(context).textTheme.bodyLarge,
);
}
}

class NoMatchingPropertiesMessage extends StatelessWidget {
const NoMatchingPropertiesMessage({super.key});

@override
Widget build(BuildContext context) {
return const Text('No properties matching the current filter.');
}
}

class NoWidgetAtLocationMessage extends StatelessWidget {
const NoWidgetAtLocationMessage({super.key});

@override
Widget build(BuildContext context) {
return Text(
'No Flutter widget found at the current cursor location.',
style: Theme.of(context).textTheme.bodyLarge,
);
}
}

class WelcomeMessage extends StatelessWidget {
const WelcomeMessage({super.key});

@override
Widget build(BuildContext context) {
return Text(
'👋 Welcome to the Flutter Property Editor!',
style: Theme.of(context).textTheme.bodyLarge,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../../../shared/ui/common_widgets.dart';
import '../../../shared/ui/filter.dart';
import 'property_editor_controller.dart';
import 'property_editor_inputs.dart';
import 'property_editor_messages.dart';
import 'property_editor_types.dart';
import 'utils/utils.dart';

Expand All @@ -26,7 +27,6 @@ class PropertyEditorView extends StatelessWidget {
controller.editorClient.editArgumentMethodName,
controller.editorClient.editableArgumentsMethodName,
controller.editableWidgetData,
controller.filteredData,
],
builder: (_, values, _) {
final editArgumentMethodName = values.first as String?;
Expand All @@ -38,56 +38,56 @@ class PropertyEditorView extends StatelessWidget {
}

final editableWidgetData = values.third as EditableWidgetData?;
if (editableWidgetData == null) {
final introSentence =
controller.waitingForFirstEvent
? '👋 Welcome to the Flutter Property Editor!'
: 'No Flutter widget found at the current cursor location.';
const howToUseSentence =
'Please move your cursor to a Flutter widget constructor invocation to view its properties.';
return CenteredMessage(
message: '$introSentence\n\n$howToUseSentence',
);
}

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 (widgetName != null)
_WidgetNameAndDocumentation(
name: widgetName,
documentation: controller.widgetDocumentation,
),
controller.allProperties.isEmpty
? _NoEditablePropertiesMessage(name: controller.widgetName)
: _PropertiesList(
controller: controller,
editableProperties: filteredProperties,
),
],
return SelectionArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _propertyEditorContents(editableWidgetData),
),
);
},
);
}

List<Widget> _propertyEditorContents(EditableWidgetData? editableWidgetData) {
if (editableWidgetData == null) {
final introSentence =
controller.waitingForFirstEvent
? const WelcomeMessage()
: const NoWidgetAtLocationMessage();
return [introSentence, const HowToUseMessage()];
}

final (:properties, :name, :documentation, :fileUri) = editableWidgetData;
if (fileUri != null && !fileUri.endsWith('.dart')) {
return [const NoDartCodeMessage(), const HowToUseMessage()];
}

final contents = <Widget>[];
if (name != null) {
contents.add(
_WidgetNameAndDocumentation(name: name, documentation: documentation),
);
}
if (properties.isEmpty) {
if (name != null) {
contents.add(_NoEditablePropertiesMessage(name: name));
} else {
contents.addAll([
const NoWidgetAtLocationMessage(),
const HowToUseMessage(),
]);
}
} else {
contents.add(_PropertiesList(controller: controller));
}
return contents;
}
}

class _PropertiesList extends StatefulWidget {
const _PropertiesList({
required this.controller,
required this.editableProperties,
});
const _PropertiesList({required this.controller});

final PropertyEditorController controller;
final List<EditableProperty> editableProperties;

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

@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_FilterControls(controller: widget.controller),
if (widget.editableProperties.isEmpty)
const _NoMatchingPropertiesMessage(),
for (final property in widget.editableProperties)
_EditablePropertyItem(
property: property,
editProperty: widget.controller.editArgument,
widgetDocumentation: widget.controller.widgetDocumentation,
),
].joinWith(const PaddedDivider.noPadding()),
return ValueListenableBuilder(
valueListenable: widget.controller.filteredData,
builder: (context, properties, _) {
return Column(
children: <Widget>[
_FilterControls(controller: widget.controller),
if (properties.isEmpty) const NoMatchingPropertiesMessage(),
for (final property in properties)
_EditablePropertyItem(
property: property,
editProperty: widget.controller.editArgument,
widgetDocumentation: widget.controller.widgetDocumentation,
),
].joinWith(const PaddedDivider.noPadding()),
);
},
);
}
}
Expand Down Expand Up @@ -436,15 +440,6 @@ class _NoEditablePropertiesMessage extends StatelessWidget {
}
}

class _NoMatchingPropertiesMessage extends StatelessWidget {
const _NoMatchingPropertiesMessage();

@override
Widget build(BuildContext context) {
return const Text('No properties matching the current filter.');
}
}

class _WidgetNameAndDocumentation extends StatelessWidget {
const _WidgetNameAndDocumentation({required this.name, this.documentation});

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

@override
Widget build(BuildContext context) {
return SelectionArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(bottom: denseSpacing),
child: Text(
name,
style: Theme.of(context).fixedFontStyle.copyWith(
fontWeight: FontWeight.bold,
fontSize: defaultFontSize + 1,
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(bottom: denseSpacing),
child: Text(
name,
style: Theme.of(context).fixedFontStyle.copyWith(
fontWeight: FontWeight.bold,
fontSize: defaultFontSize + 1,
),
),
Row(
children: [
Expanded(
child: _ExpandableWidgetDocumentation(
documentation:
documentation ?? 'Creates ${addIndefiniteArticle(name)}.',
),
),
Row(
children: [
Expanded(
child: _ExpandableWidgetDocumentation(
documentation:
documentation ?? 'Creates ${addIndefiniteArticle(name)}.',
),
],
),
const PaddedDivider.noPadding(),
],
),
),
],
),
const PaddedDivider.noPadding(),
],
);
}
}
Expand Down
Loading