Skip to content

Commit

Permalink
UI polish for narrow extension screens embedded in IDE (#8127)
Browse files Browse the repository at this point in the history
  • Loading branch information
kenzieschmoll authored Jul 31, 2024
1 parent 8179fbf commit a750a19
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 114 deletions.
13 changes: 7 additions & 6 deletions packages/devtools_app/lib/src/extensions/extension_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class _ExtensionScreenBodyState extends State<_ExtensionScreenBody> {
Widget build(BuildContext context) {
return ExtensionView(
controller: extensionController!,
extension: widget.extensionConfig,
ext: widget.extensionConfig,
);
}
}
Expand All @@ -95,27 +95,28 @@ class ExtensionView extends StatelessWidget {
const ExtensionView({
super.key,
required this.controller,
required this.extension,
required this.ext,
});

final EmbeddedExtensionController controller;

final DevToolsExtensionConfig extension;
final DevToolsExtensionConfig ext;

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
EmbeddedExtensionHeader(
extension: extension,
ext: ext,
onForceReload: () =>
controller.postMessage(DevToolsExtensionEventType.forceReload),
),
const SizedBox(height: intermediateSpacing),
Expanded(
child: ValueListenableBuilder<ExtensionEnabledState>(
valueListenable: extensionService.enabledStateListenable(
extension.name,
ext.name,
),
builder: (context, activationState, _) {
if (activationState == ExtensionEnabledState.enabled) {
Expand All @@ -126,7 +127,7 @@ class ExtensionView extends StatelessWidget {
);
}
return EnableExtensionPrompt(
extension: controller.extensionConfig,
ext: controller.extensionConfig,
);
},
),
Expand Down
228 changes: 124 additions & 104 deletions packages/devtools_app/lib/src/extensions/extension_screen_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,114 +18,140 @@ import '../shared/routing.dart';
class EmbeddedExtensionHeader extends StatelessWidget {
const EmbeddedExtensionHeader({
super.key,
required this.extension,
required this.ext,
required this.onForceReload,
});

final DevToolsExtensionConfig extension;
final DevToolsExtensionConfig ext;

final VoidCallback onForceReload;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final extensionName = extension.displayName;
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: borderPadding),
child: RichText(
text: TextSpan(
text: 'package:$extensionName extension',
style:
theme.regularTextStyle.copyWith(fontWeight: FontWeight.bold),
children: [
TextSpan(
text: ' (v${extension.version})',
style: theme.subtleTextStyle,
),
],
),
),
),
const Spacer(),
RichText(
text: GaLinkTextSpan(
link: GaLink(
display: 'Report an issue',
url: extension.issueTrackerLink,
gaScreenName: gac.DevToolsExtensionEvents.extensionScreenId.name,
gaSelectedItemDescription:
gac.DevToolsExtensionEvents.extensionFeedback(extension),
final extensionName = ext.displayName;
return SizedBox(
width: double.infinity,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: borderPadding),
child: RichText(
text: TextSpan(
text: 'package:$extensionName extension',
style: theme.regularTextStyle
.copyWith(fontWeight: FontWeight.bold),
children: [
TextSpan(
text: ' (v${ext.version})',
style: theme.subtleTextStyle,
),
],
),
),
context: context,
),
),
const SizedBox(width: denseSpacing),
ValueListenableBuilder<ExtensionEnabledState>(
valueListenable:
extensionService.enabledStateListenable(extension.displayName),
builder: (context, activationState, _) {
if (activationState == ExtensionEnabledState.enabled) {
return ContextMenuButton(
iconSize: defaultIconSize,
buttonWidth: buttonMinWidth,
menuChildren: <Widget>[
PointerInterceptor(
child: MenuItemButton(
onPressed: () {
// Do not send analytics here because the user must
// confirm that they want to disable the extension from
// the [DisableExtensionDialog]. Analytics will be sent
// there if they confirm that they'd like to disable the
// extension.
unawaited(
showDialog(
context: context,
builder: (_) =>
DisableExtensionDialog(extension: extension),
),
);
},
child: const MaterialIconLabel(
label: 'Disable extension',
iconData: Icons.extension_off_outlined,
),
),
const SizedBox(width: denseSpacing),
Row(
mainAxisSize: MainAxisSize.min,
children: [
RichText(
text: GaLinkTextSpan(
link: GaLink(
display: 'Report an issue',
url: ext.issueTrackerLink,
gaScreenName:
gac.DevToolsExtensionEvents.extensionScreenId.name,
gaSelectedItemDescription:
gac.DevToolsExtensionEvents.extensionFeedback(ext),
),
PointerInterceptor(
child: MenuItemButton(
onPressed: () {
ga.select(
gac.DevToolsExtensionEvents.extensionScreenId.name,
gac.DevToolsExtensionEvents.extensionForceReload(
extension,
),
);
onForceReload();
},
child: const MaterialIconLabel(
label: 'Force reload extension',
iconData: Icons.refresh,
),
context: context,
),
),
const SizedBox(width: denseSpacing),
_ExtensionContextMenuButton(
ext: ext,
onForceReload: onForceReload,
),
],
),
],
),
);
}
}

class _ExtensionContextMenuButton extends StatelessWidget {
const _ExtensionContextMenuButton({
required this.ext,
required this.onForceReload,
});

final DevToolsExtensionConfig ext;

final VoidCallback onForceReload;

@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ExtensionEnabledState>(
valueListenable: extensionService.enabledStateListenable(ext.displayName),
builder: (context, activationState, _) {
if (activationState != ExtensionEnabledState.enabled) {
return const SizedBox.shrink();
}
return ContextMenuButton(
iconSize: defaultIconSize,
buttonWidth: buttonMinWidth,
menuChildren: <Widget>[
PointerInterceptor(
child: MenuItemButton(
onPressed: () {
// Do not send analytics here because the user must
// confirm that they want to disable the extension from
// the [DisableExtensionDialog]. Analytics will be sent
// there if they confirm that they'd like to disable the
// extension.
unawaited(
showDialog(
context: context,
builder: (_) => DisableExtensionDialog(ext: ext),
),
),
],
);
}
return const SizedBox.shrink();
},
),
],
);
},
child: const MaterialIconLabel(
label: 'Disable extension',
iconData: Icons.extension_off_outlined,
),
),
),
PointerInterceptor(
child: MenuItemButton(
onPressed: () {
ga.select(
gac.DevToolsExtensionEvents.extensionScreenId.name,
gac.DevToolsExtensionEvents.extensionForceReload(ext),
);
onForceReload();
},
child: const MaterialIconLabel(
label: 'Force reload extension',
iconData: Icons.refresh,
),
),
),
],
);
},
);
}
}

