Skip to content

Commit

Permalink
[Property Editor] Initial UI scaffold (flutter#8538)
Browse files Browse the repository at this point in the history
  • Loading branch information
elliette authored Nov 26, 2024
1 parent d58891c commit dedca64
Show file tree
Hide file tree
Showing 8 changed files with 452 additions and 16 deletions.
10 changes: 10 additions & 0 deletions packages/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@
"program": "devtools_app/test/test_infra/scenes/standalone_ui/editor_sidebar.stager_app.g.dart",
"preLaunchTask": "Start DTD on Port 8500",
},
{
"name": "standalone_ui/editor_sidebar + experiments",
"request": "launch",
"type": "dart",
"program": "devtools_app/test/test_infra/scenes/standalone_ui/editor_sidebar.stager_app.g.dart",
"preLaunchTask": "Start DTD on Port 8500",
"args": [
"--dart-define=enable_experiments=true"
],
},
{
"name": "devtools_extensions: foo + sim",
"request": "launch",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,22 +544,7 @@ class DefaultValueLabel extends StatelessWidget {

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
decoration: BoxDecoration(
borderRadius: defaultBorderRadius,
color: colorScheme.secondary,
),
child: Text(
'default',
style: theme.regularTextStyleWithColor(
colorScheme.onSecondary,
backgroundColor: colorScheme.secondary,
),
),
);
return const RoundedLabel(labelText: 'default');
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/devtools_app/lib/src/shared/feature_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ abstract class FeatureFlags {
/// https://github.com/flutter/devtools/issues/7856
static bool wasmOptInSetting = true;

/// Flag to enable the Flutter Property Editor sidebar.
///
/// https://github.com/flutter/devtools/issues/7854
static bool propertyEditor = enableExperiments;

/// Stores a map of all the feature flags for debugging purposes.
///
/// When adding a new flag, you are responsible for adding it to this map as
Expand All @@ -113,6 +118,7 @@ abstract class FeatureFlags {
'dapDebugging': dapDebugging,
'inspectorV2': inspectorV2,
'wasmOptInSetting': wasmOptInSetting,
'propertyEditor': propertyEditor,
};

/// A helper to print the status of all the feature flags.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

import '../../../shared/primitives/utils.dart';

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

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Property Editor', style: Theme.of(context).textTheme.titleMedium),
const PaddedDivider.noPadding(),
const _PropertiesList(),
],
);
}
}

class _PropertiesList extends StatelessWidget {
const _PropertiesList();

static const itemPadding = densePadding;

@override
Widget build(BuildContext context) {
// TODO(https://github.com/flutter/devtools/issues/8546) Switch to scrollable
// ListView when this has been moved into its own panel.
return Column(
children: <Widget>[
..._properties.map(
(property) => _EditablePropertyItem(property: property),
),
].joinWith(const PaddedDivider.noPadding()),
);
}
}

class _EditablePropertyItem extends StatelessWidget {
const _EditablePropertyItem({required this.property});

final _WidgetProperty property;

@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(_PropertiesList.itemPadding),
child: _PropertyInput(property: property),
),
),
if (property.isRequired || property.isDefault) ...[
Flexible(child: _PropertyLabels(property: property)),
] else
const Spacer(),
],
);
}
}

class _PropertyLabels extends StatelessWidget {
const _PropertyLabels({required this.property});

final _WidgetProperty property;

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isRequired = property.isRequired;
final isDefault = property.isDefault;

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isRequired)
Padding(
padding: const EdgeInsets.all(_PropertiesList.itemPadding),
child: RoundedLabel(
labelText: 'required',
backgroundColor: colorScheme.primary,
textColor: colorScheme.onPrimary,
),
),
if (isDefault)
const Padding(
padding: EdgeInsets.all(_PropertiesList.itemPadding),
child: RoundedLabel(labelText: 'default'),
),
],
);
}
}

