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?") + }