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,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);
Comment thread
elliette marked this conversation as resolved.
}
}

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,
),
],
),
],
),
);
}
}
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';
Loading