class _PropertyInput extends StatelessWidget {
const _PropertyInput({required this.property});

final _WidgetProperty property;

@override
Widget build(BuildContext context) {
final decoration = InputDecoration(
helperText: '',
errorText: property.errorText,
isDense: true,
label: Text(property.name),
border: const OutlineInputBorder(),
);

switch (property.type) {
case 'enum':
case 'bool':
final options =
property.type == 'bool' ? ['true', 'false'] : property.options;
return DropdownButtonFormField(
value: property.valueDisplay,
decoration: decoration,
items:
(options ?? []).map((option) {
return DropdownMenuItem(
value: option,
// TODO(https://github.com/flutter/devtools/issues/8531) Handle onTap.
onTap: () {},
child: Text(option),
);
}).toList(),
onChanged: (_) {},
);
case 'double':
case 'int':
case 'string':
return TextFormField(
initialValue: property.valueDisplay,
enabled: property.isEditable,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: _inputValidator,
inputFormatters: [FilteringTextInputFormatter.singleLineFormatter],
decoration: decoration,
style: Theme.of(context).fixedFontStyle,
// TODO(https://github.com/flutter/devtools/issues/8531) Handle onChanged.
onChanged: (_) {},
);
default:
return Text(property.valueDisplay);
}
}

String? _inputValidator(String? inputValue) {
final isDouble = property.type == 'double';
final isInt = property.type == 'int';

// Only validate numeric types.
if (!isDouble && !isInt) {
return null;
}

final validationMessage =
'Please enter ${isInt ? 'an integer' : 'a double'}.';
if (inputValue == null || inputValue == '') {
return validationMessage;
}
final numValue =
isInt ? int.tryParse(inputValue) : double.tryParse(inputValue);
if (numValue == null) {
return validationMessage;
}
return null;
}
}

class _WidgetProperty {
const _WidgetProperty({
required this.name,
required this.type,
required this.isNullable,
this.value,
this.displayValue,
this.isEditable = true,
this.isRequired = false,
this.hasArgument = true,
this.isDefault = false,
this.errorText,
this.options,
// ignore: unused_element_parameter, TODO(https://github.com/flutter/devtools/issues/8532): Support colors.
this.swatches,
// ignore: unused_element_parameter, TODO(https://github.com/flutter/devtools/issues/8532): Support objects.
this.properties,
});

final String name;
final String type;
final bool isNullable;
final Object? value;
final String? displayValue;
final bool isEditable;
final bool isRequired;
final bool hasArgument;
final bool isDefault;
final String? errorText;
final List<String>? options;
final List<String>? swatches;
final List<_WidgetProperty>? properties;

String get valueDisplay => displayValue ?? value.toString();
}

// TODO(https://github.com/flutter/devtools/issues/8531): Connect to DTD and delete hard-coded properties.
const _titleProperty = _WidgetProperty(
name: 'title',
value: 'Hello world!',
type: 'string',
isNullable: false,
isRequired: true,
hasArgument: false,
);

const _widthProperty = _WidgetProperty(
name: 'width',
displayValue: '100.0',
type: 'double',
isEditable: false,
errorText: 'Some reason for why this can\'t be edited.',
isNullable: false,
value: 20.0,
isRequired: true,
hasArgument: false,
);

const _heightProperty = _WidgetProperty(
name: 'height',
type: 'double',
isNullable: false,
value: 20.0,
isDefault: true,
isRequired: true,
);

const _softWrapProperty = _WidgetProperty(
name: 'softWrap',
type: 'bool',
isNullable: false,
value: true,
isDefault: true,
);

const _alignProperty = _WidgetProperty(
name: 'align',
type: 'enum',
isNullable: false,
value: 'Alignment.center',
options: [
'Alignment.bottomCenter',
'Alignment.bottomLeft',
'Alignment.bottomRight',
'Alignment.center',
'Alignment.centerLeft',
'Alignment.centerRight',
'Alignment.topCenter',
'Alignment.topLeft',
'Alignment.topRight',
],
);

const _properties = [
_titleProperty,
_widthProperty,
_heightProperty,
_alignProperty,
_softWrapProperty,
];
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import '../../service/editor/api_classes.dart';
import '../../service/editor/editor_client.dart';
import '../../shared/analytics/analytics.dart' as ga;
import '../../shared/common_widgets.dart';
import '../../shared/feature_flags.dart';
import '../ide_shared/property_editor/property_editor_sidebar.dart';
import 'debug_sessions.dart';
import 'devices.dart';
import 'devtools/devtools_view.dart';
Expand Down Expand Up @@ -193,6 +195,9 @@ class _EditorConnectedPanelState extends State<_EditorConnectedPanel>
editor: widget.editor,
debugSessions: debugSessions,
),
// TODO(https://github.com/flutter/devtools/issues/8546) Move
// Property Editor to its own sidepanel.
if (FeatureFlags.propertyEditor) const PropertyEditorSidebar(),
],
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ void main() {
expect(FeatureFlags.deepLinkIosCheck, true);
expect(FeatureFlags.dapDebugging, false);
expect(FeatureFlags.wasmOptInSetting, true);
expect(FeatureFlags.inspectorV2, true);
expect(FeatureFlags.propertyEditor, false);
});
}
Loading

0 comments on commit dedca64

Please sign in to comment.