Skip to content

Commit

Permalink
Migrated ActionSystem to Compose Traversal API & UiDataProvider API (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jakub-senohrabek-jb authored Aug 20, 2024
1 parent 576c345 commit 9e136d9
Show file tree
Hide file tree
Showing 24 changed files with 395 additions and 201 deletions.
25 changes: 25 additions & 0 deletions foundation/api/foundation.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.jetbrains.jewel.foundation.actionSystem

public interface DataProviderContext {
public fun <TValue : Any> set(
key: String,
value: TValue?,
)

public fun <TValue : Any> lazy(
key: String,
initializer: () -> TValue?,
)
}
Original file line number Diff line number Diff line change
@@ -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<DataProviderNode>() {
override fun create(): DataProviderNode = DataProviderNode(dataProvider)

override fun update(node: DataProviderNode) {
node.dataProvider = dataProvider
node.updateParent()
}

override fun equals(other: Any?): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
Empty file.
37 changes: 37 additions & 0 deletions ide-laf-bridge-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -22,6 +24,7 @@ class ProvideDataTest {
@Test
fun `one component`() {
runBlocking {
val sink = TestDataSink()
val rootDataProviderModifier = RootDataProviderModifier()
var focusManager: FocusManager? = null
rule.setContent {
Expand All @@ -30,10 +33,7 @@ class ProvideDataTest {
modifier =
rootDataProviderModifier.testTag("provider")
.provideData {
when (it) {
"data" -> "ok"
else -> null
}
set("data", "ok")
}
.focusable(),
)
Expand All @@ -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"))
}
}

Expand All @@ -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(),
) {
Expand All @@ -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(),
)
Expand All @@ -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"))
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Any>()

fun clear() {
allData.clear()
}

inline fun <reified T> 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 <T : Any> setNull(key: DataKey<T>) {
allData.remove(key.name)
}

override fun <T : Any> set(
key: DataKey<T>,
data: T?,
) {
if (data != null) {
allData[key.name] = data
}
}

override fun <T : Any> lazy(
key: DataKey<T>,
data: () -> T?,
) {
set(key, data())
}
}
19 changes: 16 additions & 3 deletions ide-laf-bridge/api/ide-laf-bridge.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()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 <init> ()V
public fun uiDataSnapshot (Lcom/intellij/openapi/actionSystem/DataSink;)V
}

public final class org/jetbrains/jewel/bridge/icon/IntelliJIconKeyKt {
Expand Down
2 changes: 1 addition & 1 deletion ide-laf-bridge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9e136d9

Please sign in to comment.