From e4cebf9edafdc0a5157c2eebe7e002f02d7c3e5f Mon Sep 17 00:00:00 2001 From: dima-avdeev-jb <99798741+dima-avdeev-jb@users.noreply.github.com> Date: Tue, 5 Apr 2022 18:21:16 +0400 Subject: [PATCH] Intellij plugin to choose color (truth of concept) (#1990) --- .../build.gradle.kts | 11 +- .../compose/color/ColorLineMarkerProvider.kt | 120 ++++++++++++++++ .../jetbrains/compose/color/ColorPicker.kt | 128 ++++++++++++++++++ .../kotlin/com/jetbrains/compose/color/HSV.kt | 75 ++++++++++ .../src/main/resources/META-INF/plugin.xml | 5 + .../compose/color/ColorPickerUITest.kt | 25 ++++ .../com/jetbrains/compose/color/HSVTest.kt | 22 +++ 7 files changed, 381 insertions(+), 5 deletions(-) create mode 100644 examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorLineMarkerProvider.kt create mode 100644 examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorPicker.kt create mode 100644 examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/HSV.kt create mode 100644 examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/ColorPickerUITest.kt create mode 100644 examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/HSVTest.kt diff --git a/examples/intellij-plugin-with-experimental-shared-base/build.gradle.kts b/examples/intellij-plugin-with-experimental-shared-base/build.gradle.kts index e0f916a336..8ba4d49d21 100644 --- a/examples/intellij-plugin-with-experimental-shared-base/build.gradle.kts +++ b/examples/intellij-plugin-with-experimental-shared-base/build.gradle.kts @@ -18,17 +18,18 @@ repositories { } dependencies { - // runtime dependency is provided by org.jetbrains.compose.intellij.platform - compileOnly(compose.desktop.currentOs) - - testImplementation("junit", "junit", "4.12") +// compileOnly(compose.desktop.currentOs) runtime dependency is provided by org.jetbrains.compose.intellij.platform + testImplementation(kotlin("test")) } // See https://github.com/JetBrains/gradle-intellij-plugin/ intellij { version.set("2021.3") plugins.set( - listOf("org.jetbrains.compose.intellij.platform:0.1.0") + listOf( + "org.jetbrains.compose.intellij.platform:0.1.0", + "org.jetbrains.kotlin" + ) ) } diff --git a/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorLineMarkerProvider.kt b/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorLineMarkerProvider.kt new file mode 100644 index 0000000000..a9cc554ec0 --- /dev/null +++ b/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorLineMarkerProvider.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package com.jetbrains.compose.color + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.icons.AllIcons +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.uast.* +import javax.swing.JComponent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.toArgb +import com.intellij.openapi.application.ApplicationManager +import com.jetbrains.compose.theme.WidgetTheme +import org.intellij.datavis.r.inlays.components.GraphicsManager +import java.awt.Component +import java.awt.Graphics +import javax.swing.Icon + +class ColorLineMarkerProvider : LineMarkerProvider { + + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + val project = element.project + val ktPsiFactory = KtPsiFactory(project) + val uElement: UElement = element.toUElement() ?: return null + if (uElement is UCallExpression) { + if (uElement.kind == UastCallKind.METHOD_CALL && uElement.methodIdentifier?.name == "Color") { + val colorLongValue = (uElement.valueArguments.firstOrNull() as? ULiteralExpression)?.getLongValue() + val previousColor = try { + Color(colorLongValue!!) + } catch (t: Throwable) { + Color(0xffffffff) + } + + val iconSize = 20 + return LineMarkerInfo( + element, + element.textRange, + object : Icon { + override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) { + g?.color = java.awt.Color( + previousColor.red, + previousColor.green, + previousColor.blue, + previousColor.alpha + ) + g?.fillRect(0, 0, iconSize, iconSize) + } + override fun getIconWidth(): Int = iconSize + override fun getIconHeight(): Int = iconSize + }, + null, + { _, psiElement: PsiElement -> + val isDarkMode = try { + GraphicsManager.getInstance(project)?.isDarkModeEnabled ?: false + } catch (t: Throwable) { + false + } + class ChooseColorDialog() : DialogWrapper(project) { + val colorState = mutableStateOf(previousColor) + + init { + title = "Choose color" + init() + } + + override fun createCenterPanel(): JComponent = + ComposePanel().apply { + setBounds(0, 0, 400, 400) + setContent { + WidgetTheme(darkTheme = isDarkMode) { + Surface(modifier = Modifier.fillMaxSize()) { + ColorPicker(colorState) + } + } + } + } + } + + val chooseColorDialog = ChooseColorDialog() + val result = chooseColorDialog.showAndGet() + if (result) { + val color = chooseColorDialog.colorState.value + ApplicationManager.getApplication().runWriteAction { + psiElement.replace( + ktPsiFactory.createExpression( + "Color(${color.toHexString()})" + ) + ) + } + } + }, + GutterIconRenderer.Alignment.RIGHT, + { "change color literal" } + ) + } + } + return null + } + + override fun collectSlowLineMarkers( + elements: MutableList, + result: MutableCollection> + ) { + + } +} diff --git a/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorPicker.kt b/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorPicker.kt new file mode 100644 index 0000000000..9ef49add6e --- /dev/null +++ b/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/ColorPicker.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package com.jetbrains.compose.color + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp + +private const val VALUE_BAND_RATIO = 0.07f +private val DEFAULT_COLORS = + listOf(Color.Red, Color.Green, Color.Blue, Color.Black, Color.Gray, Color.Yellow, Color.Cyan, Color.Magenta) + +@Composable +fun ColorPicker(colorState: MutableState) { + var currentColor: Color by remember { colorState } + Column { + Row { + DEFAULT_COLORS.forEach { + Box(Modifier.size(30.dp).background(color = it).clickable { + currentColor = it + }) + } + } + Divider(Modifier.size(2.dp)) + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text("Result color:") + Divider(Modifier.size(2.dp)) + TextField(modifier = Modifier.width(120f.dp), value = currentColor.toHexString(), onValueChange = {}) + Divider(Modifier.size(2.dp)) + val size = 60f + Box(Modifier.size(size.dp).background(color = currentColor)) + } + Divider(Modifier.size(2.dp)) + var width by remember { mutableStateOf(300) } + var height by remember { mutableStateOf(256) } + val rainbowWidth by derivedStateOf { (width * (1 - VALUE_BAND_RATIO)).toInt() } + val bandWidth by derivedStateOf { width * VALUE_BAND_RATIO } + fun calcHue(x: Float) = limit0to1(x / rainbowWidth) * HSV.HUE_MAX_VALUE + fun calcSaturation(y: Float) = 1 - limit0to1(y / height) + fun calcValue(y: Float) = 1 - limit0to1(y / height) + Row(Modifier.fillMaxSize()) { + Canvas(Modifier.fillMaxSize().pointerInput(Unit) { + width = size.width + height = size.height + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + if (event.buttons.isPrimaryPressed) { + val position = event.changes.first().position + if (position.x < rainbowWidth) { + currentColor = try { + currentColor.toHsv().copy( + hue = calcHue(position.x), + saturation = calcSaturation(position.y) + ).toRgb() + } catch (t: Throwable) { + t.printStackTrace() + println("exception $t") + currentColor + } + } else { + currentColor = + currentColor.toHsv().copy( + value = calcValue(position.y) + ).toRgb() + } + } + } + } + }) { + for (x in 0..rainbowWidth) { + for (y in 0..height) { + drawRect( + color = currentColor.toHsv().copy( + hue = calcHue(x.toFloat()), + saturation = calcSaturation(y.toFloat()) + ).toRgb(), + topLeft = Offset(x.toFloat(), y.toFloat()), + size = Size(1f, 1f) + ) + } + } + val valueBandX = rainbowWidth + 1 + for (y in 0..height) { + drawRect( + color = currentColor.toHsv().copy(value = calcValue(y.toFloat())).toRgb(), + topLeft = Offset(valueBandX.toFloat(), y.toFloat()), + size = Size(bandWidth, 1f) + ) + } + val circleX = (currentColor.toHsv().hue / 360) * rainbowWidth + val circleY = (1 - currentColor.toHsv().saturation) * height + drawCircle( + center = Offset(circleX, circleY), + color = Color.Black, + radius = 5f, + style = Stroke(width = 3f) + ) + } + } + } +} + +fun Color.toHexString() = "0x" + toArgb().toUInt().toString(16) +fun limit(value: Float, min: Float, max: Float) = minOf( + maxOf(value, min), + max +) + +fun limit0to1(value: Float) = limit(value = value, 0f, 1f) diff --git a/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/HSV.kt b/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/HSV.kt new file mode 100644 index 0000000000..86fef306c4 --- /dev/null +++ b/examples/intellij-plugin-with-experimental-shared-base/src/main/kotlin/com/jetbrains/compose/color/HSV.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package com.jetbrains.compose.color + +import androidx.compose.ui.graphics.Color +import kotlin.math.abs + +data class HSV( + /** + * 0.0 .. 360.0 + */ + val hue: Float, + /** + * 0.0 .. 1.0 + */ + val saturation: Float, + /** + * 0.0 . 1.0¬ + */ + val value: Float +) { + companion object { + const val HUE_MAX_VALUE = 360f + } +} + +/** + * Convert to HSV color space + * https://www.rapidtables.com/convert/color/rgb-to-hsv.html + */ +fun Color.toHsv(): HSV { + val max = maxOf(red, green, blue) + val min = minOf(red, green, blue) + val delta = max - min + val h = when { + delta == 0f -> 0f + max == red -> 60 * ((green - blue) / delta).mod(6f) + max == green -> 60 * ((blue - red) / delta + 2) + max == blue -> 60 * ((red - green) / delta + 4) + else -> 0f + } + val s = when { + max == 0f -> 0f + else -> delta / max + } + val v = max + return HSV( + hue = h, + saturation = s, + value = v + ) +} + +/** + * Convert to RGB color space + * https://www.rapidtables.com/convert/color/hsv-to-rgb.html + */ +fun HSV.toRgb(): Color { + val c = value * saturation + val x = minOf(c * (1 - abs((hue / 60).mod(2f) - 1)), 1f) + val m = value - c + val tempColor = when { + hue >= 0 && hue < 60 -> Color(c, x, 0f) + hue >= 60 && hue < 120 -> Color(x, c, 0f) + hue >= 120 && hue < 180 -> Color(0f, c, x) + hue >= 180 && hue < 240 -> Color(0f, x, c) + hue >= 240 && hue < 300 -> Color(x, 0f, c) + else -> Color(c, 0f, x) + } + return Color(minOf(m + tempColor.red, 1f), minOf(m + tempColor.green, 1f), minOf(m + tempColor.blue, 1f)) +} + diff --git a/examples/intellij-plugin-with-experimental-shared-base/src/main/resources/META-INF/plugin.xml b/examples/intellij-plugin-with-experimental-shared-base/src/main/resources/META-INF/plugin.xml index 55a1038563..9aaa65acf2 100644 --- a/examples/intellij-plugin-with-experimental-shared-base/src/main/resources/META-INF/plugin.xml +++ b/examples/intellij-plugin-with-experimental-shared-base/src/main/resources/META-INF/plugin.xml @@ -11,6 +11,7 @@ A plugin demonstrates Jetpack compose capabilities on IntelliJ Platform on how to target different products --> com.intellij.modules.platform org.jetbrains.compose.intellij.platform + org.jetbrains.kotlin @@ -19,4 +20,8 @@ A plugin demonstrates Jetpack compose capabilities on IntelliJ Platform + + + + \ No newline at end of file diff --git a/examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/ColorPickerUITest.kt b/examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/ColorPickerUITest.kt new file mode 100644 index 0000000000..68ca6d2387 --- /dev/null +++ b/examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/ColorPickerUITest.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package com.jetbrains.compose.color + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application + +fun main() = application { + val windowState = remember { WindowState(width = 400.dp, height = 400.dp) } + Window( + onCloseRequest = ::exitApplication, + title = "ColorPicker", + state = windowState + ) { + ColorPicker(mutableStateOf(Color(0xffaabbcc))) + } +} diff --git a/examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/HSVTest.kt b/examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/HSVTest.kt new file mode 100644 index 0000000000..b1c491b1fe --- /dev/null +++ b/examples/intellij-plugin-with-experimental-shared-base/src/test/kotlin/com/jetbrains/compose/color/HSVTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package com.jetbrains.compose.color + +import androidx.compose.ui.graphics.Color +import org.junit.Test +import kotlin.test.assertEquals + +class HSVTest { + + @Test + fun testGreenToHsv() { + val greenRgb = Color(0xff00ff00) + val result = greenRgb.toHsv() + assertEquals(HSV(120f, 1f, 1f), result) + assertEquals(greenRgb, result.toRgb()) + } + +}