Stager scene for Property Editor  (#8573)

diff --git a/.vscode/launch.json b/.vscode/launch.json
index 26aabef..2f91630 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -106,13 +106,20 @@
             "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",
+            "program": "packages/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": "standalone_ui/property_editor_sidebar",
+            "request": "launch",
+            "type": "dart",
+            "program": "packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.stager_app.g.dart",
+            "preLaunchTask": "Start DTD on Port 8500",
+        },
+        {
             "name": "devtools_extensions: foo + sim",
             "request": "launch",
             "type": "dart",
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart
index 157eb5d..0372549 100644
--- a/packages/devtools_app/lib/src/shared/analytics/constants.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart
@@ -17,6 +17,7 @@
 part 'constants/_memory_constants.dart';
 part 'constants/_network_constants.dart';
 part 'constants/_performance_constants.dart';
+part 'constants/_property_editor_sidebar_constants.dart';
 
 // Type of events (event_category):
 const screenViewEvent = 'screen'; // Active screen (tab selected).
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_property_editor_sidebar_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_property_editor_sidebar_constants.dart
new file mode 100644
index 0000000..0fc2ea4
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/analytics/constants/_property_editor_sidebar_constants.dart
@@ -0,0 +1,18 @@
+// 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.
+
+part of '../constants.dart';
+
+// TODO(elliette): Send the following events from the property editor.
+enum PropertyEditorEvents {
+  /// Analytics event that is sent when the property editor is updated with new
+  /// properties.
+  widgetPropertiesUpdate,
+
+  /// Analytics event that is sent when a user requests a property edit.
+  applyEditRequest;
+
+  /// Analytics id to track events that come from the DTD editor sidebar.
+  static String get id => 'propertyEditorSidebar';
+}
diff --git a/packages/devtools_app/lib/src/standalone_ui/standalone_screen.dart b/packages/devtools_app/lib/src/standalone_ui/standalone_screen.dart
index 18fad3f..bb5954f 100644
--- a/packages/devtools_app/lib/src/standalone_ui/standalone_screen.dart
+++ b/packages/devtools_app/lib/src/standalone_ui/standalone_screen.dart
@@ -15,6 +15,8 @@
 /// meaning that this screen will not be part of DevTools' normal navigation.
 /// The only way to access a standalone screen is directly from the url.
 enum StandaloneScreenType {
+  // TODO(elliette): Add property editor as a standalone screen, see:
+  // https://github.com/flutter/devtools/issues/8546
   editorSidebar,
   vsCodeFlutterPanel; // Legacy postMessage version, shows an upgrade message.
 
diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart
index a661e27..def6b2a 100644
--- a/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart
+++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart
@@ -12,9 +12,7 @@
 import '../../service/editor/api_classes.dart';
 import '../../service/editor/editor_client.dart';
 import '../../shared/analytics/analytics.dart' as ga;
-import '../../shared/feature_flags.dart';
 import '../../shared/ui/common_widgets.dart';
-import '../ide_shared/property_editor/property_editor_sidebar.dart';
 import 'debug_sessions.dart';
 import 'devices.dart';
 import 'devtools/devtools_view.dart';
@@ -195,9 +193,6 @@
                   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(),
             ],
           ),
         ),
diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/property_editor_panel.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/property_editor_panel.dart
new file mode 100644
index 0000000..88d165e
--- /dev/null
+++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/property_editor_panel.dart
@@ -0,0 +1,114 @@
+// 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 'dart:async';
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:dtd/dtd.dart';
+import 'package:flutter/material.dart';
+
+import '../../service/editor/editor_client.dart';
+import '../../shared/analytics/analytics.dart' as ga;
+import '../../shared/analytics/constants.dart';
+import '../../shared/ui/common_widgets.dart';
+import '../ide_shared/property_editor/property_editor_sidebar.dart';
+
+/// The side panel for the Property Editor.
+class PropertyEditorSidebarPanel extends StatefulWidget {
+  const PropertyEditorSidebarPanel(this.dtd, {super.key});
+
+  final DartToolingDaemon dtd;
+
+  @override
+  State<PropertyEditorSidebarPanel> createState() =>
+      _PropertyEditorSidebarPanelState();
+}
+
+class _PropertyEditorSidebarPanelState
+    extends State<PropertyEditorSidebarPanel> {
+  _PropertyEditorSidebarPanelState();
+
+  Future<EditorClient>? _editor;
+
+  @override
+  void initState() {
+    super.initState();
+
+    final editor = EditorClient(widget.dtd);
+    ga.screen(PropertyEditorEvents.id);
+    unawaited(_editor = editor.initialized.then((_) => editor));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.topCenter,
+      child: FutureBuilder(
+        future: _editor,
+        builder:
+            (context, snapshot) => switch ((
+              snapshot.connectionState,
+              snapshot.data,
+            )) {
+              (ConnectionState.done, final editor?) =>
+                _PropertyEditorConnectedPanel(editor),
+              _ => const CenteredCircularProgressIndicator(),
+            },
+      ),
+    );
+  }
+}
+
+/// The property editor panel shown once we know an editor is available.
+class _PropertyEditorConnectedPanel extends StatefulWidget {
+  const _PropertyEditorConnectedPanel(this.editor);
+
+  final EditorClient editor;
+
+  @override
+  State<_PropertyEditorConnectedPanel> createState() =>
+      _PropertyEditorConnectedPanelState();
+}
+
+class _PropertyEditorConnectedPanelState
+    extends State<_PropertyEditorConnectedPanel>
+    with AutoDisposeMixin {
+  late final ScrollController scrollController;
+
+  @override
+  void initState() {
+    super.initState();
+    scrollController = ScrollController();
+  }
+
+  @override
+  void dispose() {
+    scrollController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scrollbar(
+      controller: scrollController,
+      thumbVisibility: true,
+      child: SingleChildScrollView(
+        controller: scrollController,
+        child: const Padding(
+          padding: EdgeInsets.fromLTRB(
+            denseSpacing,
+            defaultSpacing,
+            defaultSpacing, // Additional right padding for scroll bar.
+            defaultSpacing,
+          ),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [PropertyEditorSidebar()],
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_sidebar.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_sidebar.dart
index 50a7618..f0236cc 100644
--- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_sidebar.dart
+++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_sidebar.dart
@@ -7,7 +7,6 @@
 import 'package:devtools_app/devtools_app.dart';
 import 'package:devtools_app/src/standalone_ui/vs_code/flutter_panel.dart';
 import 'package:devtools_app_shared/service.dart';
-import 'package:devtools_app_shared/shared.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_test/devtools_test.dart';
@@ -17,7 +16,8 @@
 
 import 'editor_service/simulated_editor.dart';
 import 'mock_editor_widget.dart';
-import 'utils.dart';
+import 'shared/common_ui.dart';
+import 'shared/utils.dart';
 
 /// To run, use the "standalone_ui/editor_sidebar" launch configuration with the
 /// `devtools/packages/` folder open in VS Code, or run:
@@ -30,17 +30,7 @@
 
   @override
   Widget build(BuildContext context) {
-    return MaterialApp(
-      theme: themeFor(
-        isDarkTheme: false,
-        ideTheme: _ideTheme(const VsCodeTheme.light()),
-        theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
-      ),
-      darkTheme: themeFor(
-        isDarkTheme: true,
-        ideTheme: _ideTheme(const VsCodeTheme.dark()),
-        theme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
-      ),
+    return IdeThemedMaterialApp(
       home: Scaffold(
         body: MockEditorWidget(
           editor: editor,
@@ -51,15 +41,6 @@
     );
   }
 
-  /// Creates an [IdeTheme] using the colours from the mock editor.
-  IdeTheme _ideTheme(VsCodeTheme vsCodeTheme) {
-    return IdeTheme(
-      backgroundColor: vsCodeTheme.editorBackgroundColor,
-      foregroundColor: vsCodeTheme.foregroundColor,
-      embedMode: EmbedMode.embedOne,
-    );
-  }
-
   @override
   String get title => '$EditorSidebarScene';
 
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart
index 2159b2d..7db0edb 100644
--- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart
+++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart
@@ -12,6 +12,7 @@
 import 'package:flutter/material.dart';
 
 import 'editor_service/simulated_editor.dart';
+import 'shared/common_ui.dart';
 
 /// A simple UI that acts as a stand-in host editor to simplify the development
 /// workflow when working on embedded tooling.
@@ -335,41 +336,3 @@
     };
   }
 }
-
-/// A basic theme that matches the default colours of VS Code dart/light themes
-/// so the mock environment can be displayed in either.
-class VsCodeTheme {
-  const VsCodeTheme._({
-    required this.activityBarBackgroundColor,
-    required this.editorBackgroundColor,
-    required this.foregroundColor,
-    required this.sidebarBackgroundColor,
-  });
-
-  const VsCodeTheme.dark()
-    : this._(
-        activityBarBackgroundColor: const Color(0xFF333333),
-        editorBackgroundColor: const Color(0xFF1E1E1E),
-        foregroundColor: const Color(0xFFD4D4D4),
-        sidebarBackgroundColor: const Color(0xFF252526),
-      );
-
-  const VsCodeTheme.light()
-    : this._(
-        activityBarBackgroundColor: const Color(0xFF2C2C2C),
-        editorBackgroundColor: const Color(0xFFFFFFFF),
-        foregroundColor: const Color(0xFF000000),
-        sidebarBackgroundColor: const Color(0xFFF3F3F3),
-      );
-
-  static VsCodeTheme of(BuildContext context) {
-    return Theme.of(context).isDarkTheme
-        ? const VsCodeTheme.dark()
-        : const VsCodeTheme.light();
-  }
-
-  final Color activityBarBackgroundColor;
-  final Color editorBackgroundColor;
-  final Color foregroundColor;
-  final Color sidebarBackgroundColor;
-}
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.dart
new file mode 100644
index 0000000..c85a7ec
--- /dev/null
+++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.dart
@@ -0,0 +1,139 @@
+// 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 'dart:async';
+
+import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/standalone_ui/vs_code/property_editor_panel.dart';
+import 'package:devtools_app_shared/service.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:devtools_test/devtools_test.dart';
+import 'package:dtd/dtd.dart';
+import 'package:flutter/material.dart';
+import 'package:stager/stager.dart';
+
+import 'editor_service/simulated_editor.dart';
+import 'mock_editor_widget.dart';
+import 'shared/common_ui.dart';
+import 'shared/utils.dart';
+
+/// To run, use the "standalone_ui/property_editor_sidebar" launch configuration with the
+/// `devtools/packages/` folder open in VS Code, or run:
+///
+/// flutter run -t test/test_infra/scenes/standalone_ui/property_editor_sidebar.stager_app.g.dart -d chrome
+class PropertyEditorSidebarScene extends Scene {
+  @override
+  Widget build(BuildContext context) {
+    return const _PropertyEditorSidebar();
+  }
+
+  @override
+  String get title => '$PropertyEditorSidebarScene';
+
+  @override
+  Future<void> setUp() async {
+    setStagerMode();
+    setGlobal(
+      DevToolsEnvironmentParameters,
+      ExternalDevToolsEnvironmentParameters(),
+    );
+    setGlobal(DTDManager, MockDTDManager());
+    setGlobal(IdeTheme, IdeTheme());
+    setGlobal(PreferencesController, PreferencesController());
+  }
+}
+
+class _PropertyEditorSidebar extends StatefulWidget {
+  const _PropertyEditorSidebar();
+
+  @override
+  State<_PropertyEditorSidebar> createState() => _PropertyEditorState();
+}
+
+class _PropertyEditorState extends State<_PropertyEditorSidebar> {
+  Stream<String>? clientLog;
+  DartToolingDaemon? clientDtd;
+  SimulatedEditor? editor;
+
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return IdeThemedMaterialApp(
+      home: Scaffold(
+        body:
+            clientLog != null && clientDtd != null && editor != null
+                ? MockEditorWidget(
+                  editor: editor!,
+                  clientLog: clientLog!,
+                  child: PropertyEditorSidebarPanel(clientDtd!),
+                )
+                : _DtdUriForm(
+                  onSaved: _connectToDtd,
+                  formKey: GlobalKey<FormState>(),
+                ),
+      ),
+    );
+  }
+
+  Future<void> _connectToDtd(String? dtdUri) async {
+    if (dtdUri == null) return;
+    final uri = Uri.parse(dtdUri);
+    final connection = await createLoggedWebSocketChannel(uri);
+    setState(() {
+      clientLog = connection.log;
+      clientDtd = DartToolingDaemon.fromStreamChannel(connection.channel);
+      editor = SimulatedEditor(uri);
+    });
+  }
+}
+
+class _DtdUriForm extends StatelessWidget {
+  const _DtdUriForm({required this.onSaved, required this.formKey});
+
+  final void Function(String?) onSaved;
+  final GlobalKey<FormState> formKey;
+
+  // We assume a DTD is available on 8500. There's a VS Code task that
+  // launches this as part of the standalone_ui/editor_sidebar config.
+  static const _defaultDtdUri = 'ws://127.0.0.1:8500/';
+
+  @override
+  Widget build(BuildContext context) {
+    return Form(
+      key: formKey,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          const Text('Connect to DTD:'),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              Flexible(
+                child: TextFormField(
+                  initialValue: _defaultDtdUri,
+                  style: Theme.of(context).fixedFontStyle,
+                  onSaved: onSaved,
+                ),
+              ),
+              Flexible(
+                flex: 2,
+                child: ElevatedButton(
+                  onPressed: () {
+                    formKey.currentState?.save();
+                  },
+                  child: const Text('Submit'),
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/shared/common_ui.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/shared/common_ui.dart
new file mode 100644
index 0000000..ad0503e
--- /dev/null
+++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/shared/common_ui.dart
@@ -0,0 +1,77 @@
+// 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/shared.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+
+class IdeThemedMaterialApp extends StatelessWidget {
+  const IdeThemedMaterialApp({super.key, required this.home});
+
+  final Widget home;
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      theme: themeFor(
+        isDarkTheme: false,
+        ideTheme: _ideTheme(const VsCodeTheme.light()),
+        theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
+      ),
+      darkTheme: themeFor(
+        isDarkTheme: true,
+        ideTheme: _ideTheme(const VsCodeTheme.dark()),
+        theme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
+      ),
+      home: home,
+    );
+  }
+
+  /// Creates an [IdeTheme] using the colours from the mock editor.
+  IdeTheme _ideTheme(VsCodeTheme vsCodeTheme) {
+    return IdeTheme(
+      backgroundColor: vsCodeTheme.editorBackgroundColor,
+      foregroundColor: vsCodeTheme.foregroundColor,
+      embedMode: EmbedMode.embedOne,
+    );
+  }
+}
+
+/// A basic theme that matches the default colours of VS Code dart/light themes
+/// so the mock environment can be displayed in either.
+class VsCodeTheme {
+  const VsCodeTheme._({
+    required this.activityBarBackgroundColor,
+    required this.editorBackgroundColor,
+    required this.foregroundColor,
+    required this.sidebarBackgroundColor,
+  });
+
+  const VsCodeTheme.dark()
+    : this._(
+        activityBarBackgroundColor: const Color(0xFF333333),
+        editorBackgroundColor: const Color(0xFF1E1E1E),
+        foregroundColor: const Color(0xFFD4D4D4),
+        sidebarBackgroundColor: const Color(0xFF252526),
+      );
+
+  const VsCodeTheme.light()
+    : this._(
+        activityBarBackgroundColor: const Color(0xFF2C2C2C),
+        editorBackgroundColor: const Color(0xFFFFFFFF),
+        foregroundColor: const Color(0xFF000000),
+        sidebarBackgroundColor: const Color(0xFFF3F3F3),
+      );
+
+  static VsCodeTheme of(BuildContext context) {
+    return Theme.of(context).isDarkTheme
+        ? const VsCodeTheme.dark()
+        : const VsCodeTheme.light();
+  }
+
+  final Color activityBarBackgroundColor;
+  final Color editorBackgroundColor;
+  final Color foregroundColor;
+  final Color sidebarBackgroundColor;
+}
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/utils.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/shared/utils.dart
similarity index 100%
rename from packages/devtools_app/test/test_infra/scenes/standalone_ui/utils.dart
rename to packages/devtools_app/test/test_infra/scenes/standalone_ui/shared/utils.dart