mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[JEWEL-1205] Fix Menu Items Looking Too Tall in Presentation Mode
closes https://github.com/JetBrains/intellij-community/pull/3419 (cherry picked from commit 134b87df4a183304340fb5e85c3fc16fa4cba527) (cherry picked from commit d6e0ba4e678ffdcb5b361eeb75b598e51cc644dd) IJ-MR-211495 GitOrigin-RevId: c32b93d0e223bbb9e410e4721d0aceccdca9cd01
This commit is contained in:
committed by
intellij-monorepo-bot
parent
917cc8cb18
commit
8768cbb083
@@ -28,6 +28,7 @@ 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
|
||||
- sf:getDp(com.intellij.util.ui.JBValue):F
|
||||
- sf:getUnscaledDp(I):F
|
||||
- sf:retrieveArcAsCornerSize(java.lang.String):androidx.compose.foundation.shape.CornerSize
|
||||
- sf:retrieveArcAsCornerSizeOrDefault(java.lang.String,androidx.compose.foundation.shape.CornerSize):androidx.compose.foundation.shape.CornerSize
|
||||
- sf:retrieveArcAsCornerSizeWithFallbacks(java.lang.String[]):androidx.compose.foundation.shape.CornerSize
|
||||
@@ -49,6 +50,8 @@ f:org.jetbrains.jewel.bridge.BridgeUtilsKt
|
||||
- bs:retrieveTextStyle-WdJyH8Q$default(java.lang.String,J,J,Z,I,J,I,java.lang.Object):androidx.compose.ui.text.TextStyle
|
||||
- sf:retrieveTextStyle-tD9LlGs(java.lang.String,java.lang.String,J,Z,I,J):androidx.compose.ui.text.TextStyle
|
||||
- bs:retrieveTextStyle-tD9LlGs$default(java.lang.String,java.lang.String,J,Z,I,J,I,java.lang.Object):androidx.compose.ui.text.TextStyle
|
||||
- sf:retrieveUnscaledIntAsDpOrUnspecified(java.lang.String):F
|
||||
- sf:retrieveUnscaledIntAsNonNegativeDpOrUnspecified(java.lang.String):F
|
||||
- sf:toAwtColor-8_81llA(J):java.awt.Color
|
||||
- sf:toAwtColorOrNull-8_81llA(J):java.awt.Color
|
||||
- sf:toComposeColor(java.awt.Color):J
|
||||
|
||||
@@ -63,6 +63,8 @@ package org.jetbrains.jewel.bridge {
|
||||
method public static androidx.compose.ui.text.PlatformTextStyle retrievePlatformTextStyle();
|
||||
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String fontKey, optional String? colorKey, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
|
||||
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String key, optional long color, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
|
||||
method public static float retrieveUnscaledIntAsDpOrUnspecified(String key);
|
||||
method public static float retrieveUnscaledIntAsNonNegativeDpOrUnspecified(String key);
|
||||
method public static java.awt.Color toAwtColor(long);
|
||||
method public static java.awt.Color? toAwtColorOrNull(long);
|
||||
method public static long toComposeColor(java.awt.Color);
|
||||
@@ -74,6 +76,7 @@ package org.jetbrains.jewel.bridge {
|
||||
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(com.intellij.util.ui.JBInsets);
|
||||
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(java.awt.Insets);
|
||||
property public static androidx.compose.ui.unit.Dp com.intellij.util.ui.JBValue.dp;
|
||||
property public static androidx.compose.ui.unit.Dp int.unscaledDp;
|
||||
}
|
||||
|
||||
public abstract sealed class JewelBridgeException extends java.lang.RuntimeException {
|
||||
|
||||
@@ -61,6 +61,8 @@ package org.jetbrains.jewel.bridge {
|
||||
method public static androidx.compose.ui.text.PlatformTextStyle retrievePlatformTextStyle();
|
||||
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String fontKey, optional String? colorKey, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
|
||||
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String key, optional long color, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
|
||||
method public static float retrieveUnscaledIntAsDpOrUnspecified(String key);
|
||||
method public static float retrieveUnscaledIntAsNonNegativeDpOrUnspecified(String key);
|
||||
method public static java.awt.Color toAwtColor(long);
|
||||
method public static java.awt.Color? toAwtColorOrNull(long);
|
||||
method public static long toComposeColor(java.awt.Color);
|
||||
@@ -70,6 +72,7 @@ package org.jetbrains.jewel.bridge {
|
||||
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(com.intellij.util.ui.JBInsets);
|
||||
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(java.awt.Insets);
|
||||
property public static androidx.compose.ui.unit.Dp com.intellij.util.ui.JBValue.dp;
|
||||
property public static androidx.compose.ui.unit.Dp int.unscaledDp;
|
||||
}
|
||||
|
||||
public abstract sealed class JewelBridgeException extends java.lang.RuntimeException {
|
||||
|
||||
+56
@@ -42,6 +42,7 @@ import com.intellij.ui.scale.JBUIScale.scale
|
||||
import com.intellij.util.ui.JBDimension
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBInsets
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.JBValue
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
@@ -520,3 +521,58 @@ public fun retrieveEditorColorScheme(): EditorColorsScheme {
|
||||
val manager = EditorColorsManager.getInstance() as EditorColorsManagerImpl
|
||||
return manager.schemeManager.activeScheme ?: DefaultColorSchemesManager.getInstance().firstScheme
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a pre-scaled integer value from the current LaF as a non-negative [Dp], or returns [Dp.Unspecified] if the
|
||||
* key is not found.
|
||||
*
|
||||
* Any negative values read from the LaF will be coerced to `0.dp`.
|
||||
*
|
||||
* Use this instead of [retrieveIntAsNonNegativeDpOrUnspecified] for keys whose values are explicitly rescaled by
|
||||
* `patchHiDPI` in `LafManagerImpl`. See [unscaledDp] for the full list of affected keys and an explanation of why
|
||||
* unscaling is needed.
|
||||
*
|
||||
* @param key The key to look up the integer with.
|
||||
* @return The unscaled integer value from the LaF as a [Dp], or [Dp.Unspecified] if the key is not found.
|
||||
* @see unscaledDp
|
||||
*/
|
||||
public fun retrieveUnscaledIntAsNonNegativeDpOrUnspecified(key: String): Dp =
|
||||
retrieveUnscaledIntAsDpOrUnspecified(key).takeIf { it.isSpecified }?.safeValue() ?: Dp.Unspecified
|
||||
|
||||
/**
|
||||
* Retrieves a pre-scaled integer value from the current LaF as a [Dp], or returns [Dp.Unspecified] if the key is not
|
||||
* found.
|
||||
*
|
||||
* Use this instead of [retrieveIntAsDpOrUnspecified] for keys whose values are explicitly rescaled by `patchHiDPI` in
|
||||
* `LafManagerImpl`. See [unscaledDp] for the full list of affected keys and an explanation of why unscaling is needed.
|
||||
*
|
||||
* @param key The key to look up the integer with.
|
||||
* @return The unscaled integer value from the LaF as a [Dp], or [Dp.Unspecified] if the key is not found.
|
||||
* @see unscaledDp
|
||||
*/
|
||||
public fun retrieveUnscaledIntAsDpOrUnspecified(key: String): Dp {
|
||||
val rawValue = UIManager.get(key)
|
||||
if (rawValue is Int) return rawValue.unscaledDp
|
||||
return Dp.Unspecified
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a pre-scaled JBUI value into a Compose [Dp].
|
||||
*
|
||||
* Certain integer values in UIDefaults are explicitly rescaled by `patchHiDPI` in `LafManagerImpl` after the theme JSON
|
||||
* is applied. This means they already reflect the current JBUI scale factor. For example, `List.rowHeight` is stored as
|
||||
* `24` in the theme JSON, but `patchHiDPI` overwrites it with `scale(24)`. So in Presentation Mode (scale=1.75),
|
||||
* [com.intellij.util.ui.JBUI.CurrentTheme.List.rowHeight] returns `42`, not `24`.
|
||||
*
|
||||
* Passing such a pre-scaled value directly to `.dp` causes double-scaling:
|
||||
* 1. `patchHiDPI` scales the stored value (e.g. 24 → 42 in Presentation Mode)
|
||||
* 2. Compose scales it again via `LocalDensity` (e.g. `42.dp` → way too big)
|
||||
*
|
||||
* The keys currently explicitly rescaled by `patchHiDPI` are: `List.rowHeight`, `Table.rowHeight`, `Tree.rowHeight`,
|
||||
* VCS log row height, `Tree.leftChildIndent`, `Tree.rightChildIndent`, and `SettingsTree.rowHeight`.
|
||||
*
|
||||
* For all other keys — those NOT rescaled by `patchHiDPI` — the value in UIDefaults is the raw logical integer from the
|
||||
* theme JSON, and `.dp` is correct. Do NOT use this extension for those (e.g. via [retrieveIntAsDp] or similar).
|
||||
*/
|
||||
public val Int.unscaledDp: Dp
|
||||
get() = JBUI.unscale(this).dp
|
||||
|
||||
+5
-4
@@ -8,7 +8,7 @@ import androidx.compose.ui.unit.takeOrElse
|
||||
import com.intellij.util.ui.JBUI
|
||||
import org.jetbrains.jewel.bridge.dp
|
||||
import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified
|
||||
import org.jetbrains.jewel.bridge.retrieveIntAsNonNegativeDpOrUnspecified
|
||||
import org.jetbrains.jewel.bridge.retrieveUnscaledIntAsNonNegativeDpOrUnspecified
|
||||
import org.jetbrains.jewel.ui.component.styling.LazyTreeIcons
|
||||
import org.jetbrains.jewel.ui.component.styling.LazyTreeMetrics
|
||||
import org.jetbrains.jewel.ui.component.styling.LazyTreeStyle
|
||||
@@ -34,8 +34,8 @@ internal fun readLazyTreeStyle(): LazyTreeStyle {
|
||||
backgroundSelectedActive = selectedElementBackground,
|
||||
)
|
||||
|
||||
val leftIndent = retrieveIntAsNonNegativeDpOrUnspecified("Tree.leftChildIndent").takeOrElse { 7.dp }
|
||||
val rightIndent = retrieveIntAsNonNegativeDpOrUnspecified("Tree.rightChildIndent").takeOrElse { 11.dp }
|
||||
val leftIndent = retrieveUnscaledIntAsNonNegativeDpOrUnspecified("Tree.leftChildIndent").takeOrElse { 7.dp }
|
||||
val rightIndent = retrieveUnscaledIntAsNonNegativeDpOrUnspecified("Tree.rightChildIndent").takeOrElse { 11.dp }
|
||||
|
||||
return LazyTreeStyle(
|
||||
colors = itemColors,
|
||||
@@ -49,7 +49,8 @@ internal fun readLazyTreeStyle(): LazyTreeStyle {
|
||||
selectionBackgroundCornerSize = CornerSize(JBUI.CurrentTheme.Tree.ARC.dp / 2),
|
||||
iconTextGap = 2.dp,
|
||||
),
|
||||
elementMinHeight = retrieveIntAsNonNegativeDpOrUnspecified("Tree.rowHeight").takeOrElse { 24.dp },
|
||||
elementMinHeight =
|
||||
retrieveUnscaledIntAsNonNegativeDpOrUnspecified("Tree.rowHeight").takeOrElse { 24.dp },
|
||||
chevronContentGap = 2.dp, // See com.intellij.ui.tree.ui.ClassicPainter.GAP
|
||||
),
|
||||
icons =
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified
|
||||
import org.jetbrains.jewel.bridge.retrieveIntAsNonNegativeDpOrUnspecified
|
||||
import org.jetbrains.jewel.bridge.safeValue
|
||||
import org.jetbrains.jewel.bridge.toPaddingValues
|
||||
import org.jetbrains.jewel.bridge.unscaledDp
|
||||
import org.jetbrains.jewel.ui.component.styling.MenuColors
|
||||
import org.jetbrains.jewel.ui.component.styling.MenuIcons
|
||||
import org.jetbrains.jewel.ui.component.styling.MenuItemColors
|
||||
@@ -107,7 +108,7 @@ internal fun readMenuStyle(): MenuStyle {
|
||||
iconSize = 16.dp,
|
||||
minHeight =
|
||||
if (isNewUiTheme()) {
|
||||
JBUI.CurrentTheme.List.rowHeight().dp.safeValue()
|
||||
JBUI.CurrentTheme.List.rowHeight().unscaledDp.safeValue()
|
||||
} else {
|
||||
Dp.Unspecified
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ package org.jetbrains.jewel.ui.component
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.input.InputMode
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import javax.swing.KeyStroke
|
||||
import org.jetbrains.jewel.intui.standalone.menuShortcut.StandaloneShortcutProvider
|
||||
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package org.jetbrains.jewel.ui.component
|
||||
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isPopup
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
|
||||
import org.jetbrains.jewel.ui.component.interactions.performKeyPress
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class MenuSubmenuItemTest {
|
||||
@get:Rule val rule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun `clicking submenu item opens submenu`() {
|
||||
rule.setContent {
|
||||
IntUiTheme {
|
||||
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
|
||||
MenuSubmenuItem(
|
||||
showIcon = false,
|
||||
selected = false,
|
||||
submenu = { selectableItem(false, onClick = {}) { Text("Submenu") } },
|
||||
content = { Text("Parent") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// submenu not yet visible
|
||||
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertDoesNotExist()
|
||||
|
||||
rule.onNodeWithText("Parent").performClick()
|
||||
rule.waitForIdle()
|
||||
|
||||
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disabling while submenu is open closes the submenu`() {
|
||||
val enabled = mutableStateOf(true)
|
||||
|
||||
rule.setContent {
|
||||
IntUiTheme {
|
||||
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
|
||||
MenuSubmenuItem(
|
||||
showIcon = false,
|
||||
selected = false,
|
||||
enabled = enabled.value,
|
||||
submenu = { selectableItem(false, onClick = {}) { Text("Submenu") } },
|
||||
content = { Text("Parent") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rule.onNodeWithText("Parent").performClick()
|
||||
rule.waitForIdle()
|
||||
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertIsDisplayed()
|
||||
|
||||
enabled.value = false
|
||||
rule.waitForIdle()
|
||||
|
||||
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disabled items cannot be opened`() {
|
||||
rule.setContent {
|
||||
IntUiTheme {
|
||||
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
|
||||
MenuSubmenuItem(
|
||||
showIcon = false,
|
||||
selected = false,
|
||||
enabled = false,
|
||||
submenu = { selectableItem(selected = false, onClick = {}) { Text("Submenu") } },
|
||||
content = { Text("Parent") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rule.onNodeWithText("Parent").performClick()
|
||||
rule.waitForIdle()
|
||||
|
||||
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `right arrow opens submenu`() {
|
||||
rule.setContent {
|
||||
IntUiTheme {
|
||||
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
|
||||
MenuSubmenuItem(
|
||||
showIcon = false,
|
||||
selected = false,
|
||||
submenu = { selectableItem(selected = false, onClick = {}) { Text("Submenu") } },
|
||||
content = { Text("Parent") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rule.onNodeWithText("Parent").performSemanticsAction(SemanticsActions.RequestFocus)
|
||||
rule.waitForIdle()
|
||||
|
||||
rule.onNodeWithText("Parent").performKeyPress(Key.DirectionRight)
|
||||
rule.waitForIdle()
|
||||
|
||||
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -821,36 +822,18 @@ internal fun MenuItemBase(
|
||||
style: MenuStyle = JewelTheme.menuStyle,
|
||||
@Suppress("DEPRECATION") content: @Composable (itemState: MenuItemState) -> Unit,
|
||||
) {
|
||||
var itemState by
|
||||
remember(interactionSource) {
|
||||
@Suppress("DEPRECATION") mutableStateOf(MenuItemState.of(selected = selected, enabled = enabled))
|
||||
}
|
||||
|
||||
remember(enabled, selected) { itemState = itemState.copy(selected = selected, enabled = enabled) }
|
||||
val itemState by rememberMenuItemState(selected, enabled, interactionSource)
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is PressInteraction.Press -> itemState = itemState.copy(pressed = true)
|
||||
is PressInteraction.Cancel,
|
||||
is PressInteraction.Release -> itemState = itemState.copy(pressed = false)
|
||||
is HoverInteraction.Enter -> {
|
||||
itemState = itemState.copy(hovered = true)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
is HoverInteraction.Exit -> itemState = itemState.copy(hovered = false)
|
||||
is FocusInteraction.Focus -> itemState = itemState.copy(focused = true)
|
||||
is FocusInteraction.Unfocus -> itemState = itemState.copy(focused = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val menuController = LocalMenuController.current
|
||||
val localInputModeManager = LocalInputModeManager.current
|
||||
|
||||
LaunchedEffect(itemState.isHovered) {
|
||||
if (itemState.isHovered) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
@@ -872,52 +855,25 @@ internal fun MenuItemBase(
|
||||
onDispose {}
|
||||
}
|
||||
|
||||
val itemColors = style.colors.itemColors
|
||||
val itemMetrics = style.metrics.itemMetrics
|
||||
|
||||
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
|
||||
val updatedTextStyle = LocalTextStyle.current.copy(color = itemColors.contentFor(itemState).value)
|
||||
|
||||
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides itemColors.contentFor(itemState).value,
|
||||
LocalTextStyle provides updatedTextStyle,
|
||||
) {
|
||||
val backgroundColor by itemColors.backgroundFor(itemState)
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = itemMetrics.minHeight)
|
||||
.drawItemBackground(itemMetrics, backgroundColor)
|
||||
.padding(itemMetrics.contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (canShowIcon) {
|
||||
val iconModifier = Modifier.size(style.metrics.itemMetrics.iconSize)
|
||||
if (iconKey != null) {
|
||||
Icon(
|
||||
key = iconKey,
|
||||
contentDescription = null,
|
||||
modifier = iconModifier.thenIf(!enabled) { disabledAppearance() },
|
||||
)
|
||||
} else {
|
||||
Box(modifier = iconModifier)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.weight(1f, true)) { content(itemState) }
|
||||
|
||||
MenuItemLayout(
|
||||
itemState = itemState,
|
||||
style = style,
|
||||
iconKey = iconKey,
|
||||
canShowIcon = canShowIcon,
|
||||
enabled = enabled,
|
||||
trailingContent =
|
||||
if (canShowKeybinding) {
|
||||
Text(
|
||||
modifier = Modifier.padding(style.metrics.itemMetrics.keybindingsPadding),
|
||||
text = keybindingHint,
|
||||
color = itemColors.keybindingTintFor(itemState).value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
@Suppress("DEPRECATION") // Not really deprecated, MenuItemColors will be made internal
|
||||
Text(
|
||||
modifier = Modifier.padding(style.metrics.itemMetrics.keybindingsPadding),
|
||||
text = keybindingHint,
|
||||
color = style.colors.itemColors.keybindingTintFor(itemState).value,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
content = { content(itemState) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -939,8 +895,11 @@ public fun MenuSubmenuItem(
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@ApiStatus.Internal
|
||||
@InternalJewelApi
|
||||
@Composable
|
||||
internal fun MenuSubmenuItem(
|
||||
public fun MenuSubmenuItem(
|
||||
showIcon: Boolean,
|
||||
selected: Boolean,
|
||||
submenu: MenuScope.() -> Unit,
|
||||
@@ -951,43 +910,17 @@ internal fun MenuSubmenuItem(
|
||||
style: MenuStyle = JewelTheme.menuStyle,
|
||||
@Suppress("DEPRECATION") content: @Composable (itemState: MenuItemState) -> Unit,
|
||||
) {
|
||||
var itemState by
|
||||
remember(interactionSource) {
|
||||
@Suppress("DEPRECATION") mutableStateOf(MenuItemState.of(selected = selected, enabled = enabled))
|
||||
}
|
||||
|
||||
remember(enabled) { itemState = itemState.copy(selected = false, enabled = enabled) }
|
||||
|
||||
var itemState by rememberMenuItemState(selected, enabled, interactionSource)
|
||||
// When the item becomes disabled, close any open submenu
|
||||
remember(enabled) { if (!enabled) itemState = itemState.copy(selected = false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is PressInteraction.Press -> itemState = itemState.copy(pressed = true)
|
||||
is PressInteraction.Cancel,
|
||||
is PressInteraction.Release -> itemState = itemState.copy(pressed = false)
|
||||
|
||||
is HoverInteraction.Enter -> itemState = itemState.copy(hovered = true)
|
||||
is HoverInteraction.Exit -> itemState = itemState.copy(hovered = false)
|
||||
is FocusInteraction.Focus -> itemState = itemState.copy(focused = true)
|
||||
is FocusInteraction.Unfocus -> itemState = itemState.copy(focused = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remember(selected) { itemState = itemState.copy(selected = selected) }
|
||||
LaunchedEffect(itemState.isSelected) { if (itemState.isSelected) focusRequester.requestFocus() }
|
||||
|
||||
val itemColors = style.colors.itemColors
|
||||
val menuMetrics = style.metrics
|
||||
|
||||
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
|
||||
val backgroundColor by itemColors.backgroundFor(itemState)
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.drawItemBackground(menuMetrics.itemMetrics, backgroundColor)
|
||||
.focusRequester(focusRequester)
|
||||
.clickable(
|
||||
onClick = { itemState = itemState.copy(selected = !itemState.isSelected) },
|
||||
@@ -1004,32 +937,23 @@ internal fun MenuSubmenuItem(
|
||||
}
|
||||
}
|
||||
) {
|
||||
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
|
||||
CompositionLocalProvider(LocalContentColor provides itemColors.contentFor(itemState).value) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(menuMetrics.itemMetrics.contentPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
if (showIcon) {
|
||||
if (iconKey != null) {
|
||||
Icon(key = iconKey, contentDescription = null)
|
||||
} else {
|
||||
Box(Modifier.size(style.metrics.itemMetrics.iconSize))
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.weight(1f)) { content(itemState) }
|
||||
|
||||
MenuItemLayout(
|
||||
itemState = itemState,
|
||||
style = style,
|
||||
iconKey = iconKey,
|
||||
canShowIcon = showIcon,
|
||||
trailingContent = {
|
||||
@Suppress("DEPRECATION") // Not really deprecated, MenuItemColors will be made internal
|
||||
Icon(
|
||||
key = style.icons.submenuChevron,
|
||||
tint = itemColors.iconTintFor(itemState).value,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(style.metrics.itemMetrics.iconSize),
|
||||
tint = style.colors.itemColors.iconTintFor(itemState).value,
|
||||
hint = Stateful(itemState),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = { content(itemState) },
|
||||
)
|
||||
|
||||
if (itemState.isSelected) {
|
||||
Submenu(
|
||||
@@ -1048,6 +972,60 @@ internal fun MenuSubmenuItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION") // Not really deprecated, MenuItemColors be made internal
|
||||
@Composable
|
||||
internal fun MenuItemLayout(
|
||||
itemState: MenuItemState,
|
||||
modifier: Modifier = Modifier,
|
||||
style: MenuStyle = JewelTheme.menuStyle,
|
||||
iconKey: IconKey? = null,
|
||||
canShowIcon: Boolean = true,
|
||||
enabled: Boolean = true,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val itemColors = style.colors.itemColors
|
||||
val itemMetrics = style.metrics.itemMetrics
|
||||
|
||||
val contentColor = itemColors.contentFor(itemState).value
|
||||
val backgroundColor by itemColors.backgroundFor(itemState)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides contentColor,
|
||||
LocalTextStyle provides LocalTextStyle.current.copy(color = contentColor),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = itemMetrics.minHeight)
|
||||
.drawItemBackground(itemMetrics, backgroundColor)
|
||||
.padding(itemMetrics.contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (canShowIcon) {
|
||||
val iconModifier = Modifier.size(itemMetrics.iconSize)
|
||||
if (iconKey != null) {
|
||||
Icon(
|
||||
key = iconKey,
|
||||
contentDescription = null,
|
||||
modifier = iconModifier.thenIf(!enabled) { disabledAppearance() },
|
||||
)
|
||||
} else {
|
||||
Box(modifier = iconModifier)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) { content() }
|
||||
|
||||
if (trailingContent != null) {
|
||||
trailingContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.drawItemBackground(itemMetrics: MenuItemMetrics, backgroundColor: Color) = drawBehind {
|
||||
val cornerSizePx = itemMetrics.selectionCornerSize.toPx(size, density = this)
|
||||
val cornerRadius = CornerRadius(cornerSizePx, cornerSizePx)
|
||||
@@ -1201,3 +1179,32 @@ public value class MenuItemState(public val state: ULong) : SelectableComponentS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION") // Not really deprecated, MenuItemState will be made internal
|
||||
@Composable
|
||||
internal fun rememberMenuItemState(
|
||||
selected: Boolean,
|
||||
enabled: Boolean,
|
||||
interactionSource: MutableInteractionSource,
|
||||
): MutableState<MenuItemState> {
|
||||
val itemState =
|
||||
remember(interactionSource) { mutableStateOf(MenuItemState.of(selected = selected, enabled = enabled)) }
|
||||
|
||||
remember(enabled, selected) { itemState.value = itemState.value.copy(selected = selected, enabled = enabled) }
|
||||
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is PressInteraction.Press -> itemState.value = itemState.value.copy(pressed = true)
|
||||
is PressInteraction.Cancel,
|
||||
is PressInteraction.Release -> itemState.value = itemState.value.copy(pressed = false)
|
||||
is HoverInteraction.Enter -> itemState.value = itemState.value.copy(hovered = true)
|
||||
is HoverInteraction.Exit -> itemState.value = itemState.value.copy(hovered = false)
|
||||
is FocusInteraction.Focus -> itemState.value = itemState.value.copy(focused = true)
|
||||
is FocusInteraction.Unfocus -> itemState.value = itemState.value.copy(focused = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return itemState
|
||||
}
|
||||
|
||||
+3
@@ -76,6 +76,9 @@ jewel.swing.badge.green=Green
|
||||
jewel.swing.badge.green.secondary=Green secondary
|
||||
jewel.swing.badge.purple.secondary=Purple secondary
|
||||
jewel.swing.badge.gray.secondary=Gray secondary
|
||||
jewel.section.menu.label=Menus:
|
||||
jewel.section.menu.swing.button=Open IntelliJ Platform Menu
|
||||
jewel.section.menu.swing.submenu=Submenu
|
||||
|
||||
compose.sandbox=Compose Sandbox
|
||||
compose.sandbox.show.automatically.on.project.open=Show automatically on project open
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -30,6 +32,11 @@ import com.intellij.devkit.compose.DevkitComposeBundle
|
||||
import com.intellij.devkit.compose.icons.DevkitComposeIcons
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||
import com.intellij.openapi.editor.colors.EditorFontType
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
@@ -63,23 +70,24 @@ import org.jetbrains.jewel.ui.component.DefaultSplitButton
|
||||
import org.jetbrains.jewel.ui.component.EditableListComboBox
|
||||
import org.jetbrains.jewel.ui.component.ExternalLink
|
||||
import org.jetbrains.jewel.ui.component.Icon
|
||||
import org.jetbrains.jewel.ui.component.InformationDefaultBanner
|
||||
import org.jetbrains.jewel.ui.component.InlineInformationBanner
|
||||
import org.jetbrains.jewel.ui.component.ListComboBox
|
||||
import org.jetbrains.jewel.ui.component.OutlinedButton
|
||||
import org.jetbrains.jewel.ui.component.OutlinedSplitButton
|
||||
import org.jetbrains.jewel.ui.component.PopupMenu
|
||||
import org.jetbrains.jewel.ui.component.Text
|
||||
import org.jetbrains.jewel.ui.component.TextArea
|
||||
import org.jetbrains.jewel.ui.component.TextField
|
||||
import org.jetbrains.jewel.ui.component.separator
|
||||
import org.jetbrains.jewel.ui.disabledAppearance
|
||||
import org.jetbrains.jewel.ui.icons.AllIconsKeys
|
||||
import org.jetbrains.jewel.ui.theme.badgeStyle
|
||||
import org.jetbrains.jewel.ui.theme.textAreaStyle
|
||||
import org.jetbrains.jewel.ui.typography
|
||||
import java.awt.Dimension
|
||||
import java.awt.MouseInfo
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JPanel
|
||||
import org.jetbrains.jewel.ui.component.Badge as JewelBadge
|
||||
@@ -108,6 +116,8 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
|
||||
separator()
|
||||
badgesRow()
|
||||
separator()
|
||||
menusRow()
|
||||
separator()
|
||||
}
|
||||
.apply {
|
||||
border = JBUI.Borders.empty(0, 10)
|
||||
@@ -713,6 +723,79 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
|
||||
}.layout(RowLayout.PARENT_GRID)
|
||||
}
|
||||
|
||||
private fun Panel.menusRow() {
|
||||
row(DevkitComposeBundle.message("jewel.section.menu.label")) {
|
||||
button(DevkitComposeBundle.message("jewel.section.menu.swing.button")) {
|
||||
val actionGroup = createIntelliJMenuActionGroup()
|
||||
val popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.POPUP, actionGroup)
|
||||
val button = it.source as? JComponent
|
||||
button?.let { comp ->
|
||||
popupMenu.setTargetComponent(comp)
|
||||
popupMenu.component.show(comp, 0, comp.height)
|
||||
}
|
||||
}
|
||||
.align(AlignY.CENTER)
|
||||
|
||||
compose {
|
||||
var showJewelMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier.height(150.dp)) {
|
||||
OutlinedButton(onClick = { showJewelMenu = true }, modifier = Modifier.align(Alignment.Center)) {
|
||||
Text("Open Jewel Menu")
|
||||
}
|
||||
|
||||
if (showJewelMenu) {
|
||||
PopupMenu(
|
||||
onDismissRequest = { showJewelMenu = false; true },
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
selectableItem(selected = false, onClick = {}) { Text("Menu Item 1") }
|
||||
selectableItem(selected = false, onClick = {}) { Text("Menu Item 2") }
|
||||
selectableItem(selected = false, onClick = {}) { Text("Menu Item 3") }
|
||||
separator()
|
||||
submenu(submenu = {
|
||||
selectableItem(selected = false, onClick = {}) { Text("Submenu Item 1") }
|
||||
selectableItem(selected = false, onClick = {}) { Text("Submenu Item 2") }
|
||||
selectableItem(selected = false, onClick = {}) { Text("Submenu Item 3") }
|
||||
}) {
|
||||
Text("Submenu")
|
||||
}
|
||||
separator()
|
||||
selectableItem(selected = false, onClick = {}) { Text("Menu Item 4") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}.align(AlignY.CENTER)
|
||||
}
|
||||
.layout(RowLayout.PARENT_GRID)
|
||||
}
|
||||
|
||||
private fun createIntelliJMenuActionGroup(): DefaultActionGroup {
|
||||
return DefaultActionGroup().apply {
|
||||
add(createSimpleAction("Menu Item 1"))
|
||||
add(createSimpleAction("Menu Item 2"))
|
||||
add(createSimpleAction("Menu Item 3"))
|
||||
addSeparator()
|
||||
|
||||
val submenu = DefaultActionGroup(DevkitComposeBundle.message("jewel.section.menu.swing.submenu"), true)
|
||||
submenu.add(createSimpleAction("Submenu Item 1"))
|
||||
submenu.add(createSimpleAction("Submenu Item 2"))
|
||||
submenu.add(createSimpleAction("Submenu Item 3"))
|
||||
add(submenu)
|
||||
|
||||
addSeparator()
|
||||
add(createSimpleAction("Menu Item 4"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSimpleAction(text: String): AnAction {
|
||||
return object : AnAction(text) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
// No-op for demo purposes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PaddingValues.vertical(): Dp = calculateTopPadding() + calculateBottomPadding()
|
||||
|
||||
private fun PaddingValues.horizontal(layoutDirection: LayoutDirection): Dp =
|
||||
|
||||
Reference in New Issue
Block a user