From a777f3197d19ab1686d5933ba00e1280b37501a0 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:49:57 -0700 Subject: [PATCH 1/6] Cherry-pick 51c6bfdf210ec7d65d4b732f83526ac13b06623c --- .../chart/controller/chart_connection.dart | 4 +- .../screens/network/network_controller.dart | 4 +- .../lib/src/shared/utils/utils.dart | 36 +++++++-- .../property_editor_controller.dart | 77 +++++++++++++------ .../test/shared/utils/utils_test.dart | 67 ++++++++++++++-- 5 files changed, 149 insertions(+), 39 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_connection.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_connection.dart index 42b971e79ac..acfc21f15ba 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_connection.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_connection.dart @@ -36,7 +36,7 @@ class ChartVmConnection extends DisposableController bool initialized = false; - DebounceTimer? _polling; + PeriodicDebouncer? _polling; late final bool isDeviceAndroid; @@ -79,7 +79,7 @@ class ChartVmConnection extends DisposableController ), ); - _polling = DebounceTimer.periodic(chartUpdateDelay, ({ + _polling = PeriodicDebouncer.run(chartUpdateDelay, ({ DebounceCancelledCallback? cancelledCallback, }) async { if (!_isConnected) { diff --git a/packages/devtools_app/lib/src/screens/network/network_controller.dart b/packages/devtools_app/lib/src/screens/network/network_controller.dart index e84d45d28c5..2a29eafbdb3 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -160,7 +160,7 @@ class NetworkController extends DevToolsScreenController /// This timestamp is on the monotonic clock used by the timeline. int lastSocketDataRefreshMicros = 0; - DebounceTimer? _pollingTimer; + PeriodicDebouncer? _pollingTimer; @visibleForTesting bool get isPolling => _pollingTimer != null; @@ -272,7 +272,7 @@ class NetworkController extends DevToolsScreenController void _updatePollingState(bool recording) { if (recording) { - _pollingTimer ??= DebounceTimer.periodic( + _pollingTimer ??= PeriodicDebouncer.run( // TODO(kenz): look into improving performance by caching more data. // Polling less frequently helps performance. _pollingDuration, diff --git a/packages/devtools_app/lib/src/shared/utils/utils.dart b/packages/devtools_app/lib/src/shared/utils/utils.dart index 5b96f475e35..e023b599d4c 100644 --- a/packages/devtools_app/lib/src/shared/utils/utils.dart +++ b/packages/devtools_app/lib/src/shared/utils/utils.dart @@ -185,17 +185,43 @@ extension IsKeyType on KeyEvent { bool get isKeyDownOrRepeat => this is KeyDownEvent || this is KeyRepeatEvent; } +/// A helper class for [Timer] functionality, where the callbacks are debounced. +class Debouncer extends Disposable { + Debouncer({required this.duration}); + + final Duration duration; + Timer? _activeTimer; + + /// Invokes the [callback] once after the specified [duration] has elapsed. + /// + /// If multiple invokations are called while the timer is running, only the + /// last one will be called once no more invokations happen within the given + /// [duration]. + void run(void Function() callback) { + if (disposed) return; + _activeTimer?.cancel(); + _activeTimer = Timer(duration, callback); + } + + @override + void dispose() { + _activeTimer?.cancel(); + _activeTimer = null; + super.dispose(); + } +} + typedef DebounceCancelledCallback = bool Function(); -/// A helper class for [Timer] functionality, where the callbacks are debounced. -class DebounceTimer { - /// A periodic timer that ensures [callback] is only called at most once - /// per [duration]. +/// A periodic debouncer that calls the given [callback] at most once per +/// [duration]. +class PeriodicDebouncer { + /// Start running the periodic debouncer. /// /// [callback] is triggered once immediately, and then every [duration] the /// timer checks to see if the previous [callback] call has finished running. /// If it has finished, then then next call to [callback] will begin. - DebounceTimer.periodic( + PeriodicDebouncer.run( Duration duration, Future Function({DebounceCancelledCallback? cancelledCallback}) callback, diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart index 3f2be326c8f..5f8a493a845 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart @@ -9,6 +9,7 @@ import '../../../shared/analytics/analytics.dart' as ga; import '../../../shared/analytics/constants.dart' as gac; import '../../../shared/editor/api_classes.dart'; import '../../../shared/editor/editor_client.dart'; +import '../../../shared/utils/utils.dart'; typedef EditableWidgetData = ({List args, String? name, String? documentation}); @@ -22,7 +23,7 @@ typedef EditArgumentFunction = class PropertyEditorController extends DisposableController with AutoDisposeControllerMixin { PropertyEditorController(this.editorClient) { - _init(); + init(); } final EditorClient editorClient; @@ -36,7 +37,15 @@ class PropertyEditorController extends DisposableController _editableWidgetData; final _editableWidgetData = ValueNotifier(null); - void _init() { + late final Debouncer _editableArgsDebouncer; + + static const _editableArgsDebounceDuration = Duration(milliseconds: 600); + + @override + void init() { + super.init(); + _editableArgsDebouncer = Debouncer(duration: _editableArgsDebounceDuration); + autoDisposeStreamSubscription( editorClient.activeLocationChangedStream.listen((event) async { final textDocument = event.textDocument; @@ -45,34 +54,33 @@ class PropertyEditorController extends DisposableController if (textDocument == null) { return; } - // Don't do anything if the event corresponds to the current position. - if (textDocument == _currentDocument && + // Don't do anything if the event corresponds to the current position + // and document version. + // + // Note: This is only checked if the document version is not null. For + // IntelliJ, the document verison is always null, so identical events + // indicating a valid change are possible. + if (textDocument.version != null && + textDocument == _currentDocument && cursorPosition == _currentCursorPosition) { return; } - _currentDocument = textDocument; - _currentCursorPosition = cursorPosition; - // Get the editable arguments for the current position. - final result = await editorClient.getEditableArguments( - textDocument: textDocument, - position: cursorPosition, - ); - final args = result?.args ?? []; - final name = result?.name; - _editableWidgetData.value = ( - args: args, - name: name, - documentation: result?.documentation, - ); - // Register impression. - ga.impression( - gaId, - gac.PropertyEditorSidebar.widgetPropertiesUpdate(name: name), + _editableArgsDebouncer.run( + () => _updateWithEditableArgs( + textDocument: textDocument, + cursorPosition: cursorPosition, + ), ); }), ); } + @override + void dispose() { + _editableArgsDebouncer.dispose(); + super.dispose(); + } + Future editArgument({ required String name, required T value, @@ -88,6 +96,31 @@ class PropertyEditorController extends DisposableController ); } + Future _updateWithEditableArgs({ + required TextDocument textDocument, + required CursorPosition cursorPosition, + }) async { + _currentDocument = textDocument; + _currentCursorPosition = cursorPosition; + // Get the editable arguments for the current position. + final result = await editorClient.getEditableArguments( + textDocument: textDocument, + position: cursorPosition, + ); + final args = result?.args ?? []; + final name = result?.name; + _editableWidgetData.value = ( + args: args, + name: name, + documentation: result?.documentation, + ); + // Register impression. + ga.impression( + gaId, + gac.PropertyEditorSidebar.widgetPropertiesUpdate(name: name), + ); + } + @visibleForTesting void initForTestsOnly({ EditableArgumentsResult? editableArgsResult, diff --git a/packages/devtools_app/test/shared/utils/utils_test.dart b/packages/devtools_app/test/shared/utils/utils_test.dart index 9fa072201b2..b9cbdf7c9ec 100644 --- a/packages/devtools_app/test/shared/utils/utils_test.dart +++ b/packages/devtools_app/test/shared/utils/utils_test.dart @@ -12,11 +12,11 @@ import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('DebounceTimer', () { + group('PeriodicDebouncer', () { test('the callback happens immediately', () { fakeAsync((async) { int callbackCounter = 0; - DebounceTimer.periodic(const Duration(seconds: 1), ({ + PeriodicDebouncer.run(const Duration(seconds: 1), ({ DebounceCancelledCallback? cancelledCallback, }) async { callbackCounter++; @@ -30,7 +30,7 @@ void main() { test('only triggers another callback after the first is done', () { fakeAsync((async) { int callbackCounter = 0; - DebounceTimer.periodic(const Duration(milliseconds: 500), ({ + PeriodicDebouncer.run(const Duration(milliseconds: 500), ({ DebounceCancelledCallback? cancelledCallback, }) async { callbackCounter++; @@ -44,7 +44,7 @@ void main() { test('calls the callback at the beginning and then once per period', () { fakeAsync((async) { int callbackCounter = 0; - DebounceTimer.periodic(const Duration(seconds: 1), ({ + PeriodicDebouncer.run(const Duration(seconds: 1), ({ DebounceCancelledCallback? cancelledCallback, }) async { callbackCounter++; @@ -60,7 +60,7 @@ void main() { () { fakeAsync((async) { int callbackCounter = 0; - final timer = DebounceTimer.periodic(const Duration(seconds: 1), ({ + final timer = PeriodicDebouncer.run(const Duration(seconds: 1), ({ DebounceCancelledCallback? cancelledCallback, }) async { callbackCounter++; @@ -82,7 +82,7 @@ void main() { () { fakeAsync((async) { int callbackCounter = 0; - final timer = DebounceTimer.periodic(const Duration(seconds: 1), ({ + final timer = PeriodicDebouncer.run(const Duration(seconds: 1), ({ DebounceCancelledCallback? cancelledCallback, }) async { callbackCounter++; @@ -102,7 +102,7 @@ void main() { test('cancels the callback when cancelled during the first callback', () { fakeAsync((async) { int callbackCounter = 0; - final timer = DebounceTimer.periodic(const Duration(seconds: 1), ({ + final timer = PeriodicDebouncer.run(const Duration(seconds: 1), ({ DebounceCancelledCallback? cancelledCallback, }) async { callbackCounter++; @@ -123,7 +123,7 @@ void main() { test('cancels the callback when cancelled during the Nth callback', () { fakeAsync((async) { int callbackCounter = 0; - final timer = DebounceTimer.periodic(const Duration(seconds: 1), ({ + final timer = PeriodicDebouncer.run(const Duration(seconds: 1), ({ DebounceCancelledCallback? cancelledCallback, }) async { callbackCounter++; @@ -142,6 +142,57 @@ void main() { }); }); + group('Debouncer', () { + late Debouncer debouncer; + late int callbackCount; + + setUp(() { + callbackCount = 0; + debouncer = Debouncer(duration: const Duration(milliseconds: 100)); + }); + + tearDown(() { + debouncer.dispose(); + }); + + test('calls callback after duration', () async { + debouncer.run(() => callbackCount++); + expect(callbackCount, 0); + await Future.delayed(const Duration(milliseconds: 150)); + expect(callbackCount, 1); + }); + + test('debounces multiple calls', () async { + debouncer.run(() => callbackCount++); + debouncer.run(() => callbackCount++); + debouncer.run(() => callbackCount++); + expect(callbackCount, 0); + await Future.delayed(const Duration(milliseconds: 150)); + expect(callbackCount, 1); + }); + + test('calls callback after multiple calls with delay', () async { + debouncer.run(() => callbackCount++); + await Future.delayed(const Duration(milliseconds: 50)); + debouncer.run(() => callbackCount++); + await Future.delayed(const Duration(milliseconds: 50)); + debouncer.run(() => callbackCount++); + expect(callbackCount, 0); + await Future.delayed(const Duration(milliseconds: 150)); + expect(callbackCount, 1); + }); + + test('dispose cancels timer', () async { + debouncer.run(() => callbackCount++); + debouncer.dispose(); + await Future.delayed(const Duration(milliseconds: 150)); + expect(callbackCount, 0); + debouncer.run(() => callbackCount++); + await Future.delayed(const Duration(milliseconds: 150)); + expect(callbackCount, 0); + }); + }); + group('InterruptableChunkWorker', () { late InterruptableChunkWorker worker; late List indexes; From 678ad9cc75b0e0ab41ebefad94370eb11e23204f Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 20 Mar 2025 09:59:01 -0700 Subject: [PATCH 2/6] [Property Editor] Show reconnection overlay when disconnection detected (#9043) --- .../timeline_events/timeline_events_view.dart | 38 +++------- .../lib/src/shared/editor/editor_client.dart | 21 ++++++ .../lib/src/shared/ui/common_widgets.dart | 55 ++++++++++++++ .../property_editor_controller.dart | 27 +++++++ .../property_editor_panel.dart | 49 ++++++++----- .../property_editor/property_editor_view.dart | 10 ++- .../property_editor/reconnecting_overlay.dart | 71 +++++++++++++++++++ .../property_editor/utils/_utils_desktop.dart | 4 ++ .../property_editor/utils/_utils_web.dart | 4 ++ .../property_editor/utils/utils.dart | 7 ++ 10 files changed, 237 insertions(+), 49 deletions(-) create mode 100644 packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/reconnecting_overlay.dart diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart index 3687b02c914..1a44da58283 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart @@ -3,7 +3,6 @@ // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. import 'dart:async'; -import 'dart:math' as math; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; @@ -65,40 +64,21 @@ class _TimelineEventsTabViewState extends State } void _insertOverlay() { - final width = math.min( - _overlaySize.width, - MediaQuery.of(context).size.width - 2 * denseSpacing, - ); - final height = math.min( - _overlaySize.height, - MediaQuery.of(context).size.height - 2 * denseSpacing, - ); final theme = Theme.of(context); _refreshingOverlay?.remove(); Overlay.of(context).insert( _refreshingOverlay = OverlayEntry( maintainState: true, builder: (context) { - return Center( - child: Padding( - padding: const EdgeInsets.only(top: _overlayOffset), - child: RoundedOutlinedBorder( - clip: true, - child: Container( - width: width, - height: height, - color: theme.colorScheme.semiTransparentOverlayColor, - child: Center( - child: Text( - 'Refreshing the timeline...\n\n' - 'This may take a few seconds. Please do not\n' - 'refresh the page.', - textAlign: TextAlign.center, - style: theme.textTheme.titleMedium, - ), - ), - ), - ), + return DevToolsOverlay( + topOffset: _overlayOffset, + maxSize: _overlaySize, + content: Text( + 'Refreshing the timeline...\n\n' + 'This may take a few seconds. Please do not\n' + 'refresh the page.', + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium, ), ); }, diff --git a/packages/devtools_app/lib/src/shared/editor/editor_client.dart b/packages/devtools_app/lib/src/shared/editor/editor_client.dart index 8a29cc0e905..e9ee7105463 100644 --- a/packages/devtools_app/lib/src/shared/editor/editor_client.dart +++ b/packages/devtools_app/lib/src/shared/editor/editor_client.dart @@ -314,6 +314,27 @@ class EditorClient extends DisposableController } } + Future isClientClosed() async { + try { + // Make an empty request to DTD. + await _dtd.call('', ''); + } on StateError catch (e) { + // TODO(https://github.com/flutter/devtools/issues/9028): Replace with a + // check for whether DTD is closed. Requires a change to package:dtd. + // + // This is only a temporary fix. If the error in package:json_rpc_2 is + // changed, this will no longer catch it. See: + // https://github.com/dart-lang/tools/blob/b55643dadafd3ac6b2bd20823802f75929ebf98e/pkgs/json_rpc_2/lib/src/client.dart#L151 + if (e.message.contains('The client is closed.')) { + return true; + } + } catch (e) { + // Ignore other exceptions. If the client is open, we expect this to fail + // with the error: 'Unknown method "."'. + } + return false; + } + Future _call( EditorMethod method, { Map? params, diff --git a/packages/devtools_app/lib/src/shared/ui/common_widgets.dart b/packages/devtools_app/lib/src/shared/ui/common_widgets.dart index fb8bf822149..20f5610ad8a 100644 --- a/packages/devtools_app/lib/src/shared/ui/common_widgets.dart +++ b/packages/devtools_app/lib/src/shared/ui/common_widgets.dart @@ -2283,3 +2283,58 @@ class _PositiveIntegerSettingState extends State ); } } + +/// Creates an overlay with the provided [content]. +/// +/// Set [fullScreen] to true to take up the entire screen. Otherwise, a +/// [maxSize] and [topOffset] can be provided to determine the overlay's size +/// and location. +class DevToolsOverlay extends StatelessWidget { + const DevToolsOverlay({ + super.key, + required this.content, + this.fullScreen = false, + this.maxSize, + this.topOffset, + }) : assert(maxSize != null || topOffset != null ? !fullScreen : true); + + final Widget content; + final bool fullScreen; + final Size? maxSize; + final double? topOffset; + + @override + Widget build(BuildContext context) { + final parentSize = MediaQuery.of(context).size; + + final overlayContent = Container( + width: _overlayWidth(parentSize), + height: _overlayHeight(parentSize), + color: Theme.of(context).colorScheme.semiTransparentOverlayColor, + child: Center(child: content), + ); + + return fullScreen + ? overlayContent + : Center( + child: Padding( + padding: EdgeInsets.only(top: topOffset ?? 0.0), + child: RoundedOutlinedBorder(clip: true, child: overlayContent), + ), + ); + } + + double _overlayWidth(Size parentSize) { + if (fullScreen) return parentSize.width; + final defaultWidth = parentSize.width - largeSpacing; + return maxSize != null ? min(maxSize!.width, defaultWidth) : defaultWidth; + } + + double _overlayHeight(Size parentSize) { + if (fullScreen) return parentSize.height; + final defaultHeight = parentSize.height - largeSpacing; + return maxSize != null + ? min(maxSize!.height, defaultHeight) + : defaultHeight; + } +} diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart index 5f8a493a845..572d1509e41 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart @@ -2,6 +2,8 @@ // 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 'dart:async'; + import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; @@ -37,17 +39,31 @@ class PropertyEditorController extends DisposableController _editableWidgetData; final _editableWidgetData = ValueNotifier(null); + ValueListenable get shouldReconnect => _shouldReconnect; + final _shouldReconnect = ValueNotifier(false); + + bool get waitingForFirstEvent => _waitingForFirstEvent; + bool _waitingForFirstEvent = true; + late final Debouncer _editableArgsDebouncer; + late final Timer _checkConnectionTimer; + static const _editableArgsDebounceDuration = Duration(milliseconds: 600); + static const _checkConnectionInterval = Duration(minutes: 1); + @override void init() { super.init(); _editableArgsDebouncer = Debouncer(duration: _editableArgsDebounceDuration); + _checkConnectionTimer = _periodicallyCheckConnection( + _checkConnectionInterval, + ); autoDisposeStreamSubscription( editorClient.activeLocationChangedStream.listen((event) async { + if (_waitingForFirstEvent) _waitingForFirstEvent = false; final textDocument = event.textDocument; final cursorPosition = event.selections.first.active; // Don't do anything if the text document is null. @@ -78,6 +94,7 @@ class PropertyEditorController extends DisposableController @override void dispose() { _editableArgsDebouncer.dispose(); + _checkConnectionTimer.cancel(); super.dispose(); } @@ -121,6 +138,16 @@ class PropertyEditorController extends DisposableController ); } + Timer _periodicallyCheckConnection(Duration interval) { + return Timer.periodic(interval, (timer) async { + final isClosed = await editorClient.isClientClosed(); + if (isClosed) { + _shouldReconnect.value = true; + timer.cancel(); + } + }); + } + @visibleForTesting void initForTestsOnly({ EditableArgumentsResult? editableArgsResult, diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart index b0c67e9a554..7cb8ff3b5f1 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart @@ -15,6 +15,7 @@ import '../../../shared/editor/editor_client.dart'; import '../../../shared/ui/common_widgets.dart'; import 'property_editor_controller.dart'; import 'property_editor_view.dart'; +import 'reconnecting_overlay.dart'; /// The side panel for the Property Editor. class PropertyEditorPanel extends StatefulWidget { @@ -106,24 +107,36 @@ class _PropertyEditorConnectedPanelState @override Widget build(BuildContext context) { - return Scrollbar( - controller: scrollController, - thumbVisibility: true, - child: SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.fromLTRB( - denseSpacing, - defaultSpacing, - defaultSpacing, // Additional right padding for scroll bar. - defaultSpacing, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [PropertyEditorView(controller: widget.controller)], - ), - ), - ), + return ValueListenableBuilder( + valueListenable: widget.controller.shouldReconnect, + builder: (context, shouldReconnect, _) { + return Stack( + children: [ + Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + controller: scrollController, + child: Padding( + padding: const EdgeInsets.fromLTRB( + denseSpacing, + defaultSpacing, + defaultSpacing, // Additional right padding for scroll bar. + defaultSpacing, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PropertyEditorView(controller: widget.controller), + ], + ), + ), + ), + ), + if (shouldReconnect) const ReconnectingOverlay(), + ], + ); + }, ); } } diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart index 3db138d8e9e..4007a5436f5 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart @@ -37,8 +37,14 @@ class PropertyEditorView extends StatelessWidget { final editableWidgetData = values.third as EditableWidgetData?; if (editableWidgetData == null) { - return const CenteredMessage( - message: 'No Flutter widget found at the current cursor location.', + final introSentence = + controller.waitingForFirstEvent + ? '👋 Welcome to the Flutter Property Editor!' + : 'No Flutter widget found at the current cursor location.'; + const howToUseSentence = + 'Please move your cursor to a Flutter widget constructor invocation to view its properties.'; + return CenteredMessage( + message: '$introSentence\n\n$howToUseSentence', ); } diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/reconnecting_overlay.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/reconnecting_overlay.dart new file mode 100644 index 00000000000..66f9f3394b2 --- /dev/null +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/reconnecting_overlay.dart @@ -0,0 +1,71 @@ +// 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 'dart:async'; + +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; + +import '../../../shared/ui/common_widgets.dart'; +import 'utils/utils.dart'; + +class ReconnectingOverlay extends StatefulWidget { + const ReconnectingOverlay({super.key}); + + @override + State createState() => _ReconnectingOverlayState(); +} + +class _ReconnectingOverlayState extends State { + static const _countdownInterval = Duration(seconds: 1); + late final Timer _countdownTimer; + int _secondsUntilReconnection = 3; + + @override + void initState() { + super.initState(); + _countdownTimer = Timer.periodic(_countdownInterval, _onTick); + } + + @override + void dispose() { + _countdownTimer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return DevToolsOverlay( + fullScreen: true, + content: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: defaultSpacing), + Text( + _secondsUntilReconnection > 0 + ? 'Reconnecting in $_secondsUntilReconnection' + : 'Reconnecting...', + style: theme.textTheme.headlineMedium, + ), + ], + ), + ); + } + + void _onTick(Timer timer) { + setState(() { + _secondsUntilReconnection--; + if (_secondsUntilReconnection == 0) { + timer.cancel(); + _reconnect(); + } + }); + } + + void _reconnect() { + forceReload(); + } +} diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_desktop.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_desktop.dart index c79da0f1bac..ff2333a299b 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_desktop.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_desktop.dart @@ -2,6 +2,10 @@ // 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. +void reloadIframe() { + // No-op for desktop platforms. +} + void addBlurListener() { // No-op for desktop platforms. } diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_web.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_web.dart index 87b99146ce0..880e275da9c 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_web.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/_utils_web.dart @@ -6,6 +6,10 @@ import 'dart:js_interop'; import 'package:web/web.dart'; +void reloadIframe() { + window.location.reload(); +} + void addBlurListener() { window.addEventListener('blur', _onBlur.toJS); } diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart index f33d9789fa5..fa3f1d7e3a7 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart @@ -78,6 +78,13 @@ RichText convertDartDocToRichText( return RichText(text: TextSpan(children: children)); } +/// Workaround to force reload the Property Editor when it disconnects. +/// +/// See https://github.com/flutter/devtools/issues/9028 for details. +void forceReload() { + reloadIframe(); +} + /// Workaround to prevent TextFields from holding onto focus when IFRAME-ed. /// /// See https://github.com/flutter/devtools/issues/8929 for details. From 798a83f5707248aa9a571e0d7652b9bca75b61d2 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:11:01 -0700 Subject: [PATCH 3/6] Update to patch version --- packages/devtools_app/lib/devtools.dart | 2 +- packages/devtools_app/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devtools_app/lib/devtools.dart b/packages/devtools_app/lib/devtools.dart index 0da9cf9190f..69b0fb1fc14 100644 --- a/packages/devtools_app/lib/devtools.dart +++ b/packages/devtools_app/lib/devtools.dart @@ -10,4 +10,4 @@ /// Note: a regexp in the `dt update-version' command logic matches the constant /// declaration `const version =`. If you change the declaration you must also /// modify the regex in the `dt update-version' command logic. -const version = '2.44.0'; +const version = '2.44.1'; diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml index 8e027f80a4c..d6e9a7d24ad 100644 --- a/packages/devtools_app/pubspec.yaml +++ b/packages/devtools_app/pubspec.yaml @@ -7,7 +7,7 @@ publish_to: none # Note: this version should only be updated by running the 'dt update-version' # command that updates the version here and in 'devtools.dart'. -version: 2.44.0 +version: 2.44.1 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app From 82ac072204abfbdee461d71b566d7d0906508a81 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:47:40 -0700 Subject: [PATCH 4/6] Update property_editor_controller.dart --- .../ide_shared/property_editor/property_editor_controller.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart index 30f1f512aff..ac4ed486a7f 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart @@ -95,7 +95,6 @@ class PropertyEditorController extends DisposableController cursorPosition == _currentCursorPosition) { return; } - if (!textDocument.uriAsString.endsWith('.dart')) { _editableWidgetData.value = ( properties: [], From 1deb82d2232ca2ee13d286d88881b48c675f9419 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:49:00 -0700 Subject: [PATCH 5/6] Update editor_client.dart --- .../lib/src/shared/editor/editor_client.dart | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/devtools_app/lib/src/shared/editor/editor_client.dart b/packages/devtools_app/lib/src/shared/editor/editor_client.dart index 839cea0d22c..9d55c021864 100644 --- a/packages/devtools_app/lib/src/shared/editor/editor_client.dart +++ b/packages/devtools_app/lib/src/shared/editor/editor_client.dart @@ -319,27 +319,6 @@ class EditorClient extends DisposableController } } - Future isClientClosed() async { - try { - // Make an empty request to DTD. - await _dtd.call('', ''); - } on StateError catch (e) { - // TODO(https://github.com/flutter/devtools/issues/9028): Replace with a - // check for whether DTD is closed. Requires a change to package:dtd. - // - // This is only a temporary fix. If the error in package:json_rpc_2 is - // changed, this will no longer catch it. See: - // https://github.com/dart-lang/tools/blob/b55643dadafd3ac6b2bd20823802f75929ebf98e/pkgs/json_rpc_2/lib/src/client.dart#L151 - if (e.message.contains('The client is closed.')) { - return true; - } - } catch (e) { - // Ignore other exceptions. If the client is open, we expect this to fail - // with the error: 'Unknown method "."'. - } - return false; - } - Future _call( EditorMethod method, { Map? params, From c27d4823f2c1a4b1a06ba4e9f2bfa4d0d5ee0d97 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:49:40 -0700 Subject: [PATCH 6/6] Update utils.dart --- .../ide_shared/property_editor/utils/utils.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart index 1e52d2845b0..4c05833f7d5 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart @@ -142,13 +142,6 @@ void forceReload() { reloadIframe(); } -/// Workaround to force reload the Property Editor when it disconnects. -/// -/// See https://github.com/flutter/devtools/issues/9028 for details. -void forceReload() { - reloadIframe(); -} - /// Workaround to prevent TextFields from holding onto focus when IFRAME-ed. /// /// See https://github.com/flutter/devtools/issues/8929 for details.