diff --git a/platform/jewel/ide-laf-bridge/api-dump-experimental.txt b/platform/jewel/ide-laf-bridge/api-dump-experimental.txt index 4a828f55764e..9c34434bb18e 100644 --- a/platform/jewel/ide-laf-bridge/api-dump-experimental.txt +++ b/platform/jewel/ide-laf-bridge/api-dump-experimental.txt @@ -5,9 +5,13 @@ f:org.jetbrains.jewel.bridge.BridgeUtilsKt - *sf:toNonNegativeDpSize(java.awt.Dimension):J f:org.jetbrains.jewel.bridge.JewelComposePanelWrapperKt - *sf:JewelComposeNoThemePanel(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent +- *sf:JewelComposeNoThemePanel(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent - *bs:JewelComposeNoThemePanel$default(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent +- *bs:JewelComposeNoThemePanel$default(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent - *sf:composeWithoutTheme(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent +- *sf:composeWithoutTheme(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent - *bs:composeWithoutTheme$default(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent +- *bs:composeWithoutTheme$default(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent - *sf:getLocalComponent():androidx.compose.runtime.ProvidableCompositionLocal f:org.jetbrains.jewel.bridge.theme.IntUiBridgeTextKt - *sf:retrieveDefaultTextStyle(F):androidx.compose.ui.text.TextStyle diff --git a/platform/jewel/ide-laf-bridge/api-dump.txt b/platform/jewel/ide-laf-bridge/api-dump.txt index 244cdd7c9761..ec09a90ebe63 100644 --- a/platform/jewel/ide-laf-bridge/api-dump.txt +++ b/platform/jewel/ide-laf-bridge/api-dump.txt @@ -71,12 +71,18 @@ f:org.jetbrains.jewel.bridge.JewelBridgeException$KeysNotFoundException - (java.util.List,java.lang.String):V f:org.jetbrains.jewel.bridge.JewelComposePanelWrapperKt - sf:JewelComposePanel(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent +- sf:JewelComposePanel(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent - bs:JewelComposePanel$default(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent +- bs:JewelComposePanel$default(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent - sf:compose(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent +- sf:compose(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2):javax.swing.JComponent - bs:compose$default(kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent +- bs:compose$default(Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,I,java.lang.Object):javax.swing.JComponent f:org.jetbrains.jewel.bridge.ToolWindowExtensionsKt - sf:addComposeTab(com.intellij.openapi.wm.ToolWindow,java.lang.String,Z,Z,kotlin.jvm.functions.Function3):V +- sf:addComposeTab(com.intellij.openapi.wm.ToolWindow,java.lang.String,Z,Z,Z,kotlin.jvm.functions.Function3):V - bs:addComposeTab$default(com.intellij.openapi.wm.ToolWindow,java.lang.String,Z,Z,kotlin.jvm.functions.Function3,I,java.lang.Object):V +- bs:addComposeTab$default(com.intellij.openapi.wm.ToolWindow,java.lang.String,Z,Z,Z,kotlin.jvm.functions.Function3,I,java.lang.Object):V org.jetbrains.jewel.bridge.ToolWindowScope - a:getToolWindow():com.intellij.openapi.wm.ToolWindow f:org.jetbrains.jewel.bridge.TypographyKt diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt index 476dcb7a7090..7b26996de076 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt @@ -7,11 +7,15 @@ import androidx.compose.ui.awt.ComposePanel import com.intellij.openapi.actionSystem.DataSink import com.intellij.openapi.actionSystem.UiDataProvider import com.intellij.openapi.application.PathManager -import java.awt.BorderLayout +import com.intellij.util.ui.components.BorderLayoutPanel +import java.awt.AWTEvent import java.awt.Component +import java.awt.Toolkit +import java.awt.event.AWTEventListener +import java.awt.event.MouseEvent import java.io.File import javax.swing.JComponent -import javax.swing.JPanel +import javax.swing.SwingUtilities import org.jetbrains.annotations.ApiStatus import org.jetbrains.jewel.bridge.actionSystem.ComponentDataProviderBridge import org.jetbrains.jewel.bridge.component.JBPopupRenderer @@ -27,11 +31,28 @@ import org.jetbrains.jewel.ui.util.LocalMessageResourceResolverProvider * * The [content] is wrapped in a [SwingBridgeTheme], which will be derived from the current Swing LaF. * + * @param focusOnClickInside If `true`, the underlying [ComposePanel] will request focus when a mouse click occurs + * inside it, even if it does not hit a "focusable" element. * @param config A lambda to configure the underlying [ComposePanel]. * @param content The Composable content to display. */ +public fun compose( + focusOnClickInside: Boolean = true, + config: ComposePanel.() -> Unit = {}, + content: @Composable () -> Unit, +): JComponent = JewelComposePanel(focusOnClickInside, config, content) + +/** + * Creates a Swing component that can host Compose content. + * + * The [content] is wrapped in a [SwingBridgeTheme], which will be derived from the current Swing LaF. + * + * @param config A lambda to configure the underlying [ComposePanel]. + * @param content The Composable content to display. + */ +@Deprecated("Use the version with 'focusOnClickInside' parameter") public fun compose(config: ComposePanel.() -> Unit = {}, content: @Composable () -> Unit): JComponent = - JewelComposePanel(config, content) + JewelComposePanel(focusOnClickInside = false, config, content) /** * Creates a Swing component that can host Compose content. @@ -40,12 +61,18 @@ public fun compose(config: ComposePanel.() -> Unit = {}, content: @Composable () * * This is the same as [compose]. * + * @param focusOnClickInside If `true`, the underlying [ComposePanel] will request focus when a mouse click occurs + * inside it, even if it does not hit a "focusable" element. * @param config A lambda to configure the underlying [ComposePanel]. * @param content The Composable content to display. */ @Suppress("ktlint:standard:function-naming", "FunctionName") // Swing to Compose bridge API -public fun JewelComposePanel(config: ComposePanel.() -> Unit = {}, content: @Composable () -> Unit): JComponent = - createJewelComposePanel { jewelPanel -> +public fun JewelComposePanel( + focusOnClickInside: Boolean = true, + config: ComposePanel.() -> Unit = {}, + content: @Composable () -> Unit, +): JComponent = + createJewelComposePanel(focusOnClickInside) { jewelPanel -> config() setContent { SwingBridgeTheme { @@ -59,6 +86,41 @@ public fun JewelComposePanel(config: ComposePanel.() -> Unit = {}, content: @Com } } +/** + * Creates a Swing component that can host Compose content. + * + * The [content] is wrapped in a [SwingBridgeTheme], which will be derived from the current Swing LaF. + * + * This is the same as [compose]. + * + * @param config A lambda to configure the underlying [ComposePanel]. + * @param content The Composable content to display. + */ +@Suppress("ktlint:standard:function-naming", "FunctionName") // Swing to Compose bridge API +@Deprecated("Use the version with 'focusOnClickInside' parameter") +public fun JewelComposePanel(config: ComposePanel.() -> Unit = {}, content: @Composable () -> Unit): JComponent = + JewelComposePanel(focusOnClickInside = false, config, content) + +/** + * Creates a Swing component that can host Compose content. + * + * The [content] is **not** wrapped in a theme, meaning that you **MUST** wrap the content in a theme by yourself. + * + * This is not normally what you want; use this only if you want to provide a completely custom theme. + * + * @param focusOnClickInside If `true`, the underlying [ComposePanel] will request focus when a mouse click occurs + * inside it, even if it does not hit a "focusable" element. + * @param config A lambda to configure the underlying [ComposePanel]. + * @param content The Composable content to display. + */ +@ApiStatus.Experimental +@ExperimentalJewelApi +public fun composeWithoutTheme( + focusOnClickInside: Boolean = true, + config: ComposePanel.() -> Unit = {}, + content: @Composable () -> Unit, +): JComponent = JewelComposeNoThemePanel(focusOnClickInside, config, content) + /** * Creates a Swing component that can host Compose content. * @@ -72,8 +134,44 @@ public fun JewelComposePanel(config: ComposePanel.() -> Unit = {}, content: @Com @ApiStatus.Experimental @ExperimentalJewelApi @Suppress("ktlint:standard:function-naming") // Swing to Compose bridge API +@Deprecated("Use the version with 'focusOnClickInside' parameter") public fun composeWithoutTheme(config: ComposePanel.() -> Unit = {}, content: @Composable () -> Unit): JComponent = - JewelComposeNoThemePanel(config, content) + JewelComposeNoThemePanel(focusOnClickInside = false, config, content) + +/** + * Creates a Swing component that can host Compose content. + * + * The [content] is **not** wrapped in a theme, meaning that you **MUST** wrap the content in a theme by yourself. + * + * This is not normally what you want; use this only if you want to provide a completely custom theme. + * + * This is the same as [composeWithoutTheme]. + * + * @param focusOnClickInside If `true`, the underlying [ComposePanel] will request focus when a mouse click occurs + * inside it, even if it does not hit a "focusable" element. + * @param config A lambda to configure the underlying [ComposePanel]. + * @param content The Composable content to display. + */ +@ApiStatus.Experimental +@ExperimentalJewelApi +@Suppress("ktlint:standard:function-naming", "FunctionName") // Swing to Compose bridge API +public fun JewelComposeNoThemePanel( + focusOnClickInside: Boolean = true, + config: ComposePanel.() -> Unit = {}, + content: @Composable () -> Unit, +): JComponent = + createJewelComposePanel(focusOnClickInside) { jewelPanel -> + config() + setContent { + CompositionLocalProvider( + LocalComponentFoundation provides this@createJewelComposePanel, + LocalPopupRenderer provides JBPopupRenderer, + LocalMessageResourceResolverProvider provides BridgeMessageResourceResolver(), + ) { + ComponentDataProviderBridge(jewelPanel, content = content) + } + } + } /** * Creates a Swing component that can host Compose content. @@ -90,21 +188,14 @@ public fun composeWithoutTheme(config: ComposePanel.() -> Unit = {}, content: @C @ApiStatus.Experimental @ExperimentalJewelApi @Suppress("ktlint:standard:function-naming", "FunctionName") // Swing to Compose bridge API +@Deprecated("Use the version with 'focusOnClickInside' parameter") public fun JewelComposeNoThemePanel(config: ComposePanel.() -> Unit = {}, content: @Composable () -> Unit): JComponent = - createJewelComposePanel { jewelPanel -> - config() - setContent { - CompositionLocalProvider( - LocalComponentFoundation provides this@createJewelComposePanel, - LocalPopupRenderer provides JBPopupRenderer, - LocalMessageResourceResolverProvider provides BridgeMessageResourceResolver(), - ) { - ComponentDataProviderBridge(jewelPanel, content = content) - } - } - } + JewelComposeNoThemePanel(focusOnClickInside = false, config, content) -private fun createJewelComposePanel(config: ComposePanel.(JewelComposePanelWrapper) -> Unit): JewelComposePanelWrapper { +private fun createJewelComposePanel( + focusOnClickInside: Boolean, + config: ComposePanel.(JewelComposePanelWrapper) -> Unit, +): JewelComposePanelWrapper { if (System.getProperty("skiko.library.path") == null) { val bundledSkikoFolder = File(PathManager.getLibPath(), "/skiko-awt-runtime-all") if (bundledSkikoFolder.isDirectory && bundledSkikoFolder.canRead()) { @@ -113,22 +204,26 @@ private fun createJewelComposePanel(config: ComposePanel.(JewelComposePanelWrapp JewelLogger.getInstance("SkikoLoader").warn("Bundled Skiko not found/not readable, falling back to default") } } - val jewelPanel = JewelComposePanelWrapper() - jewelPanel.layout = BorderLayout() - val composePanel = ComposePanel() - jewelPanel.add(composePanel, BorderLayout.CENTER) - composePanel.config(jewelPanel) + val jewelPanel = JewelComposePanelWrapper(focusOnClickInside) + jewelPanel.composePanel.config(jewelPanel) ComposeUiInspector(jewelPanel) return jewelPanel } -internal class JewelComposePanelWrapper : JPanel(), UiDataProvider { +internal class JewelComposePanelWrapper(private val focusOnClickInside: Boolean) : BorderLayoutPanel(), UiDataProvider { internal var targetProvider: UiDataProvider? = null + private val listener = AWTEventListener { event -> + if (event !is MouseEvent || event.button == MouseEvent.NOBUTTON) return@AWTEventListener + if (!isFocusOwner && SwingUtilities.isDescendingFrom(event.component, this)) { + composePanel.requestFocus() + } + } - val composePanel: ComposePanel - get() = - components.singleOrNull() as? ComposePanel - ?: error("JewelComposePanelWrapper was not initialized with a ComposePanel") + val composePanel: ComposePanel = ComposePanel() + + init { + super.addToCenter(composePanel) + } override fun addImpl(comp: Component, constraints: Any?, index: Int) { require(components.isEmpty()) { @@ -142,6 +237,20 @@ internal class JewelComposePanelWrapper : JPanel(), UiDataProvider { super.addImpl(comp, constraints, index) } + override fun addNotify() { + super.addNotify() + if (focusOnClickInside) { + Toolkit.getDefaultToolkit().addAWTEventListener(listener, AWTEvent.MOUSE_EVENT_MASK) + } + } + + override fun removeNotify() { + super.removeNotify() + if (focusOnClickInside) { + Toolkit.getDefaultToolkit().removeAWTEventListener(listener) + } + } + override fun uiDataSnapshot(sink: DataSink) { targetProvider?.uiDataSnapshot(sink) } diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ToolWindowExtensions.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ToolWindowExtensions.kt index f117ceb7e5cc..b5e318af1f7b 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ToolWindowExtensions.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ToolWindowExtensions.kt @@ -8,6 +8,8 @@ import org.jetbrains.jewel.foundation.enableNewSwingCompositing /** * Adds a new tab to the tool window with Compose content. * + * @param focusOnClickInside If `true`, the underlying [ComposePanel] will request focus when a mouse click occurs + * inside it, even if it does not hit a "focusable" element. * @param tabDisplayName The title of the tab. * @param isLockable Whether the tab can be locked. * @param isCloseable Whether the tab can be closed. @@ -17,6 +19,7 @@ public fun ToolWindow.addComposeTab( @TabTitle tabDisplayName: String? = null, isLockable: Boolean = true, isCloseable: Boolean = false, + focusOnClickInside: Boolean = true, content: @Composable ToolWindowScope.() -> Unit, ) { // We need to make sure this is done before Compose is attached. @@ -25,7 +28,7 @@ public fun ToolWindow.addComposeTab( val tabContent = contentManager.factory.createContent( - JewelComposePanel { + JewelComposePanel(focusOnClickInside) { val scope = object : ToolWindowScope { override val toolWindow: ToolWindow @@ -40,6 +43,24 @@ public fun ToolWindow.addComposeTab( contentManager.addContent(tabContent) } +/** + * Adds a new tab to the tool window with Compose content. + * + * @param tabDisplayName The title of the tab. + * @param isLockable Whether the tab can be locked. + * @param isCloseable Whether the tab can be closed. + * @param content The Composable content of the tab. + */ +@Deprecated("Use the version with 'focusOnClickInside' parameter") +public fun ToolWindow.addComposeTab( + @TabTitle tabDisplayName: String? = null, + isLockable: Boolean = true, + isCloseable: Boolean = false, + content: @Composable ToolWindowScope.() -> Unit, +) { + addComposeTab(tabDisplayName, isLockable, isCloseable, focusOnClickInside = true, content) +} + /** A scope for the content of a tool window tab. */ public interface ToolWindowScope { /** The tool window in which the tab is displayed. */ diff --git a/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt b/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt index 7021efe702a3..2dde7d5ef2af 100644 --- a/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt +++ b/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt @@ -12,7 +12,10 @@ import androidx.compose.runtime.* import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.unit.dp import com.intellij.ide.BrowserUtil import com.intellij.openapi.project.Project @@ -63,7 +66,7 @@ internal fun ComponentShowcaseTab(project: Project) { @Composable private fun RowScope.ColumnOne() { - Column(Modifier.trackActivation().weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) { var activated by remember { mutableStateOf(false) } Text( "Here is a selection of our finest components(activated: $activated):", @@ -72,10 +75,16 @@ private fun RowScope.ColumnOne() { ) var selectedItem by remember { mutableIntStateOf(-1) } + val focusRequester = remember { FocusRequester() } + ListComboBox( items = remember { listOf("Hello", "World") }, selectedIndex = selectedItem, onSelectedItemChange = { selectedItem = it }, + modifier = + Modifier + .focusRequester(focusRequester) + .onFirstVisible { focusRequester.requestFocus() }, ) ListComboBox( items = remember { listOf("Hello", "World") }, diff --git a/plugins/devkit/intellij.devkit.compose/src/demo/JewelDemoToolWindowFactory.kt b/plugins/devkit/intellij.devkit.compose/src/demo/JewelDemoToolWindowFactory.kt index 982320042d3a..4bc14e9c043e 100644 --- a/plugins/devkit/intellij.devkit.compose/src/demo/JewelDemoToolWindowFactory.kt +++ b/plugins/devkit/intellij.devkit.compose/src/demo/JewelDemoToolWindowFactory.kt @@ -16,13 +16,22 @@ internal class JewelDemoToolWindowFactory : ToolWindowFactory, DumbAware { // Enable custom popup rendering to use JBPopup instead of the default Compose implementation JewelFlags.useCustomPopupRenderer = true - toolWindow.addComposeTab(DevkitComposeBundle.message("jewel.tw.tab.title.components")) { ComponentShowcaseTab(project) } + toolWindow.addComposeTab( + DevkitComposeBundle.message("jewel.tw.tab.title.components"), + focusOnClickInside = true, + ) { ComponentShowcaseTab(project) } - toolWindow.addComposeTab(DevkitComposeBundle.message("jewel.tw.tab.title.releases.demo")) { ReleasesSampleCompose(project) } + toolWindow.addComposeTab( + DevkitComposeBundle.message("jewel.tw.tab.title.releases.demo"), + focusOnClickInside = true, + ) { ReleasesSampleCompose(project) } toolWindow.addSwingTab(SwingComparisonTabPanel(), DevkitComposeBundle.message("jewel.tw.tab.title.swing.comparison")) - toolWindow.addComposeTab(DevkitComposeBundle.message("jewel.tw.tab.title.scrollbars.sample")) { ScrollbarsShowcaseTab() } + toolWindow.addComposeTab( + DevkitComposeBundle.message("jewel.tw.tab.title.scrollbars.sample"), + focusOnClickInside = true, + ) { ScrollbarsShowcaseTab() } } private fun ToolWindow.addSwingTab(component: JComponent, @TabTitle title: String) {