Skip to content

Commit af7e4f3

Browse files
#333 Dynamically change UI themes for Recent Activity
1 parent bd2debc commit af7e4f3

File tree

12 files changed

+294
-39
lines changed

12 files changed

+294
-39
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies{
2828
implementation(project(":java"))
2929
implementation(project(":python"))
3030
implementation(project(":rider"))
31+
implementation("org.freemarker:freemarker:2.3.30")
3132
}
3233

3334
// Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin

src/main/java/org/digma/intellij/plugin/toolwindow/CustomResourceHandler.kt

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package org.digma.intellij.plugin.toolwindow
22

3+
import freemarker.template.Configuration
34
import org.cef.callback.CefCallback
45
import org.cef.handler.CefLoadHandler
56
import org.cef.handler.CefResourceHandler
67
import org.cef.misc.IntRef
78
import org.cef.misc.StringRef
89
import org.cef.network.CefRequest
910
import org.cef.network.CefResponse
11+
import java.io.ByteArrayInputStream
1012
import java.io.IOException
1113
import java.io.InputStream
14+
import java.io.StringWriter
1215
import java.net.URLConnection
13-
import kotlin.math.min
16+
17+
const val INDEX_TEMPLATE_FILE: String = "indextemplate.ftl"
18+
const val BASE_PACKAGE_PATH: String = "/webview"
19+
const val ENV_VARIABLE_THEME: String = "theme"
1420

