diff --git a/foundation/api/foundation.api b/foundation/api/foundation.api index 4c845d6f6..058ef4e7f 100644 --- a/foundation/api/foundation.api +++ b/foundation/api/foundation.api @@ -146,6 +146,31 @@ public final class org/jetbrains/jewel/foundation/TextColors { public final class org/jetbrains/jewel/foundation/TextColors$Companion { } +public abstract interface class org/jetbrains/jewel/foundation/actionSystem/DataProviderContext { + public abstract fun lazy (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public abstract fun set (Ljava/lang/String;Ljava/lang/Object;)V +} + +public final class org/jetbrains/jewel/foundation/actionSystem/DataProviderNode : androidx/compose/ui/Modifier$Node, androidx/compose/ui/focus/FocusEventModifierNode, androidx/compose/ui/node/TraversableNode { + public static final field $stable I + public static final field TraverseKey Lorg/jetbrains/jewel/foundation/actionSystem/DataProviderNode$TraverseKey; + public fun (Lkotlin/jvm/functions/Function1;)V + public final fun getDataProvider ()Lkotlin/jvm/functions/Function1; + public final fun getHasFocus ()Z + public synthetic fun getTraverseKey ()Ljava/lang/Object; + public fun getTraverseKey ()Lorg/jetbrains/jewel/foundation/actionSystem/DataProviderNode$TraverseKey; + public fun onFocusEvent (Landroidx/compose/ui/focus/FocusState;)V + public final fun setDataProvider (Lkotlin/jvm/functions/Function1;)V + public final fun setHasFocus (Z)V +} + +public final class org/jetbrains/jewel/foundation/actionSystem/DataProviderNode$TraverseKey { +} + +public final class org/jetbrains/jewel/foundation/actionSystem/ProvideDataKt { + public static final fun provideData (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier; +} + public class org/jetbrains/jewel/foundation/lazy/DefaultMacOsSelectableColumnKeybindings : org/jetbrains/jewel/foundation/lazy/DefaultSelectableColumnKeybindings { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/foundation/lazy/DefaultMacOsSelectableColumnKeybindings$Companion; diff --git a/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderContext.kt b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderContext.kt new file mode 100644 index 000000000..eee78011e --- /dev/null +++ b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderContext.kt @@ -0,0 +1,13 @@ +package org.jetbrains.jewel.foundation.actionSystem + +public interface DataProviderContext { + public fun set( + key: String, + value: TValue?, + ) + + public fun lazy( + key: String, + initializer: () -> TValue?, + ) +} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderElement.kt b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderElement.kt similarity index 83% rename from ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderElement.kt rename to foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderElement.kt index cf847681d..ce1794dc6 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderElement.kt +++ b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderElement.kt @@ -1,15 +1,14 @@ -package org.jetbrains.jewel.bridge.actionSystem +package org.jetbrains.jewel.foundation.actionSystem import androidx.compose.ui.node.ModifierNodeElement internal class DataProviderElement( - val dataProvider: (dataId: String) -> Any?, + val dataProvider: DataProviderContext.() -> Unit, ) : ModifierNodeElement() { override fun create(): DataProviderNode = DataProviderNode(dataProvider) override fun update(node: DataProviderNode) { node.dataProvider = dataProvider - node.updateParent() } override fun equals(other: Any?): Boolean { diff --git a/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderNode.kt b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderNode.kt new file mode 100644 index 000000000..159bab71b --- /dev/null +++ b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/DataProviderNode.kt @@ -0,0 +1,20 @@ +package org.jetbrains.jewel.foundation.actionSystem + +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusEventModifierNode +import androidx.compose.ui.focus.FocusState +import androidx.compose.ui.node.TraversableNode + +public class DataProviderNode( + public var dataProvider: DataProviderContext.() -> Unit, +) : Modifier.Node(), FocusEventModifierNode, TraversableNode { + public var hasFocus: Boolean = false + + override fun onFocusEvent(focusState: FocusState) { + hasFocus = focusState.hasFocus + } + + override val traverseKey: TraverseKey = TraverseKey + + public companion object TraverseKey +} diff --git a/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/ProvideData.kt b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/ProvideData.kt new file mode 100644 index 000000000..9c9b19604 --- /dev/null +++ b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/actionSystem/ProvideData.kt @@ -0,0 +1,21 @@ +package org.jetbrains.jewel.foundation.actionSystem + +import androidx.compose.foundation.focusable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusEventModifierNode + +/** + * Configure component to provide data for IntelliJ Actions system. + * + * Use this modifier to provide context related data that can be used by + * IntelliJ Actions functionality such as Search Everywhere, Action Popups + * etc. + * + * Important note: modifiers order is important, so be careful with order + * of [focusable] and [provideData] (see [FocusEventModifierNode]). + * + * This can be traversed from Modifier.Node() using Compose traversal API using DataProviderNode as a TraverseKey + * + */ +@Suppress("unused") +public fun Modifier.provideData(dataProvider: DataProviderContext.() -> Unit): Modifier = this then DataProviderElement(dataProvider) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6c623094..a1c994741 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,8 @@ composeDesktop = { id = "org.jetbrains.compose", version.ref = "composeDesktop" detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } ideaPlugin = { id = "org.jetbrains.intellij.platform", version.ref = "ideaPlugin" } -ideaPluginModule = { id = "org.jetbrains.intellij.platform.base", version.ref = "ideaPlugin" } +ideaPluginBase = { id = "org.jetbrains.intellij.platform.base", version.ref = "ideaPlugin" } +ideaPluginModule = { id = "org.jetbrains.intellij.platform.module", version.ref = "ideaPlugin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinx-binaryCompatValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlinxBinaryCompat" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/ide-laf-bridge-tests/api/ide-laf-bridge-tests.api b/ide-laf-bridge-tests/api/ide-laf-bridge-tests.api new file mode 100644 index 000000000..e69de29bb diff --git a/ide-laf-bridge-tests/build.gradle.kts b/ide-laf-bridge-tests/build.gradle.kts new file mode 100644 index 000000000..72c636c0b --- /dev/null +++ b/ide-laf-bridge-tests/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + jewel + `jewel-publish` + `jewel-check-public-api` + `ide-version-checker` + alias(libs.plugins.composeDesktop) + alias(libs.plugins.ideaPluginModule) +} + +// Because we need to define IJP dependencies, the dependencyResolutionManagement +// from settings.gradle.kts is overridden and we have to redeclare everything here. +repositories { + google() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + mavenCentral() + + intellijPlatform { + defaultRepositories() + } +} + +dependencies { + testImplementation(projects.ui) { + exclude(group = "org.jetbrains.kotlinx") + } + testImplementation(projects.ideLafBridge) + + intellijPlatform { + intellijIdeaCommunity(libs.versions.idea) + instrumentationTools() + } + + testImplementation(compose.desktop.uiTestJUnit4) + testImplementation(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + } +} diff --git a/ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideDataTest.kt b/ide-laf-bridge-tests/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideDataTest.kt similarity index 70% rename from ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideDataTest.kt rename to ide-laf-bridge-tests/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideDataTest.kt index 8c25f55ef..4db9f6739 100644 --- a/ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideDataTest.kt +++ b/ide-laf-bridge-tests/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideDataTest.kt @@ -11,7 +11,9 @@ import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import kotlinx.coroutines.runBlocking +import org.jetbrains.jewel.foundation.actionSystem.provideData import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Rule import org.junit.Test @@ -22,6 +24,7 @@ class ProvideDataTest { @Test fun `one component`() { runBlocking { + val sink = TestDataSink() val rootDataProviderModifier = RootDataProviderModifier() var focusManager: FocusManager? = null rule.setContent { @@ -30,10 +33,7 @@ class ProvideDataTest { modifier = rootDataProviderModifier.testTag("provider") .provideData { - when (it) { - "data" -> "ok" - else -> null - } + set("data", "ok") } .focusable(), ) @@ -43,9 +43,10 @@ class ProvideDataTest { rule.awaitIdle() rule.onNodeWithTag("provider").assertIsFocused() + rootDataProviderModifier.uiDataSnapshot(sink) - assertEquals("ok", rootDataProviderModifier.dataProvider("data")) - assertEquals(null, rootDataProviderModifier.dataProvider("another_data")) + assertEquals("ok", sink.get("data")) + assertNull(sink.get("another_data")) } } @@ -60,10 +61,8 @@ class ProvideDataTest { modifier = rootDataProviderModifier.testTag("root_provider") .provideData { - when (it) { - "isRoot" -> "yes" - else -> null - } + set("is_root", "yes") + set("data", "notOk") } .focusable(), ) { @@ -72,10 +71,8 @@ class ProvideDataTest { modifier = Modifier.testTag("data_provider_item") .provideData { - when (it) { - "data" -> "ok" - else -> null - } + set("data", "ok") + set("one", "1") } .focusable(), ) @@ -87,26 +84,34 @@ class ProvideDataTest { focusManager!!.moveFocus(FocusDirection.Next) rule.awaitIdle() + val sink = TestDataSink() rule.onNodeWithTag("root_provider").assertIsFocused() - assertEquals("yes", rootDataProviderModifier.dataProvider("isRoot")) - assertEquals(null, rootDataProviderModifier.dataProvider("data")) + rootDataProviderModifier.uiDataSnapshot(sink) + assertEquals("yes", sink.get("is_root")) + assertEquals("notOk", sink.get("data")) + assertNull(sink.get("one")) focusManager!!.moveFocus(FocusDirection.Next) rule.awaitIdle() + sink.clear() rule.onNodeWithTag("non_data_provider").assertIsFocused() + rootDataProviderModifier.uiDataSnapshot(sink) // non_data_provider still should provide isRoot == true because it should be taken from root - // but shouldn't provide "data" yet - assertEquals("yes", rootDataProviderModifier.dataProvider("isRoot")) - assertEquals(null, rootDataProviderModifier.dataProvider("data")) + // but shouldn't provide "one" yet + assertEquals("yes", sink.get("is_root")) + assertEquals("notOk", sink.get("data")) + assertNull(sink.get("one")) focusManager!!.moveFocus(FocusDirection.Next) rule.awaitIdle() + sink.clear() rule.onNodeWithTag("data_provider_item").assertIsFocused() - - assertEquals("yes", rootDataProviderModifier.dataProvider("isRoot")) - assertEquals("ok", rootDataProviderModifier.dataProvider("data")) + rootDataProviderModifier.uiDataSnapshot(sink) + assertEquals("yes", sink.get("is_root")) + assertEquals("ok", sink.get("data")) + assertEquals("1", sink.get("one")) } } } diff --git a/ide-laf-bridge-tests/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/TestDataSink.kt b/ide-laf-bridge-tests/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/TestDataSink.kt new file mode 100644 index 000000000..8ddbb77fc --- /dev/null +++ b/ide-laf-bridge-tests/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/TestDataSink.kt @@ -0,0 +1,54 @@ +package org.jetbrains.jewel.bridge.actionSystem + +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.actionSystem.DataSink +import com.intellij.openapi.actionSystem.DataSnapshotProvider +import com.intellij.openapi.actionSystem.UiDataProvider + +@Suppress("OverrideOnly", "NonExtendableApiUsage") +internal class TestDataSink : DataSink { + val allData = mutableMapOf() + + fun clear() { + allData.clear() + } + + inline fun get(key: String): T? { + val data = allData[key] as T? + if (data is T) return data + return null + } + + override fun dataSnapshot(provider: DataSnapshotProvider) { + provider.dataSnapshot(this) + } + + override fun uiDataSnapshot(provider: DataProvider) { + // NOT needed in current tests + } + + override fun uiDataSnapshot(provider: UiDataProvider) { + provider.uiDataSnapshot(this) + } + + override fun setNull(key: DataKey) { + allData.remove(key.name) + } + + override fun set( + key: DataKey, + data: T?, + ) { + if (data != null) { + allData[key.name] = data + } + } + + override fun lazy( + key: DataKey, + data: () -> T?, + ) { + set(key, data()) + } +} diff --git a/ide-laf-bridge/api/ide-laf-bridge.api b/ide-laf-bridge/api/ide-laf-bridge.api index 777f0d70f..b2bb465e5 100644 --- a/ide-laf-bridge/api/ide-laf-bridge.api +++ b/ide-laf-bridge/api/ide-laf-bridge.api @@ -84,9 +84,22 @@ public final class org/jetbrains/jewel/bridge/TypographyKt { public static final fun small (Lorg/jetbrains/jewel/ui/component/Typography;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/text/TextStyle; } -public final class org/jetbrains/jewel/bridge/actionSystem/ProvideDataKt { - public static final fun ComponentDataProviderBridge (Ljavax/swing/JComponent;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V - public static final fun provideData (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier; +public final class org/jetbrains/jewel/bridge/actionSystem/RootDataProviderModifier : androidx/compose/ui/node/ModifierNodeElement, com/intellij/openapi/actionSystem/UiDataProvider { + public static final field $stable I + public fun ()V + public synthetic fun create ()Landroidx/compose/ui/Modifier$Node; + public fun create ()Lorg/jetbrains/jewel/bridge/actionSystem/RootDataProviderNode; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun uiDataSnapshot (Lcom/intellij/openapi/actionSystem/DataSink;)V + public synthetic fun update (Landroidx/compose/ui/Modifier$Node;)V + public fun update (Lorg/jetbrains/jewel/bridge/actionSystem/RootDataProviderNode;)V +} + +public final class org/jetbrains/jewel/bridge/actionSystem/RootDataProviderNode : androidx/compose/ui/Modifier$Node, com/intellij/openapi/actionSystem/UiDataProvider { + public static final field $stable I + public fun ()V + public fun uiDataSnapshot (Lcom/intellij/openapi/actionSystem/DataSink;)V } public final class org/jetbrains/jewel/bridge/icon/IntelliJIconKeyKt { diff --git a/ide-laf-bridge/build.gradle.kts b/ide-laf-bridge/build.gradle.kts index c7a63b4c6..cd2b3b417 100644 --- a/ide-laf-bridge/build.gradle.kts +++ b/ide-laf-bridge/build.gradle.kts @@ -4,7 +4,7 @@ plugins { `jewel-check-public-api` `ide-version-checker` alias(libs.plugins.composeDesktop) - alias(libs.plugins.ideaPluginModule) + alias(libs.plugins.ideaPluginBase) } // Because we need to define IJP dependencies, the dependencyResolutionManagement diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanel.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanel.kt index 7963a226d..b56426077 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanel.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanel.kt @@ -11,19 +11,23 @@ import androidx.compose.ui.awt.ComposePanel import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.toSize +import com.intellij.openapi.actionSystem.DataSink +import com.intellij.openapi.actionSystem.UiDataProvider import org.jetbrains.jewel.bridge.actionSystem.ComponentDataProviderBridge import org.jetbrains.jewel.bridge.theme.SwingBridgeTheme import org.jetbrains.jewel.foundation.ExperimentalJewelApi import org.jetbrains.jewel.foundation.InternalJewelApi +import java.awt.BorderLayout import javax.swing.JComponent +import javax.swing.JPanel @Suppress("ktlint:standard:function-naming", "FunctionName") // Swing to Compose bridge API public fun JewelComposePanel(content: @Composable () -> Unit): JComponent = - ComposePanel().apply { + createJewelComposePanel { jewelPanel -> setContent { SwingBridgeTheme { - CompositionLocalProvider(LocalComponent provides this@apply) { - ComponentDataProviderBridge(this@apply, content = content) + CompositionLocalProvider(LocalComponent provides this@createJewelComposePanel) { + ComponentDataProviderBridge(jewelPanel, content = content) } } } @@ -32,18 +36,35 @@ public fun JewelComposePanel(content: @Composable () -> Unit): JComponent = @InternalJewelApi @Suppress("ktlint:standard:function-naming", "FunctionName") // Swing to Compose bridge API public fun JewelToolWindowComposePanel(content: @Composable () -> Unit): JComponent = - ComposePanel().apply { + createJewelComposePanel { jewelPanel -> setContent { Compose17IJSizeBugWorkaround { SwingBridgeTheme { - CompositionLocalProvider(LocalComponent provides this@apply) { - ComponentDataProviderBridge(this@apply, content = content) + CompositionLocalProvider(LocalComponent provides this@createJewelComposePanel) { + ComponentDataProviderBridge(jewelPanel, content = content) } } } } } +private fun createJewelComposePanel(config: ComposePanel.(JewelComposePanel) -> Unit): JewelComposePanel { + val jewelPanel = JewelComposePanel() + jewelPanel.layout = BorderLayout() + val composePanel = ComposePanel() + jewelPanel.add(composePanel, BorderLayout.CENTER) + composePanel.config(jewelPanel) + return jewelPanel +} + +internal class JewelComposePanel : JPanel(), UiDataProvider { + internal var targetProvider: UiDataProvider? = null + + override fun uiDataSnapshot(sink: DataSink) { + targetProvider?.uiDataSnapshot(sink) + } +} + @ExperimentalJewelApi public val LocalComponent: ProvidableCompositionLocal = staticCompositionLocalOf { diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/ComponentDataProviderBridge.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/ComponentDataProviderBridge.kt new file mode 100644 index 000000000..0c448ef28 --- /dev/null +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/ComponentDataProviderBridge.kt @@ -0,0 +1,32 @@ +package org.jetbrains.jewel.bridge.actionSystem + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import org.jetbrains.jewel.bridge.JewelComposePanel + +@Suppress("FunctionName") +@Composable +internal fun ComponentDataProviderBridge( + component: JewelComposePanel, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val rootDataProviderModifier = remember { RootDataProviderModifier() } + + Box(modifier = Modifier.then(rootDataProviderModifier).then(modifier)) { + content() + } + + DisposableEffect(component) { + component.targetProvider = rootDataProviderModifier + + onDispose { + if (component.targetProvider == rootDataProviderModifier) { + component.targetProvider = null + } + } + } +} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderDataSinkContext.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderDataSinkContext.kt new file mode 100644 index 000000000..5c2a69ff1 --- /dev/null +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderDataSinkContext.kt @@ -0,0 +1,28 @@ +package org.jetbrains.jewel.bridge.actionSystem + +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.actionSystem.DataSink +import org.jetbrains.jewel.foundation.actionSystem.DataProviderContext + +internal class DataProviderDataSinkContext( + private val dataSink: DataSink, +) : DataProviderContext { + override fun set( + key: String, + value: TValue?, + ) { + val ijKey = DataKey.create(key) + if (value == null) { + dataSink.setNull(ijKey) + } + dataSink[ijKey] = value + } + + override fun lazy( + key: String, + initializer: () -> TValue?, + ) { + val ijKey = DataKey.create(key) + dataSink.lazy(ijKey, initializer) + } +} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderNode.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderNode.kt deleted file mode 100644 index e07525334..000000000 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderNode.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.jetbrains.jewel.bridge.actionSystem - -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusEventModifierNode -import androidx.compose.ui.focus.FocusState -import androidx.compose.ui.modifier.ModifierLocalMap -import androidx.compose.ui.modifier.ModifierLocalModifierNode -import androidx.compose.ui.modifier.modifierLocalMapOf -import androidx.compose.ui.modifier.modifierLocalOf - -/** - * Holder for parent node of current [DataProviderNode]. So, each - * [DataProviderNode] provides itself and read parent node. It allows - * building tree of [DataProviderNode] and traverse it later on. - * - * @see ModifierLocalModifierNode - */ -private val LocalDataProviderNode = modifierLocalOf { null } - -internal class DataProviderNode( - var dataProvider: (dataId: String) -> Any?, -) : Modifier.Node(), ModifierLocalModifierNode, FocusEventModifierNode { - // TODO: should we use state here and in parent with children for thread safety? Will it trigger - // recompositions? - var hasFocus = false - - var parent: DataProviderNode? = null - - private val _children = mutableSetOf() - val children: Set = _children - - override val providedValues: ModifierLocalMap = modifierLocalMapOf(LocalDataProviderNode to this) - - override fun onAttach() { - val oldParent = parent - parent = LocalDataProviderNode.current - if (parent !== oldParent) { - oldParent?._children?.remove(this) - parent?._children?.add(this) - } - } - - override fun onDetach() { - parent?._children?.remove(this) - parent = null - } - - override fun onFocusEvent(focusState: FocusState) { - hasFocus = focusState.hasFocus - } - - public fun updateParent() { - parent = LocalDataProviderNode.current - } -} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideData.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideData.kt deleted file mode 100644 index 7dd18f22a..000000000 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideData.kt +++ /dev/null @@ -1,108 +0,0 @@ -package org.jetbrains.jewel.bridge.actionSystem - -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusEventModifierNode -import androidx.compose.ui.node.ModifierNodeElement -import com.intellij.ide.DataManager -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.DataProvider -import org.jetbrains.annotations.VisibleForTesting -import javax.swing.JComponent - -// TODO: choose better naming? - -/** - * Layout composable that create the bridge between [Modifier.provideData] - * used inside [content] and [component]. So, IntelliJ's [DataManager] can - * use [component] as [DataProvider]. - * - * When IntelliJ requests [getData] from [component] Compose will traverse - * [DataProviderNode] hierarchy and collect it according [DataProvider] - * rules (see javadoc). - */ -@Suppress("unused", "FunctionName") -@Composable -public fun ComponentDataProviderBridge( - component: JComponent, - modifier: Modifier = Modifier, - content: @Composable () -> Unit, -) { - val rootDataProviderModifier = remember { RootDataProviderModifier() } - - Box(modifier = Modifier.then(rootDataProviderModifier).then(modifier)) { - content() - } - - DisposableEffect(component) { - DataManager.registerDataProvider(component, rootDataProviderModifier.dataProvider) - - onDispose { DataManager.removeDataProvider(component) } - } -} - -/** - * Configure component to provide data for IntelliJ Actions system. - * - * Use this modifier to provide context related data that can be used by - * IntelliJ Actions functionality such as Search Everywhere, Action Popups - * etc. - * - * Important note: modifiers order is important, so be careful with order - * of [focusable] and [provideData] (see [FocusEventModifierNode]). - * - * @see DataProvider - * @see DataContext - * @see ComponentDataProviderBridge - */ -@Suppress("unused") -public fun Modifier.provideData(dataProvider: (dataId: String) -> Any?): Modifier = this then DataProviderElement(dataProvider) - -@VisibleForTesting -internal class RootDataProviderModifier : ModifierNodeElement() { - private val rootNode = DataProviderNode { null } - - val dataProvider: (String) -> Any? = { rootNode.getData(it) } - - override fun create() = rootNode - - override fun update(node: DataProviderNode) { - // do nothing - } - - override fun hashCode(): Int = rootNode.hashCode() - - override fun equals(other: Any?) = other === this -} - -private fun DataProviderNode.getData(dataId: String): Any? { - val focusedNode = this.traverseDownToFocused() ?: return null - return focusedNode.collectData(dataId) -} - -private fun DataProviderNode.collectData(dataId: String): Any? { - var currentNode: DataProviderNode? = this - while (currentNode != null) { - val data = currentNode.dataProvider(dataId) - if (data != null) { - return data - } - currentNode = currentNode.parent - } - - return null -} - -private fun DataProviderNode.traverseDownToFocused(): DataProviderNode? { - for (child in children) { - if (child.hasFocus) { - return child.traverseDownToFocused() - } - } - - return this.takeIf { hasFocus } -} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/RootDataProviderModifier.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/RootDataProviderModifier.kt new file mode 100644 index 000000000..88c1d216a --- /dev/null +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/RootDataProviderModifier.kt @@ -0,0 +1,25 @@ +package org.jetbrains.jewel.bridge.actionSystem + +import androidx.compose.ui.node.ModifierNodeElement +import com.intellij.openapi.actionSystem.DataSink +import com.intellij.openapi.actionSystem.UiDataProvider +import org.jetbrains.annotations.VisibleForTesting + +@VisibleForTesting +public class RootDataProviderModifier : ModifierNodeElement(), UiDataProvider { + private val rootNode = RootDataProviderNode() + + override fun uiDataSnapshot(sink: DataSink) { + rootNode.uiDataSnapshot(sink) + } + + override fun create(): RootDataProviderNode = rootNode + + override fun update(node: RootDataProviderNode) { + // do nothing + } + + override fun hashCode(): Int = rootNode.hashCode() + + override fun equals(other: Any?): Boolean = other === this +} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/RootDataProviderNode.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/RootDataProviderNode.kt new file mode 100644 index 000000000..e565ec5cc --- /dev/null +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/RootDataProviderNode.kt @@ -0,0 +1,25 @@ +package org.jetbrains.jewel.bridge.actionSystem + +import androidx.compose.ui.Modifier +import androidx.compose.ui.node.TraversableNode +import androidx.compose.ui.node.traverseDescendants +import com.intellij.openapi.actionSystem.DataSink +import com.intellij.openapi.actionSystem.UiDataProvider +import org.jetbrains.jewel.foundation.actionSystem.DataProviderNode + +public class RootDataProviderNode : Modifier.Node(), UiDataProvider { + override fun uiDataSnapshot(sink: DataSink) { + val context = DataProviderDataSinkContext(sink) + + traverseDescendants(DataProviderNode) { dp -> + if (dp is DataProviderNode) { + if (!dp.hasFocus) { + return@traverseDescendants TraversableNode.Companion.TraverseDescendantsAction.SkipSubtreeAndContinueTraversal + } else { + dp.dataProvider(context) + } + } + TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal + } + } +} diff --git a/markdown/ide-laf-bridge-styling/build.gradle.kts b/markdown/ide-laf-bridge-styling/build.gradle.kts index 0dea0e440..f5eefe1ec 100644 --- a/markdown/ide-laf-bridge-styling/build.gradle.kts +++ b/markdown/ide-laf-bridge-styling/build.gradle.kts @@ -3,7 +3,7 @@ plugins { `jewel-publish` `jewel-check-public-api` alias(libs.plugins.composeDesktop) - alias(libs.plugins.ideaPluginModule) + alias(libs.plugins.ideaPluginBase) } // Because we need to define IJP dependencies, the dependencyResolutionManagement diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ActionSystemTestAction.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ActionSystemTestAction.kt new file mode 100644 index 000000000..988317498 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ActionSystemTestAction.kt @@ -0,0 +1,21 @@ +package org.jetbrains.jewel.samples.ideplugin + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.ui.Messages.showMessageDialog + +class ActionSystemTestAction : AnAction() { + private val logger = thisLogger() + + override fun actionPerformed(anActionEvent: AnActionEvent) { + logger.debug(anActionEvent.getData(COMPONENT_DATA_KEY)) + + showMessageDialog(anActionEvent.getData(COMPONENT_DATA_KEY), "Action System Test", null) + } + + companion object { + val COMPONENT_DATA_KEY = DataKey.create("COMPONENT") + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt index 84bd518ee..a330cdcae 100644 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt @@ -35,6 +35,7 @@ import com.intellij.util.ui.JBUI import icons.IdeSampleIconKeys import org.jetbrains.jewel.bridge.LocalComponent import org.jetbrains.jewel.bridge.toComposeColor +import org.jetbrains.jewel.foundation.actionSystem.provideData import org.jetbrains.jewel.foundation.lazy.tree.buildTree import org.jetbrains.jewel.foundation.modifier.onActivated import org.jetbrains.jewel.foundation.modifier.trackActivation @@ -147,7 +148,15 @@ private fun RowScope.ColumnOne() { val state = rememberTextFieldState("") TextField( state = state, - modifier = Modifier.width(200.dp), + modifier = + Modifier + .width(200.dp) + .provideData { + set(ActionSystemTestAction.COMPONENT_DATA_KEY.name, "TextField") + lazy(ActionSystemTestAction.COMPONENT_DATA_KEY.name) { + Math.random().toString() + } + }, placeholder = { Text("Write something...") }, ) @@ -160,6 +169,10 @@ private fun RowScope.ColumnOne() { checked = checked, onCheckedChange = { checked = it }, outline = outline, + modifier = + Modifier.provideData { + set(ActionSystemTestAction.COMPONENT_DATA_KEY.name, "Checkbox") + }, ) { Text("Hello, I am a themed checkbox") } diff --git a/samples/ide-plugin/src/main/resources/META-INF/plugin.xml b/samples/ide-plugin/src/main/resources/META-INF/plugin.xml index d10617c0c..9fb715c3d 100644 --- a/samples/ide-plugin/src/main/resources/META-INF/plugin.xml +++ b/samples/ide-plugin/src/main/resources/META-INF/plugin.xml @@ -26,5 +26,8 @@ See the Jewel repository for mo + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 533c46b8c..6197d6e72 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,7 @@ include( ":decorated-window", ":foundation", ":ide-laf-bridge", + ":ide-laf-bridge-tests", ":int-ui:int-ui-decorated-window", ":int-ui:int-ui-standalone", ":markdown:core",