Skip to content

Commit

Permalink
[Property Editor] Refactor Property Editor for better code sharing (f…
Browse files Browse the repository at this point in the history
  • Loading branch information
elliette authored Jan 23, 2025
1 parent 69c37ba commit 37960ee
Show file tree
Hide file tree
Showing 3 changed files with 424 additions and 215 deletions.
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,
),
],
),
],
),
);
}
}
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

0 comments on commit 37960ee

Please sign in to comment.