1521
class CustomResourceHandler : CefResourceHandler {
1622
private var state: ResourceHandlerState = ClosedConnection
@@ -20,10 +26,15 @@ class CustomResourceHandler : CefResourceHandler {
2026
): Boolean {
2127
val processedUrl = cefRequest.url
2228
return if (processedUrl != null) {
23-
val pathToResource = processedUrl.replace("http://myapp", "webview/")
24-
val newUrl = javaClass.classLoader.getResource(pathToResource)
25-
if (newUrl != null) {
26-
state = OpenedConnection(newUrl.openConnection())
29+
if (processedUrl == "http://myapp/index.html") {
30+
val html = loadFreemarkerTemplate()
31+
state = StringData(html)
32+
} else {
33+
val pathToResource = processedUrl.replace("http://myapp", "webview/")
34+
val newUrl = javaClass.classLoader.getResource(pathToResource)
35+
if (newUrl != null) {
36+
state = OpenedConnection(newUrl.openConnection())
37+
}
2738
}
2839
cefCallback.Continue()
2940
true
@@ -61,9 +72,9 @@ sealed interface ResourceHandlerState {
6172

6273
fun readResponse(
6374
dataOut: ByteArray,
64-
designedBytesToRead: Int,
65-
bytesRead: IntRef,
66-
callback: CefCallback
75+
designedBytesToRead: Int,
76+
bytesRead: IntRef,
77+
callback: CefCallback
6778
): Boolean
6879

6980
fun close(): Unit
@@ -77,7 +88,7 @@ data class OpenedConnection(val connection: URLConnection) : ResourceHandlerStat
7788
cefResponse.mimeType = URLConnection.guessContentTypeFromName(connection.url.file)
7889
responseLength.set(inputStream.available())
7990
cefResponse.status = 200
80-
} catch ( e: IOException) {
91+
} catch (e: IOException) {
8192
cefResponse.error = CefLoadHandler.ErrorCode.ERR_FILE_NOT_FOUND
8293
cefResponse.statusText = e.localizedMessage
8394
cefResponse.status = 404
@@ -90,17 +101,13 @@ data class OpenedConnection(val connection: URLConnection) : ResourceHandlerStat
90101
bytesRead: IntRef,
91102
callback: CefCallback
92103
): Boolean {
93-
val availableSize = inputStream.available()
94-
return if (availableSize > 0) {
95-
val maxBytesToRead = min(availableSize, designedBytesToRead)
96-
val realNumberOfReadBytes =
97-
inputStream.read(dataOut, 0, maxBytesToRead)
98-
bytesRead.set(realNumberOfReadBytes)
99-
true
100-
} else {
101-
inputStream.close()
102-
false
103-
}
104+
return CustomResourceHandlerUtil.readResponse(
105+
inputStream,
106+
dataOut,
107+
designedBytesToRead,
108+
bytesRead,
109+
callback
110+
)
104111
}
105112

106113
override fun close(): Unit = inputStream.close()
@@ -125,3 +132,46 @@ object ClosedConnection : ResourceHandlerState {
125132

126133
override fun close() {}
127134
}
135+
136+
data class StringData(val data: String) : ResourceHandlerState {
137+
private val inputStream = ByteArrayInputStream(data.toByteArray(Charsets.UTF_8))
138+
139+
override fun getResponseHeaders(
140+
cefResponse: CefResponse,
141+
responseLength: IntRef,
142+
redirectUrl: StringRef
143+
) {
144+
cefResponse.mimeType = "text/html"
145+
responseLength.set(inputStream.available())
146+
cefResponse.status = 200
147+
}
148+
149+
override fun readResponse(
150+
dataOut: ByteArray,
151+
designedBytesToRead: Int,
152+
bytesRead: IntRef,
153+
callback: CefCallback
154+
): Boolean {
155+
return CustomResourceHandlerUtil.readResponse(
156+
inputStream,
157+
dataOut,
158+
designedBytesToRead,
159+
bytesRead,
160+
callback
161+
)
162+
}
163+
164+
override fun close() {
165+
inputStream.close()
166+
}
167+
}
168+
169+
private fun loadFreemarkerTemplate(): String {
170+
val cfg = Configuration(Configuration.VERSION_2_3_30)
171+
cfg.setClassForTemplateLoading(CustomResourceHandler::class.java, BASE_PACKAGE_PATH)
172+
val template = cfg.getTemplate(INDEX_TEMPLATE_FILE)
173+
val data = mapOf(ENV_VARIABLE_THEME to ThemeUtil.getCurrentThemeName())
174+
val writer = StringWriter()
175+
template.process(data, writer)
176+
return writer.toString()
177+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.digma.intellij.plugin.toolwindow
2+
3+
import org.cef.callback.CefCallback
4+
import org.cef.misc.IntRef
5+
import java.io.InputStream
6+
import kotlin.math.min
7+
8+
class CustomResourceHandlerUtil {
9+
10+
companion object {
11+
fun readResponse(
12+
inputStream: InputStream,
13+
dataOut: ByteArray,
14+
designedBytesToRead: Int,
15+
bytesRead: IntRef,
16+
callback: CefCallback
17+
): Boolean {
18+
val availableSize = inputStream.available()
19+
return if (availableSize > 0) {
20+
val maxBytesToRead = min(availableSize, designedBytesToRead)
21+
val realNumberOfReadBytes =
22+
inputStream.read(dataOut, 0, maxBytesToRead)
23+
bytesRead.set(realNumberOfReadBytes)
24+
true
25+
} else {
26+
inputStream.close()
27+
false
28+
}
29+
}
30+
}
31+
}

src/main/java/org/digma/intellij/plugin/toolwindow/DigmaBottomToolWindowFactory.java

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ public class DigmaBottomToolWindowFactory implements ToolWindowFactory {
6565
private static final Logger LOGGER = Logger.getInstance(DigmaBottomToolWindowFactory.class);
6666
private static final String DIGMA_LEFT_TOOL_WINDOW_NAME = "Digma";
6767
private static final String SUCCESSFULLY_PROCESSED_JCEF_REQUEST_MESSAGE = "Successfully processed JCEF request with action =";
68-
private final ObjectMapper objectMapper = new ObjectMapper();
6968
private EditorService editorService;
7069
private AnalyticsService analyticsService;
7170
private String localHostname;
@@ -123,6 +122,10 @@ private Content createCodeAnalyticsTab(Project project, ToolWindow toolWindow, C
123122
JBCefClient jbCefClient = jbCefBrowser.getJBCefClient();
124123

125124
CefMessageRouter msgRouter = CefMessageRouter.create();
125+
126+
ThemeChangeListener listener = new ThemeChangeListener(jbCefBrowser);
127+
UIManager.addPropertyChangeListener(listener);
128+
126129
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
127130
@Override
128131
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
@@ -212,7 +215,7 @@ private boolean processRecentActivityGetDataRequest(AnalyticsService analyticsSe
212215
} catch (AnalyticsServiceException e) {
213216
Log.log(LOGGER::debug, "AnalyticsServiceException for getRecentActivity: {}", e.getMessage());
214217
}
215-
String requestMessage = resultToString(new JcefMessageRequest(
218+
String requestMessage = JBCefBrowserUtil.resultToString(new JcefMessageRequest(
216219
REQUEST_MESSAGE_TYPE,
217220
RECENT_ACTIVITY_SET_DATA,
218221
new JcefMessagePayload(
@@ -221,7 +224,7 @@ private boolean processRecentActivityGetDataRequest(AnalyticsService analyticsSe
221224
)
222225
));
223226

224-
postJSMessage(requestMessage, jbCefBrowser);
227+
JBCefBrowserUtil.postJSMessage(requestMessage, jbCefBrowser);
225228

226229
callback.success(SUCCESSFULLY_PROCESSED_JCEF_REQUEST_MESSAGE + " RECENT_ACTIVITY_SET_DATA at " + new Date());
227230
return true;
@@ -246,22 +249,6 @@ private String getAdjustedEnvName(String environment) {
246249
return environment.toUpperCase().endsWith("["+ LOCAL_ENV + "]") ? LOCAL_ENV: environment;
247250
}
248251

249-
private void postJSMessage(String jsonMessage, JBCefBrowser jbCefBrowser) {
250-
jbCefBrowser.getCefBrowser().executeJavaScript(
251-
"window.postMessage(" + jsonMessage + ");",
252-
jbCefBrowser.getCefBrowser().getURL(),
253-
0
254-
);
255-
}
256-
257-
private String resultToString(Object result) {
258-
try {
259-
return objectMapper.writeValueAsString(result);
260-
} catch (Exception e) {
261-
return "Error parsing object " + e.getMessage();
262-
}
263-
}
264-
265252
private <T> T parseJsonToObject(String jsonString, Class<T> jcefMessageRequestClass) {
266253
JsonObject object = JsonParser.parseString(jsonString).getAsJsonObject();
267254
return new Gson().fromJson(object, jcefMessageRequestClass);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.digma.intellij.plugin.toolwindow;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.intellij.ui.jcef.JBCefBrowser;
5+
6+
public class JBCefBrowserUtil {
7+
8+
public static void postJSMessage(String jsonMessage, JBCefBrowser jbCefBrowser) {
9+
jbCefBrowser.getCefBrowser().executeJavaScript(
10+
"window.postMessage(" + jsonMessage + ");",
11+
jbCefBrowser.getCefBrowser().getURL(),
12+
0
13+
);
14+
}
15+
16+
public static String resultToString(Object result) {
17+
try {
18+
return new ObjectMapper().writeValueAsString(result);
19+
} catch (Exception e) {
20+
return "Error parsing object " + e.getMessage();
21+
}
22+
}
23+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.digma.intellij.plugin.toolwindow;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.intellij.openapi.diagnostic.Logger;
5+
import com.intellij.ui.jcef.JBCefBrowser;
6+
import org.apache.commons.lang3.StringUtils;
7+
import org.digma.intellij.plugin.log.Log;
8+
9+
import java.beans.PropertyChangeEvent;
10+
import java.beans.PropertyChangeListener;
11+
12+
import static org.digma.intellij.plugin.toolwindow.ToolWindowUtil.RECENT_ACTIVITY_SET_UI_THEME;
13+
import static org.digma.intellij.plugin.toolwindow.ToolWindowUtil.REQUEST_MESSAGE_TYPE;
14+
15+
public class ThemeChangeListener implements PropertyChangeListener {
16+
private static final Logger LOGGER = Logger.getInstance(ThemeChangeListener.class);
17+
private JBCefBrowser jbCefBrowser;
18+
19+
public ThemeChangeListener(JBCefBrowser jbCefBrowser) {
20+
this.jbCefBrowser = jbCefBrowser;
21+
}
22+
23+
public void propertyChange(PropertyChangeEvent evt) {
24+
if ("lookAndFeel".equals(evt.getPropertyName())) {
25+
// The UI theme has been changed
26+
changeUiThemeForRecentActivity();
27+
}
28+
}
29+
30+
private void changeUiThemeForRecentActivity() {
31+
String theme = ThemeUtil.getCurrentThemeName();
32+
if (StringUtils.isNotEmpty(theme)) {
33+
changeUiThemeForRecentActivityTab(jbCefBrowser, theme);
34+
Log.log(LOGGER::debug, "UI theme changed to " + theme);
35+
} else {
36+
Log.log(LOGGER::debug, "UI theme was not changed because theme was null or empty.");
37+
}
38+
}
39+
40+
private void changeUiThemeForRecentActivityTab(JBCefBrowser jbCefBrowser, String uiTheme) {
41+
String requestMessage = JBCefBrowserUtil.resultToString(new UIThemeRequest(
42+
REQUEST_MESSAGE_TYPE,
43+
RECENT_ACTIVITY_SET_UI_THEME,
44+
new UiThemePayload(uiTheme)
45+
));
46+
JBCefBrowserUtil.postJSMessage(requestMessage, jbCefBrowser);
47+
}
48+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.digma.intellij.plugin.toolwindow;
2+
3+
import javax.swing.*;
4+
import java.awt.*;
5+
6+
public class ThemeUtil {
7+
8+
public static String getCurrentThemeName() {
9+
String lafClassName = UIManager.getLookAndFeel().getClass().getSimpleName();
10+
if ("IntelliJLaf".equals(lafClassName) || "MacIntelliJLaf".equals(lafClassName)) {
11+
Color bgColor = UIManager.getLookAndFeelDefaults().getColor("EditorPane.background");
12+
if (bgColor != null && isDark(bgColor)) {
13+
return UiTheme.DARK.getThemeName();
14+
} else {
15+
return UiTheme.LIGHT.getThemeName();
16+
}
17+
} else {
18+
return UiTheme.DARK.getThemeName();
19+
}
20+
}
21+
22+
private static boolean isDark(Color color) {
23+
// Calculate the perceived brightness of the color using the sRGB color space
24+
double brightness = (0.2126 * color.getRed() + 0.7152 * color.getGreen() + 0.0722 * color.getBlue()) / 255;
25+
return brightness < 0.5;
26+
}
27+
}

src/main/java/org/digma/intellij/plugin/toolwindow/ToolWindowUtil.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public class ToolWindowUtil {
44
public static final String RECENT_ACTIVITY_SET_DATA = "RECENT_ACTIVITY/SET_DATA";
5+
public static final String RECENT_ACTIVITY_SET_UI_THEME = "GLOBAL/SET_THEME";
56
public static final String RECENT_ACTIVITY_GET_DATA = "RECENT_ACTIVITY/GET_DATA";
67
public static final String RECENT_ACTIVITY_GO_TO_SPAN = "RECENT_ACTIVITY/GO_TO_SPAN";
78
public static final String RECENT_ACTIVITY_GO_TO_TRACE = "RECENT_ACTIVITY/GO_TO_TRACE";
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.digma.intellij.plugin.toolwindow
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator
4+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
5+
import java.beans.ConstructorProperties
6+
7+
8+
data class UIThemeRequest
9+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
10+
@JsonIgnoreProperties(ignoreUnknown = true)
11+
@ConstructorProperties("type", "action", "payload")
12+
constructor(
13+
val type: String?,
14+
val action: String,
15+
val payload: UiThemePayload
16+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.digma.intellij.plugin.toolwindow;
2+
3+
public enum UiTheme {
4+
DARK("dark"),
5+
LIGHT("light"),
6+
;
7+
8+
private String themeName;
9+
UiTheme(String themeName) {
10+
this.themeName = themeName;
11+
}
12+
13+
public String getThemeName() {
14+
return themeName;
15+
}
16+
17+
}

0 commit comments

Comments
 (0)