Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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';

/// A text button that displays "Show more" or "Show less" depending on whether
/// it [isExpanded].
class ExpandableTextButton extends StatelessWidget {
const ExpandableTextButton({
super.key,
required this.isExpanded,
required this.onTap,
});

final bool isExpanded;
final VoidCallback onTap;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: onTap,
child: Text(
isExpanded ? 'Show less' : 'Show more',
style: theme.boldTextStyle.copyWith(color: theme.colorScheme.primary),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ class PropertyEditorController extends DisposableController
);
}

Future<GenericApiResponse?> executeCommand(CodeActionCommand refactor) {
return editorClient.executeCommand(
commandName: refactor.command,
commandArgs: refactor.args,
screenId: gac.PropertyEditorSidebar.id,
);
}

int hashProperty(EditableProperty property) {
final widgetData = editableWidgetData.value;
if (widgetData == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// 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 '../../../shared/editor/api_classes.dart';
import 'property_editor_common.dart';
import 'property_editor_controller.dart';

typedef ExecuteCommandFunction =
Future<GenericApiResponse?> Function(CodeActionCommand refactor);

/// Widget for displaying the available "Wrap with" refactors.
///
/// Each refactor is in a [_WrapWithButton].
class WrapWithRefactors extends StatefulWidget {
const WrapWithRefactors({
required this.refactors,
required this.controller,
super.key,
});

final List<CodeActionCommand> refactors;
final PropertyEditorController controller;

@override
State<WrapWithRefactors> createState() => _RefactorsState();
}

class _RefactorsState extends State<WrapWithRefactors> {
final _mainRefactorsGroup = <CodeActionCommand>[];
final _otherRefactorsGroup = <CodeActionCommand>[];

bool _showAllRefactors = false;

@override
void initState() {
super.initState();
_categorizeRefactors();
}

@override
void didUpdateWidget(covariant WrapWithRefactors oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.refactors != oldWidget.refactors) {
_categorizeRefactors();
}
}

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(densePadding),
child: Text('Wrap with:'),
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final refactor in _mainRefactorsGroup)
_WrapWithButton(
refactor,
executeCommand: widget.controller.executeCommand,
iconOnly: true,
),
],
),
if (_showAllRefactors)
Padding(
padding: const EdgeInsets.only(top: densePadding / 2),
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final refactor in _otherRefactorsGroup)
_WrapWithButton(
refactor,
executeCommand: widget.controller.executeCommand,
iconOnly: false,
),
],
),
),
if (_otherRefactorsGroup.isNotEmpty)
Row(
children: [
Padding(
padding: const EdgeInsets.all(densePadding),
child: ExpandableTextButton(
isExpanded: _showAllRefactors,
onTap: () {
setState(() {
_showAllRefactors = !_showAllRefactors;
});
},
),
),
],
),
const PaddedDivider.noPadding(),
],
);
}

void _categorizeRefactors() {
_mainRefactorsGroup.clear();
_otherRefactorsGroup.clear();
for (final refactor in widget.refactors) {
// Ignore any unexpected refactors that don't begin with "Wrap with".
if (!refactor.title.startsWith('Wrap with')) continue;

final category = _mainRefactors.contains(refactor.title)
? _mainRefactorsGroup
: _otherRefactorsGroup;
category.add(refactor);
}
}
}

/// A button which triggers a single "Wrap with" refactor.
///
/// Buttons for refactors in [_refactorsWithIconAsset] have an icon.
class _WrapWithButton extends StatelessWidget {
const _WrapWithButton(
this.action, {
required this.executeCommand,
required this.iconOnly,
});

final CodeActionCommand action;
final ExecuteCommandFunction executeCommand;
final bool iconOnly;

static const _iconAssetPath = 'icons/property_editor/';

String get label {
final wrapperName = action.title.split('Wrap with ').last;
return wrapperName == 'widget...' ? 'Widget' : wrapperName;
}

String get command => action.command;

String? _iconAsset({bool darkMode = false}) {
if (!_refactorsWithIconAsset.contains(action.title)) {
return null;
}

final path = '$_iconAssetPath${label.toLowerCase()}';
return '${path}_${darkMode ? 'dark' : 'light'}_theme.png';
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconAsset = _iconAsset(darkMode: theme.isDarkTheme);

final button = OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)),
side: BorderSide(
color: iconOnly ? Colors.transparent : theme.colorScheme.onSurface,
),
padding: const EdgeInsets.all(densePadding),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: iconAsset != null
? Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset(iconAsset, height: actionsIconSize),
],
)
: _buttonText(label, theme: theme),
onPressed: () async {
await executeCommand(action);
},
);

