From 91ce83f05db6239471b727607d9c2a4510c7e9d5 Mon Sep 17 00:00:00 2001 From: Daniel Bertoldi Date: Fri, 27 Jun 2025 11:15:38 -0300 Subject: [PATCH] [JEWEL-845] Add Context Menu to External Links [JEWEL-845] Add Context Menu to External Links Updating branch now that #3091 merged update api dumps applying pr suggestions reran apichecktest add KDoc to MessageResourceResolver add KDoc to MessageResourceResolver undo sample xml run config code fixes running ktlint and apichecktest again remove default value for uri param in ExternalLinkImpl fix api dump + run configs remove empty blank line at stand_alone_sample eof more code fixes closes https://github.com/JetBrains/intellij-community/pull/3107 GitOrigin-RevId: c5842c900120b6d96c563e79568f524e3e96540b --- platform/jewel/ide-laf-bridge/api-dump.txt | 5 + .../ide-laf-bridge/api/ide-laf-bridge.api | 6 + .../bridge/BridgeMessageResourceResolver.kt | 26 ++++ .../jewel/bridge/BridgeUriHandler.kt | 25 ++++ .../jewel/bridge/JewelComposePanelWrapper.kt | 3 + .../jewel/bridge/theme/SwingBridgeTheme.kt | 6 + .../IntUiMessageResourceResolver.kt | 30 ++++ .../intui/standalone/theme/IntUiTheme.kt | 3 + .../ideplugin/SwingComparisonTabPanel.kt | 35 +++-- .../samples/showcase/components/Banners.kt | 4 +- .../samples/showcase/components/Links.kt | 5 +- platform/jewel/ui/api-dump.txt | 6 + platform/jewel/ui/api/ui.api | 9 ++ .../jewel/ui/component/ContextMenu.kt | 2 +- .../org/jetbrains/jewel/ui/component/Link.kt | 131 +++++++++++++++++- .../org/jetbrains/jewel/ui/component/Menu.kt | 4 +- .../jewel/ui/util/MessageResourceResolver.kt | 29 ++++ 17 files changed, 306 insertions(+), 23 deletions(-) create mode 100644 platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeMessageResourceResolver.kt create mode 100644 platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUriHandler.kt create mode 100644 platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiMessageResourceResolver.kt create mode 100644 platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/MessageResourceResolver.kt diff --git a/platform/jewel/ide-laf-bridge/api-dump.txt b/platform/jewel/ide-laf-bridge/api-dump.txt index d03aa76fc68a..754faa87c9e5 100644 --- a/platform/jewel/ide-laf-bridge/api-dump.txt +++ b/platform/jewel/ide-laf-bridge/api-dump.txt @@ -26,6 +26,11 @@ f:org.jetbrains.jewel.bridge.BridgeTypography - getMedium(androidx.compose.runtime.Composer,I):androidx.compose.ui.text.TextStyle - getRegular(androidx.compose.runtime.Composer,I):androidx.compose.ui.text.TextStyle - getSmall(androidx.compose.runtime.Composer,I):androidx.compose.ui.text.TextStyle +f:org.jetbrains.jewel.bridge.BridgeUriHandler +- androidx.compose.ui.platform.UriHandler +- sf:$stable:I +- sf:INSTANCE:org.jetbrains.jewel.bridge.BridgeUriHandler +- openUri(java.lang.String):V f:org.jetbrains.jewel.bridge.BridgeUtilsKt - sf:createVerticalBrush-8A-3gB4(java.util.List,F,F,I):androidx.compose.ui.graphics.Brush - bs:createVerticalBrush-8A-3gB4$default(java.util.List,F,F,I,I,java.lang.Object):androidx.compose.ui.graphics.Brush diff --git a/platform/jewel/ide-laf-bridge/api/ide-laf-bridge.api b/platform/jewel/ide-laf-bridge/api/ide-laf-bridge.api index 363dd12ffd94..76644d50edeb 100644 --- a/platform/jewel/ide-laf-bridge/api/ide-laf-bridge.api +++ b/platform/jewel/ide-laf-bridge/api/ide-laf-bridge.api @@ -35,6 +35,12 @@ public final class org/jetbrains/jewel/bridge/BridgeTypography : org/jetbrains/j public fun getSmall (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/text/TextStyle; } +public final class org/jetbrains/jewel/bridge/BridgeUriHandler : androidx/compose/ui/platform/UriHandler { + public static final field $stable I + public static final field INSTANCE Lorg/jetbrains/jewel/bridge/BridgeUriHandler; + public fun openUri (Ljava/lang/String;)V +} + public final class org/jetbrains/jewel/bridge/BridgeUtilsKt { public static final fun createVerticalBrush-8A-3gB4 (Ljava/util/List;FFI)Landroidx/compose/ui/graphics/Brush; public static synthetic fun createVerticalBrush-8A-3gB4$default (Ljava/util/List;FFIILjava/lang/Object;)Landroidx/compose/ui/graphics/Brush; diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeMessageResourceResolver.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeMessageResourceResolver.kt new file mode 100644 index 000000000000..a467443da7c8 --- /dev/null +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeMessageResourceResolver.kt @@ -0,0 +1,26 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.bridge + +import com.intellij.ide.IdeBundle +import org.jetbrains.jewel.ui.util.MessageResourceResolver + +/** + * A provider for fetching localized strings using the IDE's message bundle. + * + * This class implements the [MessageResourceResolver] interface and delegates the retrieval of strings to the + * [IdeBundle.message] method. It can be used to retrieve strings associated with a specific key from the IDE's resource + * bundle. + */ +internal class BridgeMessageResourceResolver : MessageResourceResolver { + /** + * Fetches a string associated with the given key from the IDE's message bundle. + * + * This function uses the [IdeBundle.message] method to retrieve a localized string based on the provided key. The + * key should correspond to a message ID defined in the IDE's resource bundle. + * + * @param key The key representing the string to fetch. This key typically corresponds to a message ID in the IDE's + * message bundle (e.g, ""action.text.copy.link.address"). + * @return The string associated with the provided key. If the key is not found, it will return an empty string. + */ + override fun resolveIdeBundleMessage(key: String): String = IdeBundle.message(key) +} diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUriHandler.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUriHandler.kt new file mode 100644 index 000000000000..49e7c2c88d2c --- /dev/null +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUriHandler.kt @@ -0,0 +1,25 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.bridge + +import androidx.compose.ui.platform.UriHandler +import com.intellij.ide.BrowserUtil + +/** + * A custom implementation of [UriHandler] that delegates the handling to IntelliJ's BrowserUtil class. + * + * This object overrides the [openUri] function to invoke the [BrowserUtil.browse] method, which attempts to open the + * provided URI. If the URI is valid, it opens in the system's default web browser. If the URI is invalid (e.g., an + * empty string), it may open a local file explorer (Windows) or Finder (macOS), depending on the URI and operating + * system. + * + * Example usage: + * ``` + * BridgeUriHandler.openUri("https://www.example.com") + * BridgeUriHandler.openUri("") // May open file explorer or Finder + * ``` + */ +public object BridgeUriHandler : UriHandler { + override fun openUri(uri: String) { + BrowserUtil.browse(uri) + } +} 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 c5ec4402caab..e0759c67d33e 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 @@ -20,6 +20,7 @@ import org.jetbrains.jewel.foundation.ExperimentalJewelApi import org.jetbrains.jewel.foundation.InternalJewelApi import org.jetbrains.jewel.foundation.util.JewelLogger import org.jetbrains.jewel.ui.component.LocalPopupRenderer +import org.jetbrains.jewel.ui.util.LocalMessageResourceResolverProvider public fun compose(config: ComposePanel.() -> Unit = {}, content: @Composable () -> Unit): JComponent = JewelComposePanel(config, content) @@ -73,6 +74,7 @@ public fun JewelComposeNoThemePanel(config: ComposePanel.() -> Unit = {}, conten CompositionLocalProvider( LocalComponent provides this@createJewelComposePanel, LocalPopupRenderer provides JBPopupRenderer, + LocalMessageResourceResolverProvider provides BridgeMessageResourceResolver(), ) { ComponentDataProviderBridge(jewelPanel, content = content) } @@ -97,6 +99,7 @@ public fun JewelToolWindowNoThemeComposePanel( CompositionLocalProvider( LocalComponent provides this@createJewelComposePanel, LocalPopupRenderer provides JBPopupRenderer, + LocalMessageResourceResolverProvider provides BridgeMessageResourceResolver(), ) { ComponentDataProviderBridge(jewelPanel, content = content) } diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt index 4328aafed5cb..5888dac8797a 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt @@ -7,8 +7,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler +import org.jetbrains.jewel.bridge.BridgeMessageResourceResolver import org.jetbrains.jewel.bridge.BridgePainterHintsProvider import org.jetbrains.jewel.bridge.BridgeTypography +import org.jetbrains.jewel.bridge.BridgeUriHandler import org.jetbrains.jewel.bridge.SwingBridgeReader import org.jetbrains.jewel.bridge.clipboard.JewelBridgeClipboard import org.jetbrains.jewel.bridge.icon.BridgeNewUiChecker @@ -23,6 +26,7 @@ import org.jetbrains.jewel.ui.LocalTypography import org.jetbrains.jewel.ui.icon.LocalNewUiChecker import org.jetbrains.jewel.ui.painter.LocalPainterHintsProvider import org.jetbrains.jewel.ui.theme.BaseJewelTheme +import org.jetbrains.jewel.ui.util.LocalMessageResourceResolverProvider private val bridgeThemeReader by lazy { SwingBridgeReader() } @@ -44,6 +48,8 @@ public fun SwingBridgeTheme(content: @Composable () -> Unit) { LocalMenuItemShortcutProvider provides BridgeMenuItemShortcutProvider, LocalMenuItemShortcutHintProvider provides BridgeMenuItemShortcutHintProvider, LocalTypography provides BridgeTypography, + LocalUriHandler provides BridgeUriHandler, + LocalMessageResourceResolverProvider provides BridgeMessageResourceResolver(), ) { content() } diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiMessageResourceResolver.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiMessageResourceResolver.kt new file mode 100644 index 000000000000..f4e009481c43 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiMessageResourceResolver.kt @@ -0,0 +1,30 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.intui.standalone + +import org.jetbrains.jewel.ui.util.MessageResourceResolver + +/** + * A provider for UI strings that returns localized or default string values based on a given key. + * + * This class implements the [MessageResourceResolver] interface and is responsible for fetching string values + * associated with specific keys. It can be used to retrieve action text for UI elements, like buttons or menu items + * based on the string keys from IntelliJ Platform resource bundle. + */ +internal class IntUiMessageResourceResolver : MessageResourceResolver { + /** + * Fetches the string associated with a given key. + * + * This function looks up the provided key and returns the corresponding string value. If the key is not found, it + * returns an empty string by default. + * + * @param key The key representing the string to fetch. This key typically corresponds to a message ID in the IDE's + * message bundle (e.g, ""action.text.copy.link.address"). + * @return The string associated with the provided key. If the key is not found, an empty string is returned. + */ + override fun resolveIdeBundleMessage(key: String): String = + when (key) { + "action.text.open.link.in.browser" -> "Open Link in Browser" + "action.text.copy.link.address" -> "Copy Link Address" + else -> "" + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt index 5d8f93bde3be..21b7488dc42d 100644 --- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt @@ -14,6 +14,7 @@ import org.jetbrains.jewel.foundation.theme.ThemeDefinition import org.jetbrains.jewel.foundation.theme.ThemeIconData import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme +import org.jetbrains.jewel.intui.standalone.IntUiMessageResourceResolver import org.jetbrains.jewel.intui.standalone.IntUiTypography import org.jetbrains.jewel.intui.standalone.StandalonePainterHintsProvider import org.jetbrains.jewel.intui.standalone.icon.StandaloneNewUiChecker @@ -62,6 +63,7 @@ import org.jetbrains.jewel.ui.component.styling.TooltipStyle import org.jetbrains.jewel.ui.icon.LocalNewUiChecker import org.jetbrains.jewel.ui.painter.LocalPainterHintsProvider import org.jetbrains.jewel.ui.theme.BaseJewelTheme +import org.jetbrains.jewel.ui.util.LocalMessageResourceResolverProvider /** * Create a light theme definition. @@ -320,6 +322,7 @@ public fun IntUiTheme( LocalMenuItemShortcutProvider provides StandaloneShortcutProvider, LocalMenuItemShortcutHintProvider provides StandaloneMenuItemShortcutHintProvider, LocalTypography provides IntUiTypography, + LocalMessageResourceResolverProvider provides IntUiMessageResourceResolver(), ) { content() } diff --git a/platform/jewel/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt b/platform/jewel/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt index d2a7793140dd..fcf532365e76 100644 --- a/platform/jewel/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt +++ b/platform/jewel/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt @@ -89,26 +89,35 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { } private fun Panel.linksRow() { - row("Links:") { - browserLink("Enabled link", "") + val jewelReadmeLink = "https://github.com/JetBrains/intellij-community/tree/master/platform/jewel/#readme" - compose { ExternalLink(text = "Enabled link", enabled = true, onClick = {}) } + row("Links:") { + cell( + component = + BrowserLink( + icon = AllIcons.Ide.External_link_arrow, + text = "Enabled link", + tooltip = null, + url = "", + ) + .apply { enabled(true) } + ) + + compose { ExternalLink(text = "Enabled link", uri = "", enabled = true) } cell( component = BrowserLink( - icon = IconLoader.getDisabledIcon(AllIcons.Ide.External_link_arrow), - text = "Disabled link", - tooltip = null, - url = "", - ) + icon = IconLoader.getDisabledIcon(AllIcons.Ide.External_link_arrow), + text = "Disabled link", + tooltip = null, + url = jewelReadmeLink, + ) + .apply { isEnabled = false } ) - .applyToComponent { - enabled(false) - autoHideOnDisable = false - } + .applyToComponent { autoHideOnDisable = false } - compose { ExternalLink(text = "Disabled link", enabled = false, onClick = {}) } + compose { ExternalLink(text = "Disabled link", uri = "", enabled = false) } } .layout(RowLayout.PARENT_GRID) } diff --git a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt index e89042bee1d9..d65179c7bebc 100644 --- a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt +++ b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt @@ -63,8 +63,8 @@ public fun Banners() { style = JewelTheme.defaultBannerStyle.information, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", actions = { - Link("Action A", onClick = { clickLabel = "Info default no icon Action A clicked" }) - Link("Action B", onClick = { clickLabel = "Info default no icon Action B clicked" }) + Link("Action A", onClick = { clickLabel = "Info default with icon Action A clicked" }) + Link("Action B", onClick = { clickLabel = "Info default with icon Action B clicked" }) }, ) diff --git a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Links.kt b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Links.kt index 6df39719f625..1146c85ca167 100644 --- a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Links.kt +++ b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Links.kt @@ -27,12 +27,13 @@ import org.jetbrains.jewel.ui.theme.linkStyle @Composable public fun Links() { val alwaysUnderline = JewelTheme.linkStyle.copy(underlineBehavior = LinkUnderlineBehavior.ShowAlways) + val jewelReadMeLink = "https://github.com/JetBrains/intellij-community/tree/master/platform/jewel/#readme" Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { Link(text = "Link", onClick = {}) Link(text = "Always underlined", onClick = {}, style = alwaysUnderline) - ExternalLink("ExternalLink", {}) + ExternalLink(text = "ExternalLink", uri = jewelReadMeLink) val items = remember { listOf("Light", "Dark", "---", "High Contrast", "Darcula", "IntelliJ Light") } var selected by remember { mutableStateOf(items.first()) } @@ -51,7 +52,7 @@ public fun Links() { Link(text = "Always underlined", onClick = {}, style = alwaysUnderline, enabled = false) - ExternalLink(text = "ExternalLink", onClick = {}, enabled = false) + ExternalLink(text = "ExternalLink", uri = jewelReadMeLink) DropdownLink(text = "DropdownLink", enabled = false) {} } diff --git a/platform/jewel/ui/api-dump.txt b/platform/jewel/ui/api-dump.txt index ffd4a77cb279..ffba5eaaab4e 100644 --- a/platform/jewel/ui/api-dump.txt +++ b/platform/jewel/ui/api-dump.txt @@ -88,6 +88,8 @@ f:org.jetbrains.jewel.ui.component.DefaultMenuController - onHoveredChange(Z):V - registerShortcutAction(javax.swing.KeyStroke,kotlin.jvm.functions.Function0):V - submenuController(kotlin.jvm.functions.Function1):org.jetbrains.jewel.ui.component.DefaultMenuController +f:org.jetbrains.jewel.ui.component.LinkKt +- sf:ExternalLink-kye4rC8(java.lang.String,java.lang.String,androidx.compose.ui.Modifier,Z,androidx.compose.ui.text.TextStyle,I,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.LinkStyle,androidx.compose.runtime.Composer,I,I):V org.jetbrains.jewel.ui.component.MenuController - a:clearShortcutActions():V - a:close-iuPiT84(I):Z @@ -372,3 +374,7 @@ org.jetbrains.jewel.ui.painter.PainterProviderScope org.jetbrains.jewel.ui.painter.ResourcePainterProviderScope - org.jetbrains.jewel.ui.painter.PainterProviderScope - a:getClassLoaders():java.util.Set +org.jetbrains.jewel.ui.util.MessageResourceResolver +- a:resolveIdeBundleMessage(java.lang.String):java.lang.String +f:org.jetbrains.jewel.ui.util.MessageResourceResolverKt +- sf:getLocalMessageResourceResolverProvider():androidx.compose.runtime.ProvidableCompositionLocal diff --git a/platform/jewel/ui/api/ui.api b/platform/jewel/ui/api/ui.api index b14b80af7f6b..3ca0fb146706 100644 --- a/platform/jewel/ui/api/ui.api +++ b/platform/jewel/ui/api/ui.api @@ -625,6 +625,7 @@ public final class org/jetbrains/jewel/ui/component/LinearProgressBarKt { public final class org/jetbrains/jewel/ui/component/LinkKt { public static final fun DropdownLink-RWo7tUw (Ljava/lang/String;Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/text/TextStyle;ILandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ExternalLink-kye4rC8 (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/text/TextStyle;ILandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Landroidx/compose/runtime/Composer;II)V public static final fun ExternalLink-kye4rC8 (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/text/TextStyle;ILandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Landroidx/compose/runtime/Composer;II)V public static final fun Link-kye4rC8 (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/text/TextStyle;ILandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Landroidx/compose/runtime/Composer;II)V } @@ -5096,6 +5097,14 @@ public final class org/jetbrains/jewel/ui/util/ColorExtensionsKt { public static final fun toRgbaHexString-8_81llA (J)Ljava/lang/String; } +public abstract interface class org/jetbrains/jewel/ui/util/MessageResourceResolver { + public abstract fun resolveIdeBundleMessage (Ljava/lang/String;)Ljava/lang/String; +} + +public final class org/jetbrains/jewel/ui/util/MessageResourceResolverKt { + public static final fun getLocalMessageResourceResolverProvider ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + public final class org/jetbrains/jewel/ui/util/ModifierExtensionsKt { public static final fun thenIf (Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier; } diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ContextMenu.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ContextMenu.kt index cd23bb28625a..c5a2b91a65b5 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ContextMenu.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ContextMenu.kt @@ -118,7 +118,7 @@ public class ContextSubmenu(label: String, public val submenu: () -> List Unit, ) : ContextMenuItem(label, action) diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Link.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Link.kt index 6674e6023568..b9834973ba13 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Link.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Link.kt @@ -1,5 +1,6 @@ package org.jetbrains.jewel.ui.component +import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.HoverInteraction @@ -17,18 +18,26 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalInputModeManager +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import java.awt.Cursor +import java.awt.datatransfer.StringSelection +import java.io.IOException +import kotlinx.coroutines.launch import org.jetbrains.jewel.foundation.modifier.onHover import org.jetbrains.jewel.foundation.modifier.thenIf import org.jetbrains.jewel.foundation.state.CommonStateBitMask @@ -40,6 +49,7 @@ import org.jetbrains.jewel.foundation.state.CommonStateBitMask.Pressed import org.jetbrains.jewel.foundation.state.FocusableComponentState import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.JewelTheme.Companion.isSwingCompatMode +import org.jetbrains.jewel.foundation.util.JewelLogger import org.jetbrains.jewel.ui.component.styling.LinkStyle import org.jetbrains.jewel.ui.component.styling.LinkUnderlineBehavior.ShowAlways import org.jetbrains.jewel.ui.component.styling.LinkUnderlineBehavior.ShowOnHover @@ -49,7 +59,9 @@ import org.jetbrains.jewel.ui.component.styling.MenuStyle import org.jetbrains.jewel.ui.disabledAppearance import org.jetbrains.jewel.ui.focusOutline import org.jetbrains.jewel.ui.icon.IconKey +import org.jetbrains.jewel.ui.icons.AllIconsKeys import org.jetbrains.jewel.ui.painter.hints.Stateful +import org.jetbrains.jewel.ui.util.LocalMessageResourceResolverProvider /** * A clickable text link that follows the standard visual styling with customizable appearance. @@ -112,8 +124,8 @@ public fun Link( * **Usage example:** * [`Links.kt`](https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Links.kt) * - * **Swing equivalent:** [`JLabel`](https://docs.oracle.com/javase/tutorial/uiswing/components/label.html) with HTML - * link styling and external link icon + * **Swing equivalent:** + * [`BrowserLink`](https://github.com/JetBrains/intellij-community/blob/master/platform/platform-api/src/com/intellij/ui/components/BrowserLink.kt) * * @param text The text to be displayed as a link * @param onClick Called when the link is clicked @@ -136,8 +148,9 @@ public fun ExternalLink( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: LinkStyle = LocalLinkStyle.current, ) { - LinkImpl( + ExternalLinkImpl( text = text, + uri = "", onClick = onClick, modifier = modifier, enabled = enabled, @@ -145,10 +158,120 @@ public fun ExternalLink( interactionSource = interactionSource, style = style, textStyle = textStyle, - icon = style.icons.externalLink, ) } +/** + * An external link that follows the standard visual styling, including an external link icon. + * + * Please be aware that this ExternalLink will automatically open a link on click, unlike the other overloads. + * + * Provides a text link with an external link icon that indicates the link leads to external content. The link supports + * various states including enabled/disabled, focused, and hovered, with optional underline behavior based on the style + * configuration. + * + * **Guidelines:** [on IJP SDK webhelp](https://plugins.jetbrains.com/docs/intellij/link.html#external-link-icon) + * + * **Usage example:** + * [`Links.kt`](https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Links.kt) + * + * **Swing equivalent:** + * [`BrowserLink`](https://github.com/JetBrains/intellij-community/blob/master/platform/platform-api/src/com/intellij/ui/components/BrowserLink.kt) + * + * @param text The text to be displayed as a link + * @param uri The actual uri that will be used to open in the browser. If it fails to open, the error is suppressed and + * the error is logged. + * @param modifier Modifier to be applied to the link + * @param enabled Controls whether the link can be interacted with + * @param textStyle The typography style to be applied to the link text + * @param overflow How the text should handle overflow + * @param interactionSource Source of interactions for this link + * @param style The visual styling configuration for the link + * @see javax.swing.JLabel + */ +@Composable +public fun ExternalLink( + text: String, + uri: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + textStyle: TextStyle = JewelTheme.defaultTextStyle, + overflow: TextOverflow = TextOverflow.Clip, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: LinkStyle = LocalLinkStyle.current, +) { + val uriHandler = LocalUriHandler.current + + ExternalLinkImpl( + text = text, + onClick = { openUri(uriHandler, uri) }, + uri = uri, + modifier = modifier, + enabled = enabled, + overflow = overflow, + interactionSource = interactionSource, + style = style, + textStyle = textStyle, + ) +} + +@Composable +private fun ExternalLinkImpl( + text: String, + onClick: () -> Unit, + uri: String, + modifier: Modifier, + enabled: Boolean, + overflow: TextOverflow, + interactionSource: MutableInteractionSource, + style: LinkStyle, + textStyle: TextStyle, +) { + val clipboard = LocalClipboard.current + val stringProvider = LocalMessageResourceResolverProvider.current + val scope = rememberCoroutineScope() + + ContextMenuArea( + items = { + listOf( + ContextMenuItemOption( + label = stringProvider.resolveIdeBundleMessage("action.text.open.link.in.browser"), + action = { onClick() }, + icon = AllIconsKeys.Nodes.PpWeb, + ), + ContextMenuItemOption( + label = stringProvider.resolveIdeBundleMessage("action.text.copy.link.address"), + action = { scope.launch { clipboard.setClipEntry(ClipEntry(StringSelection(uri))) } }, + icon = AllIconsKeys.Actions.Copy, + ), + ) + }, + enabled = enabled, + content = { + LinkImpl( + text = text, + onClick = onClick, + modifier = modifier, + enabled = enabled, + overflow = overflow, + interactionSource = interactionSource, + style = style, + textStyle = textStyle, + icon = style.icons.externalLink, + ) + }, + ) +} + +private fun openUri(uriHandler: UriHandler, link: String) = + try { + uriHandler.openUri(link) + } catch (e: IllegalArgumentException) { + JewelLogger.getInstance("ExternalLink").error("Unable to open link ($link). Error: $e") + } catch (e: IOException) { + JewelLogger.getInstance("ExternalLink").error("Unable to open link ($link). Error: $e") + } + /** * A dropdown link that follows the standard visual styling with customizable appearance and menu content. * diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt index e32f7024682a..4c4fd424710d 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt @@ -166,7 +166,9 @@ public fun MenuContent( val selectableItems = remember(items) { items.filterIsInstance() } val anyItemHasIcon = remember { selectableItems.any { it.iconKey != null } } - val anyItemHasKeybinding = remember { selectableItems.any { it.keybinding != null || it.itemOptionAction != null } } + val anyItemHasKeybinding = remember { + selectableItems.any { it.keybinding?.isNotEmpty() == true || it.itemOptionAction != null } + } val localMenuController = LocalMenuController.current val localInputModeManager = LocalInputModeManager.current diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/MessageResourceResolver.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/MessageResourceResolver.kt new file mode 100644 index 000000000000..c477ca3c0470 --- /dev/null +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/MessageResourceResolver.kt @@ -0,0 +1,29 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.ui.util + +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.staticCompositionLocalOf +import org.jetbrains.jewel.foundation.InternalJewelApi + +/** + * Interface for resolving messages from the IDE's resource bundle. + * + * This interface provides a mechanism to resolve strings based on a given key. It is used to fetch UI strings from the + * IDE's internal resource bundle. + */ +@InternalJewelApi +public interface MessageResourceResolver { + /** + * Resolves a message using the provided key. + * + * @param key The key representing the message to resolve. + * @return The resolved message string associated with the provided key. If the key is not found, an empty string + * will be returned. + */ + public fun resolveIdeBundleMessage(key: String): String +} + +public val LocalMessageResourceResolverProvider: ProvidableCompositionLocal = + staticCompositionLocalOf { + error("No LocalMessageResourceResolverProvider provided. Have you forgotten the theme?") + }