@visibleForTesting
class DisableExtensionDialog extends StatelessWidget {
const DisableExtensionDialog({super.key, required this.extension});
const DisableExtensionDialog({super.key, required this.ext});

final DevToolsExtensionConfig extension;
final DevToolsExtensionConfig ext;

@override
Widget build(BuildContext context) {
Expand All @@ -142,7 +168,7 @@ class DisableExtensionDialog extends StatelessWidget {
style: theme.regularTextStyle,
children: [
TextSpan(
text: extension.displayName,
text: ext.displayName,
style: theme.fixedFontStyle,
),
const TextSpan(text: ' extension?'),
Expand Down Expand Up @@ -176,13 +202,10 @@ class DisableExtensionDialog extends StatelessWidget {
onPressed: () {
ga.select(
gac.DevToolsExtensionEvents.extensionScreenId.name,
gac.DevToolsExtensionEvents.extensionDisableManual(extension),
gac.DevToolsExtensionEvents.extensionDisableManual(ext),
);
unawaited(
extensionService.setExtensionEnabledState(
extension,
enable: false,
),
extensionService.setExtensionEnabledState(ext, enable: false),
);
Navigator.of(context).pop(dialogDefaultContext);
DevToolsRouterDelegate.of(context)
Expand All @@ -197,12 +220,9 @@ class DisableExtensionDialog extends StatelessWidget {
}

class EnableExtensionPrompt extends StatelessWidget {
const EnableExtensionPrompt({
super.key,
required this.extension,
});
const EnableExtensionPrompt({super.key, required this.ext});

final DevToolsExtensionConfig extension;
final DevToolsExtensionConfig ext;

@override
Widget build(BuildContext context) {
Expand All @@ -218,7 +238,7 @@ class EnableExtensionPrompt extends StatelessWidget {
style: theme.regularTextStyle,
children: [
TextSpan(
text: extension.name,
text: ext.name,
style: theme.fixedFontStyle,
),
const TextSpan(
Expand Down Expand Up @@ -246,13 +266,13 @@ class EnableExtensionPrompt extends StatelessWidget {
label: 'Enable',
gaScreen: gac.DevToolsExtensionEvents.extensionScreenId.name,
gaSelection: gac.DevToolsExtensionEvents.extensionEnablePrompt(
extension,
ext,
),
elevated: true,
onPressed: () {
unawaited(
extensionService.setExtensionEnabledState(
extension,
ext,
enable: true,
),
);
Expand All @@ -263,12 +283,12 @@ class EnableExtensionPrompt extends StatelessWidget {
label: 'No, hide this screen',
gaScreen: gac.DevToolsExtensionEvents.extensionScreenId.name,
gaSelection: gac.DevToolsExtensionEvents.extensionDisablePrompt(
extension,
ext,
),
onPressed: () {
unawaited(
extensionService.setExtensionEnabledState(
extension,
ext,
enable: false,
),
);
Expand Down
7 changes: 3 additions & 4 deletions packages/devtools_app/lib/src/framework/scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,10 @@ class DevToolsScaffold extends StatefulWidget {
isEmbedded() ? 2.0 : intermediateSpacing,
);

// Note: when changing this value, also update `flameChartContainerOffset`
// from flame_chart.dart.
/// Horizontal padding around the content in the DevTools UI.
static EdgeInsets get horizontalPadding =>
EdgeInsets.symmetric(horizontal: isEmbedded() ? 2.0 : 16.0);
static EdgeInsets get horizontalPadding => EdgeInsets.symmetric(
horizontal: isEmbedded() ? densePadding : largeSpacing,
);

/// All of the [Screen]s that it's possible to navigate to from this Scaffold.
final List<Screen> screens;
Expand Down

0 comments on commit a750a19

Please sign in to comment.