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

Support IJ 233 with new theming API #169

Merged
merged 3 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
matrix:
supported-ij-version:
- 232
# - 233
- 233

steps:
- uses: actions/checkout@v3
Expand All @@ -35,7 +35,7 @@ jobs:
matrix:
supported-ij-version:
- 232
# - 233
- 233

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
matrix:
supported-ij-version:
- 232
# - 233
- 233

steps:
- uses: actions/checkout@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,12 @@ internal object IntUiThemeDescriptorReader {
}

addProperty(createOverrideStringMapProperty("iconOverrides", iconOverrides))
addProperty(createOverrideStringMapProperty("selectionColorPalette", theme.iconColorsOnSelection))
addProperty(
createOverrideStringMapProperty(
"selectionColorPalette",
theme.iconColorsOnSelection
)
)
}.build())

addProperty(
Expand All @@ -185,11 +190,11 @@ internal object IntUiThemeDescriptorReader {
)
}

private fun createOverrideStringMapProperty(name: String, values: Map<String, String>) =
private inline fun <reified K, reified V> createOverrideStringMapProperty(name: String, values: Map<K, V>) =
PropertySpec.builder(
name = name,
type = Map::class.asTypeName()
.parameterizedBy(String::class.asTypeName(), String::class.asTypeName()),
.parameterizedBy(K::class.asTypeName(), V::class.asTypeName()),
KModifier.OVERRIDE
)
.initializer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,5 @@ data class IntellijThemeDescriptor(
val colors: Map<String, String> = emptyMap(),
val ui: Map<String, JsonElement> = emptyMap(),
val icons: Map<String, JsonElement> = emptyMap(),
val iconColorsOnSelection: Map<String, String> = emptyMap(),
val iconColorsOnSelection: Map<String, Int> = emptyMap(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import androidx.compose.ui.graphics.Color
interface IntelliJThemeIconData {

val iconOverrides: Map<String, String>
val colorPalette: Map<String, String>
val selectionColorPalette: Map<String, String>
val colorPalette: Map<String, String?>
val selectionColorPalette: Map<String, Int>

fun selectionColorMapping() =
selectionColorPalette.mapNotNull { (key, value) ->
val keyColor = key.toColorOrNull() ?: return@mapNotNull null
val valueColor = value.toColorOrNull() ?: return@mapNotNull null
val valueColor = Color(value)
keyColor to valueColor
}.toMap()
}
Expand Down Expand Up @@ -41,7 +41,7 @@ object EmptyThemeIconData : IntelliJThemeIconData {

override val colorPalette: Map<String, String> = emptyMap()

override val selectionColorPalette: Map<String, String> = emptyMap()
override val selectionColorPalette: Map<String, Int> = emptyMap()

override fun toString() =
"EmptyThemeIconData(iconOverrides=[], colorPalette=[], selectionColorPalette=[])"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ package org.jetbrains.jewel
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.PROPERTY,
)
annotation class InternalJewelApi
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import org.jetbrains.jewel.PaletteMapper
abstract class PaletteMapperFactory {

protected fun createInternal(
iconColorPalette: Map<String, String>,
keyPalette: Map<String, String>,
iconColorPalette: Map<String, String?>,
keyPalette: Map<String, String?>,
themeColors: Map<String, Any>,
isDark: Boolean,
): PaletteMapper {
Expand All @@ -20,15 +20,20 @@ abstract class PaletteMapperFactory {
val map = selectMap(key, checkBoxes, trees, ui) ?: continue

// If the value is one of the named colors in the theme, use that named color's value
val namedColor = themeColors.get(value) as? String
val resolvedValue = namedColor ?: value
val namedColor = themeColors[value]?.let { rawColor ->
when (rawColor) {
is Int -> Color(rawColor)
is String -> rawColor.toColorOrNull()
else -> null
}
}

// If either the key or the resolved value aren't valid colors, ignore the entry
val keyAsColor = resolveKeyColor(key, keyPalette, isDark) ?: continue
val resolvedValueAsColor = resolvedValue.toColorOrNull() ?: continue
val resolvedColor = namedColor ?: value?.toColorOrNull() ?: continue

// Save the new entry (oldColor -> newColor) in the map
map[keyAsColor] = resolvedValueAsColor
map[keyAsColor] = resolvedColor
}

return PaletteMapper(
Expand All @@ -39,7 +44,7 @@ abstract class PaletteMapperFactory {
}

// See com.intellij.ide.ui.UITheme.toColorString
private fun resolveKeyColor(key: String, keyPalette: Map<String, String>, isDark: Boolean): Color? {
private fun resolveKeyColor(key: String, keyPalette: Map<String, String?>, isDark: Boolean): Color? {
val darkKey = "$key.Dark"
val resolvedKey = if (isDark && keyPalette.containsKey(darkKey)) darkKey else key
return keyPalette[resolvedKey]?.toColorOrNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ object StandalonePaletteMapperFactory : PaletteMapperFactory() {
isDark = isDark,
)

private fun Map<String, Color>.asColorStringsMap() =
private fun Map<String, Color>.asColorStringsMap(): Map<String, Int> =
mapValues { (_, color) ->
"#${color.toArgb().toString(16).padStart(6, '0')}"
color.toArgb()
}

override fun logInfo(message: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package org.jetbrains.jewel.bridge

import androidx.compose.runtime.Immutable
import com.intellij.ide.ui.UITheme
import com.intellij.ui.ColorUtil
import org.jetbrains.jewel.IntelliJThemeIconData
import org.jetbrains.jewel.InternalJewelApi

@Immutable
internal class BridgeIconData(
@InternalJewelApi
class BridgeIconData(
override val iconOverrides: Map<String, String>,
override val colorPalette: Map<String, String>,
override val selectionColorPalette: Map<String, String>,
override val selectionColorPalette: Map<String, Int>,
) : IntelliJThemeIconData {

override fun equals(other: Any?): Boolean {
Expand Down Expand Up @@ -37,10 +40,13 @@ internal class BridgeIconData(

companion object {

@OptIn(InternalJewelApi::class)
fun readFromLaF(): BridgeIconData {
val uiTheme = currentUiThemeOrNull()
val iconMap = uiTheme?.icons.orEmpty()
val selectedIconColorPalette = uiTheme?.selectedIconColorPalette.orEmpty()
val selectedIconColorPalette = uiTheme?.selectedIconColorPalette.orEmpty().mapValues {
ColorUtil.fromHex(it.value).rgb
}

val colorPalette = UITheme.getColorPalette()
return BridgeIconData(iconMap, colorPalette, selectedIconColorPalette)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.jetbrains.jewel.bridge

import com.intellij.util.ui.DirProvider
import org.jetbrains.jewel.InternalJewelApi

@InternalJewelApi
fun getPatchedIconPath(
dirProvider: DirProvider,
originalPath: String,
classLoaders: List<ClassLoader>,
): String? {
val clazz = Class.forName("com.intellij.ui.icons.CachedImageIconKt")
val patchIconPath = clazz.getMethod("patchIconPath", String::class.java, ClassLoader::class.java)
patchIconPath.isAccessible = true

// For all provided classloaders, we try to get the patched path, both using
// the original path, and an "abridged" path that has gotten the icon path prefix
// removed (the classloader is set up differently in prod IDEs and when running
// from Gradle, and the icon could be in either place depending on the environment)
val fallbackPath = originalPath.removePrefix(dirProvider.dir())
val patchedPath = classLoaders.firstNotNullOfOrNull { classLoader ->
val patchedPathAndClassLoader =
patchIconPath.invoke(null, originalPath.removePrefix("/"), classLoader)
?: patchIconPath.invoke(null, fallbackPath, classLoader)
patchedPathAndClassLoader as? Pair<*, *>
}?.first as? String

return patchedPath
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ import com.intellij.ide.ui.LafManager
import com.intellij.ide.ui.UITheme
import com.intellij.ide.ui.laf.UIThemeBasedLookAndFeelInfo
import com.intellij.openapi.diagnostic.Logger
import org.jetbrains.jewel.InternalJewelApi
import java.lang.reflect.Field

private val logger = Logger.getInstance("UiThemeExtensions")

private val classUITheme
get() = UITheme::class.java

@Suppress("UnstableApiUsage")
@InternalJewelApi
internal fun currentUiThemeOrNull() =
(LafManager.getInstance().currentLookAndFeel as? UIThemeBasedLookAndFeelInfo)?.theme

// TODO #116 replace with public API access once it's made available (IJP 233?)
internal val UITheme.icons: Map<String, String>
@InternalJewelApi
val UITheme.icons: Map<String, String>
get() = readMapField<String>(classUITheme.getDeclaredField("icons"))
.filterKeys { it != "ColorPalette" }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.jetbrains.jewel.bridge

import androidx.compose.runtime.Immutable
import com.intellij.ide.ui.UITheme
import org.jetbrains.jewel.IntelliJThemeIconData
import org.jetbrains.jewel.InternalJewelApi

@Immutable
@InternalJewelApi
class BridgeIconData(
override val iconOverrides: Map<String, String>,
override val colorPalette: Map<String, String?>,
override val selectionColorPalette: Map<String, Int>,
) : IntelliJThemeIconData {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as BridgeIconData

if (iconOverrides != other.iconOverrides) return false
if (colorPalette != other.colorPalette) return false
if (selectionColorPalette != other.selectionColorPalette) return false

return true
}

override fun hashCode(): Int {
var result = iconOverrides.hashCode()
result = 31 * result + colorPalette.hashCode()
result = 31 * result + selectionColorPalette.hashCode()
return result
}

override fun toString(): String =
"BridgeIconData(iconOverrides=$iconOverrides, colorPalette=$colorPalette, " +
"selectionColorPalette=$selectionColorPalette)"

companion object {

@Suppress("UnstableApiUsage")
fun readFromLaF(): BridgeIconData {
val uiTheme = currentUiThemeOrNull()
val bean = uiTheme?.describe()
val iconMap = bean?.icons.orEmpty()
val selectedIconColorPalette = bean?.iconColorsOnSelection.orEmpty()

val colorPalette = UITheme.getColorPalette()
return BridgeIconData(iconMap, colorPalette, selectedIconColorPalette)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.jetbrains.jewel.bridge

import com.intellij.ide.ui.UITheme
import com.intellij.openapi.diagnostic.thisLogger
import org.jetbrains.jewel.PaletteMapper
import org.jetbrains.jewel.themes.PaletteMapperFactory

object BridgePaletteMapperFactory : PaletteMapperFactory() {

private val logger = thisLogger()

@Suppress("UnstableApiUsage")
fun create(isDark: Boolean): PaletteMapper {
// If we can't read the current theme, no mapping is possible
val uiTheme = currentUiThemeOrNull() ?: return PaletteMapper.Empty
logger.info("Parsing theme info from theme ${uiTheme.name} (id: ${uiTheme.id}, isDark: ${uiTheme.isDark})")

val bean = uiTheme.describe()

val iconColorPalette = bean.colorPalette
val keyPalette = UITheme.getColorPalette()
val themeColors = bean.colors

return createInternal(iconColorPalette, keyPalette, themeColors, isDark)
}

override fun logInfo(message: String) {
logger.info(message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.jetbrains.jewel.bridge

import com.intellij.ui.icons.patchIconPath
import com.intellij.util.ui.DirProvider
import org.jetbrains.jewel.InternalJewelApi

@InternalJewelApi
@Suppress("UnstableApiUsage")
fun getPatchedIconPath(
dirProvider: DirProvider,
originalPath: String,
classLoaders: List<ClassLoader>,
): String? {
// For all provided classloaders, we try to get the patched path, both using
// the original path, and an "abridged" path that has gotten the icon path prefix
// removed (the classloader is set up differently in prod IDEs and when running
// from Gradle, and the icon could be in either place depending on the environment)
val fallbackPath = originalPath.removePrefix(dirProvider.dir())

for (classLoader in classLoaders) {
val patchedPath = patchIconPath(originalPath.removePrefix("/"), classLoader)?.first
?: patchIconPath(fallbackPath, classLoader)?.first

if (patchedPath != null) {
return patchedPath
}
}
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.jetbrains.jewel.bridge

import com.intellij.ide.ui.LafManager
import com.intellij.ide.ui.laf.UIThemeLookAndFeelInfo

@Suppress("UnstableApiUsage")
internal fun currentUiThemeOrNull(): UIThemeLookAndFeelInfo? =
LafManager.getInstance().currentUIThemeLookAndFeel?.takeIf { it.isInitialized }
Loading