-
Notifications
You must be signed in to change notification settings - Fork 396
[Property Editor] Refactor Property Editor for better code sharing #8797
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c3a03dc
Add type info and have all inputs used fixed font
elliette 6f52818
Big refactor
elliette 24e2624
Editable property is a class
elliette f84786a
Split into multiple files and clean up
elliette 90a94cc
Merge branch 'master' into refactor-property-editor
elliette 65a41f6
Call Theme.of once
elliette 928a8b9
Use isNully function in validator
elliette 0259435
Respond to PR comments
elliette 340a399
Fix DCM errors
elliette File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
223 changes: 223 additions & 0 deletions
223
...devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| // 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 'package:flutter/services.dart'; | ||
|
|
||
| import 'property_editor_controller.dart'; | ||
| import 'property_editor_types.dart'; | ||
|
|
||
| class BooleanInput extends StatelessWidget { | ||
| const BooleanInput({ | ||
| super.key, | ||
| required this.property, | ||
| required this.controller, | ||
| }); | ||
|
|
||
| final FiniteValuesProperty property; | ||
| final PropertyEditorController controller; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return _DropdownInput<Object>(property: property, controller: controller); | ||
| } | ||
| } | ||
|
|
||
| class DoubleInput extends StatelessWidget { | ||
| const DoubleInput({ | ||
| super.key, | ||
| required this.property, | ||
| required this.controller, | ||
| }); | ||
|
|
||
| final NumericProperty property; | ||
| final PropertyEditorController controller; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return _TextInput<double>(property: property, controller: controller); | ||
| } | ||
| } | ||
|
|
||
| class EnumInput extends StatelessWidget { | ||
| const EnumInput({ | ||
| super.key, | ||
| required this.property, | ||
| required this.controller, | ||
| }); | ||
|
|
||
| final FiniteValuesProperty property; | ||
| final PropertyEditorController controller; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return _DropdownInput<String>(property: property, controller: controller); | ||
| } | ||
| } | ||
|
|
||
| class IntegerInput extends StatelessWidget { | ||
| const IntegerInput({ | ||
| super.key, | ||
| required this.property, | ||
| required this.controller, | ||
| }); | ||
|
|
||
| final NumericProperty property; | ||
| final PropertyEditorController controller; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return _TextInput<int>(property: property, controller: controller); | ||
| } | ||
| } | ||
|
|
||
| class StringInput extends StatelessWidget { | ||
| const StringInput({ | ||
| super.key, | ||
| required this.property, | ||
| required this.controller, | ||
| }); | ||
|
|
||
| final EditableProperty property; | ||
| final PropertyEditorController controller; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return _TextInput<String>(property: property, controller: controller); | ||
| } | ||
| } | ||
|
|
||
| class _DropdownInput<T> extends StatelessWidget with _PropertyInputMixin<T> { | ||
| _DropdownInput({super.key, required this.property, required this.controller}); | ||
|
|
||
| final FiniteValuesProperty property; | ||
| final PropertyEditorController controller; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final theme = Theme.of(context); | ||
| return DropdownButtonFormField( | ||
| value: property.valueDisplay, | ||
| decoration: decoration(property, theme: theme), | ||
| isExpanded: true, | ||
| items: | ||
| property.propertyOptions.map((option) { | ||
| return DropdownMenuItem( | ||
| value: option, | ||
| child: Text(option, style: theme.fixedFontStyle), | ||
| ); | ||
| }).toList(), | ||
| onChanged: (newValue) async { | ||
| await editProperty( | ||
| property, | ||
| valueAsString: newValue, | ||
| controller: controller, | ||
| ); | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class _TextInput<T> extends StatefulWidget with _PropertyInputMixin<T> { | ||
| _TextInput({super.key, required this.property, required this.controller}); | ||
|
|
||
| final EditableProperty property; | ||
| final PropertyEditorController controller; | ||
|
|
||
| @override | ||
| State<_TextInput> createState() => _TextInputState(); | ||
| } | ||
|
|
||
| class _TextInputState extends State<_TextInput> { | ||
| String currentValue = ''; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final theme = Theme.of(context); | ||
| return TextFormField( | ||
| initialValue: widget.property.valueDisplay, | ||
| enabled: widget.property.isEditable, | ||
| autovalidateMode: AutovalidateMode.onUserInteraction, | ||
| validator: widget.property.inputValidator, | ||
| inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], | ||
| decoration: widget.decoration(widget.property, theme: theme), | ||
| style: theme.fixedFontStyle, | ||
| onChanged: (newValue) { | ||
| setState(() { | ||
| currentValue = newValue; | ||
| }); | ||
| }, | ||
| onEditingComplete: _editProperty, | ||
| onTapOutside: (_) async { | ||
| await _editProperty(); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| Future<void> _editProperty() async { | ||
| await widget.editProperty( | ||
| widget.property, | ||
| valueAsString: currentValue, | ||
| controller: widget.controller, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| mixin _PropertyInputMixin<T> { | ||
| Future<void> editProperty( | ||
| EditableProperty property, { | ||
| required PropertyEditorController controller, | ||
| required String? valueAsString, | ||
| }) async { | ||
| final argName = property.name; | ||
|
|
||
| // Can edit values to null. | ||
| if (property.isNullable && property.isNully(valueAsString)) { | ||
| await controller.editArgument(name: argName, value: null); | ||
| return; | ||
| } | ||
|
|
||
| final value = property.convertFromString(valueAsString) as T?; | ||
| await controller.editArgument(name: argName, value: value); | ||
| } | ||
|
|
||
| InputDecoration decoration( | ||
| EditableProperty property, { | ||
| required ThemeData theme, | ||
| }) { | ||
| return InputDecoration( | ||
| helperText: property.isRequired ? '*required' : '', | ||
| errorText: property.errorText, | ||
| isDense: true, | ||
| label: inputLabel(property, theme: theme), | ||
| border: const OutlineInputBorder(), | ||
| ); | ||
| } | ||
|
|
||
| Widget inputLabel(EditableProperty property, {required ThemeData theme}) { | ||
| return RichText( | ||
| overflow: TextOverflow.ellipsis, | ||
| text: TextSpan( | ||
| text: '${property.displayType} ', | ||
| style: theme.fixedFontStyle, | ||
| children: [ | ||
| TextSpan( | ||
| text: property.name, | ||
| style: theme.fixedFontStyle.copyWith( | ||
| fontWeight: FontWeight.bold, | ||
| color: theme.colorScheme.primary, | ||
| ), | ||
| children: [ | ||
| TextSpan( | ||
| text: property.isRequired ? '*' : '', | ||
| style: theme.fixedFontStyle, | ||
| ), | ||
| ], | ||
| ), | ||
| ], | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
157 changes: 157 additions & 0 deletions
157
.../devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // 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/utils.dart'; | ||
| import 'package:meta/meta.dart'; | ||
|
|
||
| import '../../../shared/editor/api_classes.dart'; | ||
|
|
||
| class EditableString extends EditableProperty { | ||
| EditableString(super.argument); | ||
|
|
||
| @override | ||
| String? convertFromString(String? valueAsString) => valueAsString; | ||
|
|
||
| @override | ||
| String get dartType => 'String'; | ||
|
|
||
| @override | ||
| bool isNully(String? inputValue) { | ||
| return inputValue == null || inputValue == 'null'; | ||
| } | ||
| } | ||
|
|
||
| class EditableBool extends EditableProperty with FiniteValuesProperty { | ||
| EditableBool(super.argument); | ||
|
|
||
| @override | ||
| Object? convertFromString(String? valueAsString) => | ||
| valueAsString == 'true' || valueAsString == 'false' | ||
| ? valueAsString == 'true' | ||
| : valueAsString; // The boolean value might be an expression. | ||
|
|
||
| @override | ||
| Set<String> get propertyOptions { | ||
| return {'true', 'false', valueDisplay, if (isNullable) 'null'}; | ||
| } | ||
| } | ||
|
|
||
| class EditableDouble extends EditableProperty with NumericProperty { | ||
| EditableDouble(super.argument); | ||
|
|
||
| @override | ||
| double? convertFromString(String? valueAsString) => | ||
| toNumber(valueAsString) as double?; | ||
| } | ||
|
|
||
| class EditableInt extends EditableProperty with NumericProperty { | ||
| EditableInt(super.argument); | ||
|
|
||
| @override | ||
| int? convertFromString(String? valueAsString) => | ||
| toNumber(valueAsString) as int?; | ||
| } | ||
|
|
||
| class EditableEnum extends EditableProperty with FiniteValuesProperty { | ||
| EditableEnum(super.argument); | ||
|
|
||
| @override | ||
| String? convertFromString(String? valueAsString) => valueAsString; | ||
|
|
||
| @override | ||
| String get dartType => options?.first.split('.').first ?? type; | ||
|
|
||
| @override | ||
| Set<String> get propertyOptions { | ||
| return {...(options ?? []), valueDisplay, if (isNullable) 'null'}; | ||
| } | ||
| } | ||
|
|
||
| class EditableProperty extends EditableArgument { | ||
| EditableProperty(EditableArgument argument) | ||
| : super( | ||
| name: argument.name, | ||
| type: argument.type, | ||
| value: argument.value, | ||
| hasArgument: argument.hasArgument, | ||
| isDefault: argument.isDefault, | ||
| isNullable: argument.isNullable, | ||
| isRequired: argument.isRequired, | ||
| isEditable: argument.isEditable, | ||
| options: argument.options, | ||
| displayValue: argument.displayValue, | ||
| errorText: argument.errorText, | ||
| ); | ||
|
|
||
| String get dartType => type; | ||
|
|
||
| String get displayType => isNullable ? '$dartType?' : dartType; | ||
|
|
||
| String get typeError => 'Please enter ${addIndefiniteArticle(dartType)}.'; | ||
|
|
||
| String? inputValidator(String? _) { | ||
| return null; | ||
| } | ||
|
|
||
| bool isNully(String? inputValue) { | ||
| final isNull = inputValue == null || inputValue == 'null'; | ||
| final isEmpty = inputValue == ''; | ||
| return isNull || isEmpty; | ||
| } | ||
|
|
||
| @mustBeOverridden | ||
| Object? convertFromString(String? _) { | ||
| throw UnimplementedError(); | ||
| } | ||
| } | ||
|
|
||
| mixin NumericProperty on EditableProperty { | ||
| @override | ||
| String? inputValidator(String? inputValue) { | ||
| // Permit sending null values with an empty input or with explicit "null". | ||
| if (isNullable && isNully(inputValue)) { | ||
| return null; | ||
| } | ||
| final numValue = toNumber(inputValue); | ||
| if (numValue == null) { | ||
| return typeError; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| Object? toNumber(String? valueAsString) { | ||
| if (valueAsString == null || valueAsString == '') return null; | ||
| final isInt = type == 'int'; | ||
| return isInt ? int.tryParse(valueAsString) : double.tryParse(valueAsString); | ||
| } | ||
| } | ||
|
|
||
| mixin FiniteValuesProperty on EditableProperty { | ||
| Set<String> get propertyOptions; | ||
| } | ||
|
|
||
| EditableProperty? argToProperty(EditableArgument argument) { | ||
| switch (argument.type) { | ||
| case boolType: | ||
| return EditableBool(argument); | ||
| case doubleType: | ||
| return EditableDouble(argument); | ||
| case enumType: | ||
| return EditableEnum(argument); | ||
| case intType: | ||
| return EditableInt(argument); | ||
| case stringType: | ||
| return EditableString(argument); | ||
| default: | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /// The following types should match those returned by the Analysis Server. See: | ||
| /// https://github.com/dart-lang/sdk/blob/154b473cdb65c2686bb44fedec03ba2deddb80fd/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/handler_editable_arguments.dart#L182 | ||
| const stringType = 'string'; | ||
| const doubleType = 'double'; | ||
| const intType = 'int'; | ||
| const boolType = 'bool'; | ||
| const enumType = 'enum'; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.