Skip to content

Commit 5d838d4

Browse files
authored
Send theme changed event over DTD (#7647)
Addresses #7626
1 parent 056a5dd commit 5d838d4

File tree

5 files changed

+155
-34
lines changed

5 files changed

+155
-34
lines changed

flutter-idea/src/io/flutter/FlutterInitializer.java

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
package io.flutter;
77

88
import com.google.common.annotations.VisibleForTesting;
9+
import com.google.gson.JsonObject;
10+
import com.google.gson.JsonPrimitive;
911
import com.intellij.ProjectTopics;
1012
import com.intellij.ide.BrowserUtil;
1113
import com.intellij.ide.plugins.IdeaPluginDescriptor;
1214
import com.intellij.ide.plugins.PluginManagerCore;
15+
import com.intellij.ide.ui.UISettingsListener;
1316
import com.intellij.ide.util.PropertiesComponent;
1417
import com.intellij.notification.*;
1518
import com.intellij.openapi.actionSystem.AnAction;
1619
import com.intellij.openapi.actionSystem.AnActionEvent;
1720
import com.intellij.openapi.application.ApplicationInfo;
1821
import com.intellij.openapi.application.ApplicationManager;
22+
import com.intellij.openapi.diagnostic.Logger;
23+
import com.intellij.openapi.editor.colors.EditorColorsListener;
24+
import com.intellij.openapi.editor.colors.EditorColorsManager;
1925
import com.intellij.openapi.extensions.PluginId;
2026
import com.intellij.openapi.fileEditor.FileEditorManager;
2127
import com.intellij.openapi.module.Module;
@@ -25,16 +31,19 @@
2531
import com.intellij.openapi.roots.ProjectRootManager;
2632
import com.intellij.openapi.startup.StartupActivity;
2733
import com.intellij.openapi.vfs.VirtualFile;
34+
import com.intellij.util.messages.MessageBusConnection;
2835
import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService;
36+
import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService;
37+
import de.roderick.weberknecht.WebSocketException;
2938
import io.flutter.analytics.Analytics;
3039
import io.flutter.analytics.FlutterAnalysisServerListener;
3140
import io.flutter.analytics.ToolWindowTracker;
3241
import io.flutter.analytics.UnifiedAnalytics;
3342
import io.flutter.android.IntelliJAndroidSdk;
3443
import io.flutter.bazel.WorkspaceCache;
35-
import io.flutter.deeplinks.DeepLinksViewFactory;
44+
import io.flutter.dart.DtdUtils;
3645
import io.flutter.devtools.DevToolsExtensionsViewFactory;
37-
import io.flutter.devtools.DevToolsExtensionsViewService;
46+
import io.flutter.devtools.DevToolsUtils;
3847
import io.flutter.devtools.RemainingDevToolsViewFactory;
3948
import io.flutter.editor.FlutterSaveActionsManager;
4049
import io.flutter.logging.FlutterConsoleLogManager;
@@ -59,6 +68,10 @@
5968
import javax.swing.event.HyperlinkEvent;
6069
import java.util.List;
6170
import java.util.UUID;
71+
import java.util.concurrent.ExecutionException;
72+
import java.util.concurrent.Executors;
73+
import java.util.concurrent.TimeUnit;
74+
import java.util.concurrent.atomic.AtomicLong;
6275

6376
/**
6477
* Runs actions after the project has started up and the index is up to date.
@@ -68,6 +81,7 @@
6881
* may run when a project is being imported.
6982
*/
7083
public class FlutterInitializer implements StartupActivity {
84+
private static final Logger LOG = Logger.getInstance(FlutterInitializer.class);
7185
private static final String analyticsClientIdKey = "io.flutter.analytics.clientId";
7286
private static final String analyticsOptOutKey = "io.flutter.analytics.optOut";
7387
private static final String analyticsToastShown = "io.flutter.analytics.toastShown";
@@ -76,6 +90,10 @@ public class FlutterInitializer implements StartupActivity {
7690

7791
private boolean toolWindowsInitialized = false;
7892

93+
private boolean busSubscribed = false;
94+
95+
private @NotNull AtomicLong lastScheduledThemeChangeTime = new AtomicLong();
96+
7997
@Override
8098
public void runActivity(@NotNull Project project) {
8199
// Convert all modules of deprecated type FlutterModuleType.
@@ -194,6 +212,9 @@ public void moduleAdded(@NotNull Project project, @NotNull Module module) {
194212
// Set our preferred settings for the run console.
195213
FlutterConsoleLogManager.initConsolePreferences();
196214

215+
// Initialize notifications for theme changes.
216+
setUpThemeChangeNotifications(project);
217+
197218
setUpDtdAnalytics(project);
198219

199220
// Initialize analytics.
@@ -255,6 +276,80 @@ private void setUpDtdAnalytics(Project project) {
255276
t1.start();
256277
}
257278

279+
private void setUpThemeChangeNotifications(Project project) {
280+
if (project == null) return;
281+
FlutterSdk sdk = FlutterSdk.getFlutterSdk(project);
282+
if (sdk == null || !sdk.getVersion().canUseDtd()) return;
283+
Thread t1 = new Thread(() -> {
284+
if (busSubscribed) return;
285+
final MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
286+
connection.subscribe(EditorColorsManager.TOPIC, (EditorColorsListener)scheme -> {
287+
sendThemeChangedEvent(project);
288+
});
289+
connection.subscribe(UISettingsListener.TOPIC, (UISettingsListener)scheme -> {
290+
sendThemeChangedEvent(project);
291+
});
292+
busSubscribed = true;
293+
});
294+
t1.start();
295+
}
296+
297+
private void sendThemeChangedEvent(@NotNull Project project) {
298+
// Debounce this request because the topic subscriptions can trigger multiple times (potentially from initial notification of change and
299+
// also from application of change)
300+
301+
// Set the current time of this request
302+
final long requestTime = System.currentTimeMillis();
303+
lastScheduledThemeChangeTime.set(requestTime);
304+
305+
// Schedule event to be sent in a second if nothing more recent has come in.
306+
Executors.newSingleThreadScheduledExecutor().schedule(() -> {
307+
if (lastScheduledThemeChangeTime.get() != requestTime) {
308+
// A more recent request has been set, so drop this request.
309+
return;
310+
}
311+
312+
final JsonObject params = new JsonObject();
313+
params.addProperty("eventKind", "themeChanged");
314+
params.addProperty("streamId", "Editor");
315+
final JsonObject eventData = new JsonObject();
316+
final DevToolsUtils utils = new DevToolsUtils();
317+
eventData.addProperty("isDarkMode", Boolean.FALSE.equals(utils.getIsBackgroundBright()));
318+
eventData.addProperty("backgroundColor", utils.getColorHexCode());
319+
eventData.addProperty("fontSize", utils.getFontSize().intValue());
320+
params.add("eventData", eventData);
321+
322+
try {
323+
final DtdUtils dtdUtils = new DtdUtils();
324+
final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get();
325+
if (dtdService == null) {
326+
LOG.error("Unable to send theme changed event because DTD service is null");
327+
return;
328+
}
329+
330+
dtdService.sendRequest("postEvent", params, false, object -> {
331+
JsonObject result = object.getAsJsonObject("result");
332+
if (result == null) {
333+
LOG.error("Theme changed event returned null result");
334+
return;
335+
}
336+
JsonPrimitive type = result.getAsJsonPrimitive("type");
337+
if (type == null) {
338+
LOG.error("Theme changed event result type is null");
339+
return;
340+
}
341+
if (!"Success".equals(type.getAsString())) {
342+
LOG.error("Theme changed event result: " + type.getAsString());
343+
}
344+
}
345+
);
346+
}
347+
catch (WebSocketException | InterruptedException | ExecutionException e) {
348+
LOG.error("Unable to send theme changed event", e);
349+
}
350+
}, 1, TimeUnit.SECONDS);
351+
}
352+
258353
private static void enableAnalytics(@NotNull Project project) {
259354
ToolWindowTracker.track(project, getAnalytics());
260355
DartAnalysisServerService.getInstance(project).addAnalysisServerListener(FlutterAnalysisServerListener.getInstance(project));

flutter-idea/src/io/flutter/analytics/UnifiedAnalytics.java

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.intellij.openapi.project.Project;
1818
import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService;
1919
import de.roderick.weberknecht.WebSocketException;
20+
import io.flutter.dart.DtdUtils;
2021
import io.flutter.sdk.FlutterSdkUtil;
2122
import org.jetbrains.annotations.NotNull;
2223
import org.jetbrains.annotations.Nullable;
@@ -31,19 +32,19 @@ public class UnifiedAnalytics {
3132

3233
@Nullable Boolean enabled = null;
3334
final Project project;
34-
final DartToolingDaemonService dtdService;
35+
final DtdUtils dtdUtils;
3536
@NotNull final FlutterSdkUtil flutterSdkUtil;
3637

3738

3839
public UnifiedAnalytics(@NotNull Project project) {
3940
this.project = project;
40-
this.dtdService = DartToolingDaemonService.getInstance(project);
41+
this.dtdUtils = new DtdUtils();
4142
this.flutterSdkUtil = new FlutterSdkUtil();
4243
}
4344

4445
public void manageConsent() {
4546
try {
46-
DartToolingDaemonService service = readyDtdService().get();
47+
DartToolingDaemonService service = dtdUtils.readyDtdService(project).get();
4748
if (service != null) {
4849
final JsonObject params = new JsonObject();
4950
params.addProperty("tool", getToolName());
@@ -201,28 +202,4 @@ else if ("Android-Studio".equals(ideValue)) {
201202
return innerResult;
202203
});
203204
}
204-
205-
private CompletableFuture<DartToolingDaemonService> readyDtdService() {
206-
CompletableFuture<DartToolingDaemonService> readyService = new CompletableFuture<>();
207-
int attemptsRemaining = 10;
208-
final int TIME_IN_BETWEEN = 2;
209-
while (attemptsRemaining > 0) {
210-
attemptsRemaining--;
211-
if (dtdService != null && dtdService.getWebSocketReady()) {
212-
readyService.complete(dtdService);
213-
break;
214-
}
215-
try {
216-
Thread.sleep(TIME_IN_BETWEEN * 1000);
217-
}
218-
catch (InterruptedException e) {
219-
readyService.completeExceptionally(e);
220-
break;
221-
}
222-
}
223-
if (!readyService.isDone()) {
224-
readyService.completeExceptionally(new Exception("Timed out waiting for DTD websocket to start"));
225-
}
226-
return readyService;
227-
}
228205
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2024 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
package io.flutter.dart;
7+
8+
import com.intellij.openapi.project.Project;
9+
import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import java.util.concurrent.CompletableFuture;
13+
14+
public class DtdUtils {
15+
public CompletableFuture<DartToolingDaemonService> readyDtdService(@NotNull Project project) {
16+
final DartToolingDaemonService dtdService = DartToolingDaemonService.getInstance(project);
17+
CompletableFuture<DartToolingDaemonService> readyService = new CompletableFuture<>();
18+
int attemptsRemaining = 10;
19+
final int TIME_IN_BETWEEN = 2;
20+
while (attemptsRemaining > 0) {
21+
attemptsRemaining--;
22+
if (dtdService.getWebSocketReady()) {
23+
readyService.complete(dtdService);
24+
break;
25+
}
26+
try {
27+
Thread.sleep(TIME_IN_BETWEEN * 1000);
28+
}
29+
catch (InterruptedException e) {
30+
readyService.completeExceptionally(e);
31+
break;
32+
}
33+
}
34+
if (!readyService.isDone()) {
35+
readyService.completeExceptionally(new Exception("Timed out waiting for DTD websocket to start"));
36+
}
37+
return readyService;
38+
}
39+
}

flutter-idea/src/io/flutter/devtools/DevToolsUrl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.nio.charset.StandardCharsets;
1919
import java.util.ArrayList;
2020
import java.util.List;
21+
import java.util.Objects;
2122

2223
public class DevToolsUrl {
2324
private String devToolsHost;
@@ -38,7 +39,7 @@ public class DevToolsUrl {
3839

3940
public final DevToolsIdeFeature ideFeature;
4041

41-
private final DevToolsUtils devToolsUtils;
42+
@NotNull private final DevToolsUtils devToolsUtils;
4243

4344
public static class Builder {
4445
private String devToolsHost;
@@ -222,7 +223,7 @@ public String getUrlString() {
222223

223224
public void maybeUpdateColor() {
224225
final String newColor = devToolsUtils.getColorHexCode();
225-
if (colorHexCode == newColor) {
226+
if (Objects.equals(colorHexCode, newColor)) {
226227
return;
227228
}
228229

@@ -231,8 +232,7 @@ public void maybeUpdateColor() {
231232
}
232233

233234
public void maybeUpdateFontSize() {
234-
EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
235-
final Float newFontSize = (float) scheme.getEditorFontSize();
235+
final Float newFontSize = devToolsUtils.getFontSize();
236236
if (fontSize == null || !fontSize.equals(newFontSize)) {
237237
fontSize = newFontSize;
238238
}

flutter-idea/src/io/flutter/devtools/DevToolsUtils.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
*/
66
package io.flutter.devtools;
77

8+
import com.intellij.openapi.editor.colors.EditorColorsManager;
9+
import com.intellij.openapi.editor.colors.EditorColorsScheme;
810
import com.intellij.ui.ColorUtil;
911
import com.intellij.ui.JBColor;
1012
import com.intellij.util.ui.UIUtil;
13+
import org.jetbrains.annotations.NotNull;
1114

1215
public class DevToolsUtils {
1316
public static String findWidgetId(String url) {
@@ -29,5 +32,12 @@ public Boolean getIsBackgroundBright() {
2932
return JBColor.isBright();
3033
}
3134

32-
public Float getFontSize() {return UIUtil.getFontSize(UIUtil.FontSize.NORMAL);}
35+
public @NotNull Float getFontSize() {
36+
EditorColorsManager manager = EditorColorsManager.getInstance();
37+
if (manager == null) {
38+
// Return the default normal font size if editor manager is not found.
39+
return UIUtil.getFontSize(UIUtil.FontSize.NORMAL);
40+
}
41+
return (float) manager.getGlobalScheme().getEditorFontSize();
42+
}
3343
}

0 commit comments

Comments
 (0)