normalize dialogs (#2073)

normalize dialogs
diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart
index 0313d5e..1b893ec 100644
--- a/packages/devtools_app/lib/src/app.dart
+++ b/packages/devtools_app/lib/src/app.dart
@@ -12,6 +12,7 @@
 import 'connect_screen.dart';
 import 'debugger/debugger_controller.dart';
 import 'debugger/debugger_screen.dart';
+import 'dialogs.dart';
 import 'framework/framework_core.dart';
 import 'globals.dart';
 import 'initializer.dart';
@@ -334,29 +335,29 @@
 class DevToolsAboutDialog extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
-    final textTheme = Theme.of(context).textTheme;
+    final theme = Theme.of(context);
 
-    return AlertDialog(
-      actions: [
-        DialogCloseButton(),
-      ],
+    return DevToolsDialog(
+      title: dialogTitleText(theme, 'About DevTools'),
       content: Column(
         mainAxisSize: MainAxisSize.min,
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
-          ...headerInColumn(textTheme, 'About DevTools'),
           _aboutDevTools(context),
           const SizedBox(height: defaultSpacing),
-          ...headerInColumn(textTheme, 'Feedback'),
+          ...dialogSubHeader(theme, 'Feedback'),
           Wrap(
             children: [
               const Text('Encountered an issue? Let us know at '),
-              _createFeedbackLink(context, textTheme),
+              _createFeedbackLink(context),
               const Text('.')
             ],
           ),
         ],
       ),
+      actions: [
+        DialogCloseButton(),
+      ],
     );
   }
 
@@ -364,7 +365,7 @@
     return const SelectableText('DevTools version ${devtools.version}');
   }
 
-  Widget _createFeedbackLink(BuildContext context, TextTheme textTheme) {
+  Widget _createFeedbackLink(BuildContext context) {
     const urlPath = 'github.com/flutter/devtools/issues';
 
     return InkWell(
@@ -387,15 +388,12 @@
   Widget build(BuildContext context) {
     final preferences = DevToolsApp.of(context).preferences;
 
-    return AlertDialog(
-      actions: [
-        DialogCloseButton(),
-      ],
+    return DevToolsDialog(
+      title: dialogTitleText(Theme.of(context), 'Settings'),
       content: Column(
         mainAxisSize: MainAxisSize.min,
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
-          ...headerInColumn(Theme.of(context).textTheme, 'Settings'),
           InkWell(
             onTap: () {
               preferences.toggleDarkModeTheme(!preferences.darkModeTheme.value);
@@ -419,6 +417,9 @@
           ),
         ],
       ),
+      actions: [
+        DialogCloseButton(),
+      ],
     );
   }
 }
diff --git a/packages/devtools_app/lib/src/common_widgets.dart b/packages/devtools_app/lib/src/common_widgets.dart
index 78cf724..d0620dd 100644
--- a/packages/devtools_app/lib/src/common_widgets.dart
+++ b/packages/devtools_app/lib/src/common_widgets.dart
@@ -24,20 +24,6 @@
 
 const areaPaneHeaderHeight = 36.0;
 
-List<Widget> headerInColumn(TextTheme textTheme, String title) {
-  return [
-    Text(title, style: textTheme.headline6),
-    const PaddedDivider(padding: EdgeInsets.only(bottom: denseRowSpacing)),
-  ];
-}
-
-List<Widget> subHeaderInColumn(TextTheme textTheme, String title) {
-  return [
-    Text(title, style: textTheme.subtitle2),
-    const PaddedDivider(padding: EdgeInsets.only(bottom: denseRowSpacing)),
-  ];
-}
-
 /// Convenience [Divider] with [Padding] that provides a good divider in forms.
 class PaddedDivider extends StatelessWidget {
   const PaddedDivider({
@@ -498,19 +484,6 @@
   );
 }
 
-/// A FlatButton used to close a containing dialog.
-class DialogCloseButton extends StatelessWidget {
-  @override
-  Widget build(BuildContext context) {
-    return FlatButton(
-      onPressed: () {
-        Navigator.of(context, rootNavigator: true).pop('dialog');
-      },
-      child: const Text('CLOSE'),
-    );
-  }
-}
-
 /// Toggle button for use as a child of a [ToggleButtons] widget.
 class ToggleButton extends StatelessWidget {
   const ToggleButton({
diff --git a/packages/devtools_app/lib/src/device_dialog.dart b/packages/devtools_app/lib/src/device_dialog.dart
index 0a3a133..200e792 100644
--- a/packages/devtools_app/lib/src/device_dialog.dart
+++ b/packages/devtools_app/lib/src/device_dialog.dart
@@ -10,6 +10,7 @@
 import 'auto_dispose_mixin.dart';
 import 'common_widgets.dart';
 import 'connected_app.dart';
+import 'dialogs.dart';
 import 'globals.dart';
 import 'info/info_controller.dart';
 import 'table.dart';
@@ -58,16 +59,12 @@
           '${flutterVersion.engineRevision}';
     }
 
-    return AlertDialog(
-      actions: [
-        if (connectedApp.isRunningOnDartVM) _ViewVMFlagsButton(),
-        DialogCloseButton(),
-      ],
+    return DevToolsDialog(
+      title: dialogTitleText(theme, 'Device Info'),
       content: Column(
         mainAxisSize: MainAxisSize.min,
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
-          ...headerInColumn(theme.textTheme, 'Device Info'),
           for (var name in items.keys)
             Padding(
               padding: const EdgeInsets.only(bottom: denseRowSpacing),
@@ -80,6 +77,10 @@
             ),
         ],
       ),
+      actions: [
+        if (connectedApp.isRunningOnDartVM) _ViewVMFlagsButton(),
+        DialogCloseButton(),
+      ],
     );
   }
 }
@@ -154,36 +155,31 @@
 
   @override
   Widget build(BuildContext context) {
-    final textTheme = Theme.of(context).textTheme;
+    final theme = Theme.of(context);
 
-    return AlertDialog(
-      actions: [
-        DialogCloseButton(),
-      ],
+    return DevToolsDialog(
+      title: Row(
+        children: [
+          dialogTitleText(theme, 'VM Flags'),
+          const Expanded(child: SizedBox(width: denseSpacing)),
+          Container(
+            width: defaultSearchTextWidth,
+            height: defaultSearchTextHeight,
+            child: TextField(
+              controller: filterController,
+              decoration: const InputDecoration(
+                isDense: true,
+                border: OutlineInputBorder(),
+                labelText: 'Filter',
+              ),
+            ),
+          ),
+        ],
+      ),
       content: Column(
         mainAxisSize: MainAxisSize.min,
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
-          Row(
-            children: [
-              Text('VM Flags', style: textTheme.headline6),
-              const Expanded(child: SizedBox(width: denseSpacing)),
-              Container(
-                width: defaultSearchTextWidth,
-                height: defaultSearchTextHeight,
-                child: TextField(
-                  controller: filterController,
-                  decoration: const InputDecoration(
-                    isDense: true,
-                    border: OutlineInputBorder(),
-                    labelText: 'Filter',
-                  ),
-                ),
-              ),
-            ],
-          ),
-          const PaddedDivider(
-              padding: EdgeInsets.only(bottom: denseRowSpacing)),
           SizedBox(
             width: 875,
             height: 375,
@@ -191,6 +187,9 @@
           ),
         ],
       ),
+      actions: [
+        DialogCloseButton(),
+      ],
     );
   }
 
diff --git a/packages/devtools_app/lib/src/dialogs.dart b/packages/devtools_app/lib/src/dialogs.dart
new file mode 100644
index 0000000..384c791
--- /dev/null
+++ b/packages/devtools_app/lib/src/dialogs.dart
@@ -0,0 +1,102 @@
+// Copyright 2020 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:flutter/material.dart';
+
+import 'common_widgets.dart';
+import 'theme.dart';
+
+const _dialogDefaultContext = 'dialog';
+
+Text dialogTitleText(ThemeData theme, String text) {
+  return Text(text, style: theme.textTheme.headline6);
+}
+
+List<Widget> dialogSubHeader(ThemeData theme, String titleText) {
+  return [
+    Text(titleText, style: theme.textTheme.subtitle1),
+    const PaddedDivider(padding: EdgeInsets.only(bottom: denseRowSpacing)),
+  ];
+}
+
+/// A standardized dialog for use in DevTools.
+///
+/// It normalizes dialog layout, spacing, and look and feel.
+class DevToolsDialog extends StatelessWidget {
+  const DevToolsDialog({
+    @required this.title,
+    @required this.content,
+    this.actions,
+  });
+
+  static const contentPadding = 24.0;
+
+  final Widget title;
+  final Widget content;
+  final List<Widget> actions;
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          title,
+          const PaddedDivider(
+            padding: EdgeInsets.only(bottom: denseRowSpacing),
+          ),
+        ],
+      ),
+      contentPadding: const EdgeInsets.fromLTRB(
+          contentPadding, 0, contentPadding, contentPadding),
+      content: content,
+      actions: actions,
+    );
+  }
+}
+
+/// A FlatButton used to close a containing dialog (Close).
+class DialogCloseButton extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return FlatButton(
+      onPressed: () {
+        Navigator.of(context, rootNavigator: true).pop('dialog');
+      },
+      child: const Text('CLOSE'),
+    );
+  }
+}
+
+/// A FlatButton used to close a containing dialog (Cancel).
+class DialogCancelButton extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return FlatButton(
+      onPressed: () {
+        Navigator.of(context).pop(_dialogDefaultContext);
+      },
+      child: const Text('CANCEL'),
+    );
+  }
+}
+
+/// A FlatButton used to close a containing dialog (OK).
+class DialogOkButton extends StatelessWidget {
+  const DialogOkButton(this.onOk) : super();
+
+  final Function onOk;
+
+  @override
+  Widget build(BuildContext context) {
+    return FlatButton(
+      onPressed: () {
+        if (onOk != null) onOk();
+        Navigator.of(context).pop(_dialogDefaultContext);
+      },
+      child: const Text('OK'),
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/memory/memory_filter.dart b/packages/devtools_app/lib/src/memory/memory_filter.dart
index 8dc5bcb..4a9699b 100644
--- a/packages/devtools_app/lib/src/memory/memory_filter.dart
+++ b/packages/devtools_app/lib/src/memory/memory_filter.dart
@@ -6,9 +6,8 @@
 import 'package:vm_service/vm_service.dart';
 
 import '../auto_dispose_mixin.dart';
-import '../common_widgets.dart';
+import '../dialogs.dart';
 import '../flutter_widgets/linked_scroll_controller.dart';
-import '../ui/dialog.dart';
 import '../ui/utils.dart';
 import 'memory_controller.dart';
 import 'memory_snapshot_models.dart';
@@ -260,7 +259,7 @@
 
   Widget okCancelButtons() {
     return Row(
-      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+      mainAxisAlignment: MainAxisAlignment.end,
       children: [
         DialogOkButton(
           () {
@@ -312,10 +311,9 @@
     //      - one list of libraries with at least 5 entries
     //      - one row of buttons Ok/Cancel
     // For very tall app keep the dialog at a reasonable height w/o too much vertical whitespace.
-    // The listbox area is the area that grows to accomodate the list of known libraries.
+    // The listbox area is the area that grows to accommodate the list of known libraries.
+    // TODO(devoncarew): Convert to a DevToolsDialog.
     return Dialog(
-      shape: RoundedRectangleBorder(
-          borderRadius: BorderRadius.circular(defaultDialogRadius)),
       child: LayoutBuilder(
         builder: (BuildContext context, BoxConstraints constraints) {
           return Container(
diff --git a/packages/devtools_app/lib/src/timeline/timeline_screen.dart b/packages/devtools_app/lib/src/timeline/timeline_screen.dart
index d7f5081..e180242 100644
--- a/packages/devtools_app/lib/src/timeline/timeline_screen.dart
+++ b/packages/devtools_app/lib/src/timeline/timeline_screen.dart
@@ -12,6 +12,7 @@
 import '../banner_messages.dart';
 import '../common_widgets.dart';
 import '../config_specific/import_export/import_export.dart';
+import '../dialogs.dart';
 import '../globals.dart';
 import '../notifications.dart';
 import '../octicons.dart';
@@ -354,22 +355,24 @@
   @override
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
-    return Dialog(
-      child: Container(
+
+    return DevToolsDialog(
+      title: dialogTitleText(theme, 'Recorded Streams'),
+      content: Container(
         width: dialogWidth,
-        padding: const EdgeInsets.all(defaultSpacing),
         child: Column(
-          mainAxisSize: MainAxisSize.min,
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
-            ...headerInColumn(theme.textTheme, 'Recorded Streams'),
             ..._defaultRecordedStreams(theme),
             const SizedBox(height: denseSpacing),
-            ...subHeaderInColumn(theme.textTheme, 'Advanced'),
+            ...dialogSubHeader(theme, 'Advanced'),
             ..._advancedStreams(theme),
           ],
         ),
       ),
+      actions: [
+        DialogCloseButton(),
+      ],
     );
   }
 
diff --git a/packages/devtools_app/lib/src/ui/dialog.dart b/packages/devtools_app/lib/src/ui/dialog.dart
deleted file mode 100644
index d2ef1a4..0000000
--- a/packages/devtools_app/lib/src/ui/dialog.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2019 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:flutter/material.dart';
-
-const dialogDefault = 'dialog';
-
-/// A FlatButton used to close a containing dialog - Cancel.
-class DialogCancelButton extends StatelessWidget {
-  @override
-  Widget build(BuildContext context) {
-    return FlatButton(
-      onPressed: () {
-        Navigator.of(context).pop(dialogDefault);
-      },
-      child: const Text('Cancel'),
-    );
-  }
-}
-
-/// A FlatButton used to close a containing dialog - OK.
-class DialogOkButton extends StatelessWidget {
-  const DialogOkButton(this.onOk) : super();
-
-  final Function onOk;
-
-  @override
-  Widget build(BuildContext context) {
-    return FlatButton(
-      onPressed: () {
-        if (onOk != null) onOk();
-        Navigator.of(context).pop(dialogDefault);
-      },
-      child: const Text('OK'),
-    );
-  }
-}