Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respond to ThemeChanged event from IDEs #8336

Merged
merged 24 commits into from
Oct 2, 2024
Merged

Conversation

helin24
Copy link
Member

@helin24 helin24 commented Sep 23, 2024

@helin24 helin24 marked this pull request as ready for review September 26, 2024 20:06
@helin24 helin24 requested review from kenzieschmoll and a team as code owners September 26, 2024 20:06
Copy link
Member

@kenzieschmoll kenzieschmoll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done with first pass

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md Outdated Show resolved Hide resolved
packages/devtools_app/lib/src/framework/theme_manager.dart Outdated Show resolved Hide resolved
e,
errorType: 'Dart Tooling Daemon connection failed.',
stack: st,
);
},
);

if (dtdManager.connection.value != null) {
ThemeManager(dtdManager.connection.value!).listenForThemeChanges();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of calling a method on a new class to initialize the event handler, perhaps we should add an optional parameter void Function(DtdEvent) onEvent to dtdManager.connect where we can pass a callback to initialize an event handler.

This stream subscription will need to be stored on the DTDManager class so that it can be cancelled on disconnect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean editorClient.event needs to be a var on DTD manager and set to null on disconnect? I'm not sure how to structure this because DtdEditorClient is within devtools_app while DTDManager is in devtools_app_shared. Does this mean DtdEditorClient would need to be moved to the shared package as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay I see what you are saying. The only thing that is unfortunate about having this as a separate class is that disposing the stream subscription has to take place separately instead of that being tied to the lifecycle of dtdManger. But this can be done separately.

To do this, mixin add the following to the ThemeManager class signature:
extends DisposableController with AutoDisposeControllerMixin

Then add the stream subscription with addAutoDisposeStreamSubscription.

And then just make sure you call ThemeManager.dispose in the FrameworkCore.dispose method before disposing dtdManager.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Should ThemeManager be stored in globals.dart or can it be just in this file if it's not referenced elsewhere?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in this file should be fine since it is not referenced elsewhere. Globals are generally used for Objects we want accessible from all of DevTools, and this object is just needed for the framework initialization.

Comment on lines 87 to 102
Color? _tryParseColor(String? input) {
if (input == null) return null;

try {
return parseCssHexColor(input);
} catch (e, st) {
// The user can manipulate the query string so if the value is invalid
// print the value but otherwise continue.
_log.warning(
'Failed to parse "$input" as a color from the querystring, ignoring: $e',
e,
st,
);
return null;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is duplicated in ide_theme.dart. Can we find a way to re-use?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this to a utility class; hopefully the logger param isn't too awkward

Copy link
Contributor

@DanTup DanTup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat, I'll take a look at adding the postMessage part for this and wiring up VS Code :-)

@DanTup
Copy link
Contributor

DanTup commented Oct 1, 2024

I haven't figured it out yet, but when I use this version of DevTools, the sidebar no longer loads in VS Code (it shows a spinner indefinitely - though there are no errors in the console). If I revert to the last commit this was based on, everything works fine.

I'll keep debugging, but if anyone has any ideas what could impact this (nothing in the code looks related to me!), please let me know!

@DanTup
Copy link
Contributor

DanTup commented Oct 1, 2024

I see the issue... we're creating a second DtdEditorClient on the same DTD connection. DtdEditorClient expects to have a one-to-one relationship with a DTD connection, because it calls things like streamListen('Service') and if there is another user of the connection, it may have already subscribed to that.

To fix this specific issue, we could ensure the theme manager uses the same DtdEditorClient that the sidebar is using - however I think there may be some additional issues we need to deal with here - which is if DevTools is going to have a single connection to DTD and we may want to subscribe to different kinds of services, we may need a single central class that manages tracking which services are available (since we can only subscribe to the Service stream once, and if the DtdEditorClient has done it, then other code won't be able to do it and won't know if their services are available).

@DanTup
Copy link
Contributor

DanTup commented Oct 1, 2024

Some ideas:

  1. DtdEditorClient always creates its own DTD connection from the URI, regardless of anything else
  2. Create a single DtdEditorClient in DTDManager that can be used in both places (this fixes the current issue, but doesn't make DTD generally easier to consume multiple services from)
  3. Create a wrapped version of DartToolingDaemon that supports multiple subscribers to _dtd.onEvent('Service') by tracking the active services internally, and when there's a new subscriber, it re-sends events for the active services and then forwards any new incoming events
  4. Similar to 3, but instead of trying to emulate the streams directly, the class just exposes the list of current services and has strongly-typed events for services being registered/unregistered (and DtdEditorClient uses them instead of directly interacting with the service stream at all)

@helin24
Copy link
Member Author

helin24 commented Oct 1, 2024

@DanTup thanks for the debugging and thoughts here!

I'm inclined for this PR to do:

  1. Create a single DtdEditorClient in DTDManager that can be used in both places (this fixes the current issue, but doesn't make DTD generally easier to consume multiple services from)

It seems odd to create multiple DTD connections for one service (DevTools), and it sounds like options 3 and 4 warrant more consideration outside the scope of this change. What do you think? Also @kenzieschmoll

@DanTup
Copy link
Contributor

DanTup commented Oct 1, 2024

@helin24 I started trying to share one, but then found that DtdEditorClient.event is also a single-listener stream, but both the sidebar and theme manager try to listen on it. Making it broadcast could result in lost events depending on the ordering.

So as a simple fix to avoid holding this PR up, I suggest we just use DTD directly here (don't use the editor client). It's a tiny change and I can look at refactoring DtdEditorClient later (it will be easier when we get rid of the PostMessage implementation, since they share interfaces, and I think we should do that after the next branch for stable).

Here's the diff that uses DTD directly:

PS D:\Dev\Google\devtools> git diff
diff --git a/packages/devtools_app/lib/src/framework/theme_manager.dart b/packages/devtools_app/lib/src/framework/theme_manager.dart
index f79a96b8f..c41bec15d 100644
--- a/packages/devtools_app/lib/src/framework/theme_manager.dart
+++ b/packages/devtools_app/lib/src/framework/theme_manager.dart
@@ -10,7 +10,6 @@ import 'package:dtd/dtd.dart';
 import 'package:logging/logging.dart';

 import '../service/editor/api_classes.dart';
-import '../service/editor/editor_client.dart';
 import '../shared/globals.dart';

 final _log = Logger('theme_manager');
@@ -18,17 +17,16 @@ final _log = Logger('theme_manager');
 /// Manages changes in theme settings from an editor/IDE.
 class EditorThemeManager extends DisposableController
     with AutoDisposeControllerMixin {
-  EditorThemeManager(DartToolingDaemon dtd)
-      : editorClient = DtdEditorClient(dtd);
+  EditorThemeManager(this.dtd);

-  final DtdEditorClient editorClient;
+  final DartToolingDaemon dtd;

   void listenForThemeChanges() {
     autoDisposeStreamSubscription(
-      editorClient.event.listen((event) {
-        if (event is ThemeChangedEvent) {
+      dtd.onEvent(editorStreamName).listen((event) {
+        if (event.kind == EditorEventKind.themeChanged.toString()) {
           final currentTheme = getIdeTheme();
-          final newTheme = event.theme;
+          final newTheme = ThemeChangedEvent.fromJson(event.data).theme;

           if (currentTheme.isDarkMode != newTheme.isDarkMode) {
             updateQueryParameter(

@DanTup
Copy link
Contributor

DanTup commented Oct 1, 2024

@helin24 @kenzieschmoll btw, just as an FYI in case it comes up... while testing this in VS Code I found a weird bug where query string params (for colors + font size) were being lost. It turns out to be that something in Flutter is decoding encoded #s in the URL causing anything after them to be treated as a client-side fragment any time we update the URL in updateQueryParameter.

I've filed flutter/flutter#155992 about it, but as a workaround, it turns out we will parse hex colours from the querystring with or without the hashes, so I'm updating VS Code to instead of doing (%23 is a percent-encoded #):

&backgroundColor=%23000000

Just doing:

&backgroundColor=000000

This fixes the issue I saw (which is that the background color for dark mode was always black, even when the theme had a slightly different dark shade). If IntelliJ also passes background/foreground colors, it may be worth ensuring it doesn't include the # to avoid any issue arising from that too.

@DanTup
Copy link
Contributor

DanTup commented Oct 2, 2024

@helin24 we'll need to also add the .catch for streamListen in DtdEditorClient, like this:

DanTup@b062884

(I think you should be able to easily apply this locally with git fetch https://github.com/DanTup/devtools && git cherry-pick b062884104a3498f1b1e10f785e94a36016975f8)

With this PR + that change, the sidebar is working fine in VS Code.

(I also fixed that we were using editorServiceName instead of editorStreamName in that code - the values are the same so it doesn't change anything, but it was a little misleading).

@helin24
Copy link
Member Author

helin24 commented Oct 2, 2024

@DanTup thanks for the patch! I applied it with copy and paste because I got this error from git: fatal: bad object b062884104a3498f1b1e10f785e94a36016975f8 Maybe something related to forks? 🤷

@helin24 helin24 merged commit f8a6fd0 into flutter:master Oct 2, 2024
23 checks passed
@helin24 helin24 deleted the theme-event branch October 2, 2024 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants