forked from flutter/devtools
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Property Editor] Refactor Property Editor for better code sharing (f…
- Loading branch information
Showing
3 changed files
with
424 additions
and
215 deletions.
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 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 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.