-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
...laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderElement.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package org.jetbrains.jewel.bridge.actionSystem | ||
|
||
import androidx.compose.ui.node.ModifierNodeElement | ||
|
||
internal class DataProviderElement(val dataProvider: (dataId: String) -> Any?) : ModifierNodeElement<DataProviderNode>() { | ||
|
||
override fun create(): DataProviderNode { | ||
return DataProviderNode(dataProvider) | ||
} | ||
|
||
override fun update(node: DataProviderNode) { | ||
node.dataProvider = dataProvider | ||
node.updateParent() | ||
} | ||
|
||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (javaClass != other?.javaClass) return false | ||
|
||
other as DataProviderElement | ||
|
||
return dataProvider == other.dataProvider | ||
} | ||
|
||
override fun hashCode(): Int { | ||
return dataProvider.hashCode() | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/DataProviderNode.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
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<DataProviderNode?> { | ||
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<DataProviderNode>() | ||
val children: Set<DataProviderNode> = _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 | ||
} | ||
|
||
fun updateParent() { | ||
parent = LocalDataProviderNode.current | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideData.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
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 | ||
|
||
/** | ||
* 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). | ||
*/ | ||
// TODO: choose better naming? | ||
@Suppress("unused", "FunctionName") | ||
@Composable | ||
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 [Modifier.focusable] and [provideData] | ||
* (see [FocusEventModifierNode]). | ||
* | ||
* @see DataProvider | ||
* @see DataContext | ||
* @see ComponentDataProviderBridge | ||
*/ | ||
@Suppress("unused") | ||
fun Modifier.provideData(dataProvider: (dataId: String) -> Any?) = this.then( | ||
DataProviderElement(dataProvider) | ||
) | ||
|
||
@VisibleForTesting | ||
internal class RootDataProviderModifier : ModifierNodeElement<DataProviderNode>() { | ||
|
||
private val rootNode = DataProviderNode { null } | ||
|
||
val dataProvider: (String) -> Any? = { | ||
rootNode.getData(it) | ||
} | ||
|
||
override fun create() = rootNode | ||
|
||
override fun update(node: DataProviderNode) {} | ||
|
||
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 } | ||
} |
112 changes: 112 additions & 0 deletions
112
ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/actionSystem/ProvideDataKtTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package org.jetbrains.jewel.bridge.actionSystem | ||
|
||
import androidx.compose.foundation.focusable | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.focus.FocusDirection | ||
import androidx.compose.ui.focus.FocusManager | ||
import androidx.compose.ui.platform.LocalFocusManager | ||
import androidx.compose.ui.platform.testTag | ||
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.junit.Assert.assertEquals | ||
import org.junit.Rule | ||
import org.junit.Test | ||
|
||
class ProvideDataTest { | ||
|
||
@JvmField | ||
@Rule | ||
val rule = createComposeRule() | ||
|
||
@Test | ||
fun `one component`(): Unit = runBlocking { | ||
val rootDataProviderModifier = RootDataProviderModifier() | ||
var focusManager: FocusManager? = null | ||
rule.setContent { | ||
focusManager = LocalFocusManager.current | ||
Box( | ||
modifier = Modifier | ||
.then(rootDataProviderModifier) | ||
.testTag("provider") | ||
.provideData { | ||
when (it) { | ||
"data" -> "ok" | ||
else -> null | ||
} | ||
} | ||
.focusable() | ||
) | ||
} | ||
rule.awaitIdle() | ||
focusManager!!.moveFocus(FocusDirection.Next) | ||
rule.awaitIdle() | ||
|
||
rule.onNodeWithTag("provider").assertIsFocused() | ||
|
||
assertEquals("ok", rootDataProviderModifier.dataProvider("data")) | ||
assertEquals(null, rootDataProviderModifier.dataProvider("another_data")) | ||
} | ||
|
||
@Test | ||
fun `component hierarchy`(): Unit = runBlocking { | ||
val rootDataProviderModifier = RootDataProviderModifier() | ||
var focusManager: FocusManager? = null | ||
rule.setContent { | ||
focusManager = LocalFocusManager.current | ||
Box( | ||
modifier = Modifier | ||
.then(rootDataProviderModifier) | ||
.testTag("root_provider") | ||
.provideData { | ||
when (it) { | ||
"isRoot" -> "yes" | ||
else -> null | ||
} | ||
} | ||
.focusable() | ||
) { | ||
Box(modifier = Modifier.testTag("non_data_provider").focusable()) { | ||
Box(modifier = Modifier | ||
.testTag("data_provider_item") | ||
.provideData { | ||
when (it) { | ||
"data" -> "ok" | ||
else -> null | ||
} | ||
} | ||
.focusable() | ||
) | ||
} | ||
} | ||
} | ||
|
||
rule.awaitIdle() | ||
focusManager!!.moveFocus(FocusDirection.Next) | ||
rule.awaitIdle() | ||
|
||
rule.onNodeWithTag("root_provider").assertIsFocused() | ||
assertEquals("yes", rootDataProviderModifier.dataProvider("isRoot")) | ||
assertEquals(null, rootDataProviderModifier.dataProvider("data")) | ||
|
||
|
||
focusManager!!.moveFocus(FocusDirection.Next) | ||
rule.awaitIdle() | ||
|
||
rule.onNodeWithTag("non_data_provider").assertIsFocused() | ||
// 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")) | ||
|
||
focusManager!!.moveFocus(FocusDirection.Next) | ||
rule.awaitIdle() | ||
|
||
rule.onNodeWithTag("data_provider_item").assertIsFocused() | ||
|
||
assertEquals("yes", rootDataProviderModifier.dataProvider("isRoot")) | ||
assertEquals("ok", rootDataProviderModifier.dataProvider("data")) | ||
} | ||
} |