return Padding(
padding: EdgeInsets.all(iconOnly ? densePadding / 2 : densePadding),
child: iconOnly ? DevToolsTooltip(message: label, child: button) : button,
);
}

Text _buttonText(String label, {required ThemeData theme}) {
// Show the first letter of the label as the icon.
Comment thread
elliette marked this conversation as resolved.
if (_refactorsWithLetterIcon.contains(action.title)) {
return Text(
label[0],
style: theme.regularTextStyle.copyWith(
fontWeight: FontWeight.bold,
fontSize: unscaledExtraLargeFontSize,
color: theme.isDarkTheme ? Colors.white : Colors.black,
),
);
}

return Text(label, style: theme.regularTextStyle);
}
}

const _wrapWithPadding = 'Wrap with Padding';
const _wrapWithContainer = 'Wrap with Container';
const _wrapWithColumn = 'Wrap with Column';
const _wrapWithRow = 'Wrap with Row';
const _wrapWithCenter = 'Wrap with Center';
const _wrapWithSizedBox = 'Wrap with SizedBox';
const _wrapWithWidget = 'Wrap with widget...';

const _refactorsWithIconAsset = {
_wrapWithPadding,
_wrapWithContainer,
_wrapWithColumn,
_wrapWithRow,
_wrapWithCenter,
_wrapWithSizedBox,
};

const _refactorsWithLetterIcon = {_wrapWithWidget};

const _mainRefactors = {
..._refactorsWithIconAsset,
..._refactorsWithLetterIcon,
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import 'package:flutter/material.dart';
import '../../../shared/primitives/utils.dart';
import '../../../shared/ui/common_widgets.dart';
import '../../../shared/ui/filter.dart';
import 'property_editor_common.dart';
import 'property_editor_controller.dart';
import 'property_editor_inputs.dart';
import 'property_editor_messages.dart';
import 'property_editor_refactors.dart';
import 'property_editor_types.dart';
import 'utils/utils.dart';

Expand Down Expand Up @@ -64,6 +66,13 @@ class PropertyEditorView extends StatelessWidget {
_WidgetNameAndDocumentation(name: name, documentation: documentation),
);
}

if (refactors.isNotEmpty) {
contents.add(
WrapWithRefactors(refactors: refactors, controller: controller),
);
}

if (properties.isEmpty) {
if (name != null) {
contents.add(_NoEditablePropertiesMessage(name: name));
Expand Down Expand Up @@ -516,15 +525,7 @@ class _ExpandableWidgetDocumentationState
),
),
const SizedBox(height: denseSpacing),
InkWell(
onTap: _toggleExpansion,
child: Text(
_isExpanded ? 'Show less' : 'Show more',
style: theme.boldTextStyle.copyWith(
color: theme.colorScheme.primary,
),
),
),
ExpandableTextButton(isExpanded: _isExpanded, onTap: _toggleExpansion),
],
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/devtools_app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ flutter:
- icons/inspector/widget_icons/
- icons/memory/
- icons/perf/
- icons/property_editor/
# We have to explicitly list every asset under `packages/perfetto_ui_compiled/`
# since directory support is not available for assets under `packages/`.
# See (https://github.com/flutter/flutter/issues/112019).
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools_app_shared/lib/src/ui/theme/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ double get mediumProgressSize => scaleByFontFactor(24.0);
const defaultTabBarViewPhysics = NeverScrollableScrollPhysics();

// Font size constants:
const unscaledExtraLargeFontSize = 16.0;
Comment thread
elliette marked this conversation as resolved.
Outdated

double get largeFontSize => scaleByFontFactor(unscaledLargeFontSize);
const unscaledLargeFontSize = 14.0;

Expand Down
Loading