fix(jetbrains): align picker popup selection

This commit is contained in:
kirillk
2026-05-02 23:38:36 -04:00
parent 77e515ec8e
commit 16548c7488
7 changed files with 89 additions and 16 deletions
@@ -0,0 +1,34 @@
package ai.kilocode.client.session.ui
import com.intellij.openapi.ui.popup.util.PopupUtil
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.popup.list.SelectablePanel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import java.awt.BorderLayout
import javax.swing.JComponent
import javax.swing.JList
internal class PickerRow : SelectablePanel() {
init {
layout = BorderLayout()
isOpaque = true
}
fun setContent(component: JComponent) {
accessibleContextProvider = component
add(component, BorderLayout.CENTER)
}
fun update(list: JList<*>, selected: Boolean, focused: Boolean) {
background = list.background
selectionColor = if (selected) UIUtil.getListBackground(true, focused) else null
if (ExperimentalUI.isNewUI()) {
PopupUtil.configListRendererFlexibleHeight(this)
return
}
border = JBUI.Borders.empty()
selectionArc = 0
selectionInsets = JBUI.emptyInsets()
}
}
@@ -79,6 +79,7 @@ class ModePicker : JBLabel() {
.setCancelKeyEnabled(true)
.setResizable(false)
.setMovable(false)
.setAutoselectOnMouseMove(true)
.setItemChosenCallback { value ->
selected = value
refresh()
@@ -1,6 +1,7 @@
package ai.kilocode.client.session.ui.mode
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.PickerRow
import ai.kilocode.client.ui.UiStyle
import com.intellij.icons.AllIcons
import com.intellij.ui.RoundedLineBorder
@@ -51,10 +52,11 @@ internal class ModePickerRenderer(
UiStyle.Components.transparent(this)
}
private val row = JPanel(BorderLayout())
private val wrap = PickerRow()
init {
isOpaque = true
row.isOpaque = true
UiStyle.Components.transparent(this)
UiStyle.Components.transparent(row)
(row.layout as BorderLayout).hgap = UiStyle.Gap.inline()
row.border = JBUI.Borders.empty(
UiStyle.Space.MD,
@@ -66,7 +68,8 @@ internal class ModePickerRenderer(
body.add(desc, BorderLayout.CENTER)
row.add(icon, BorderLayout.WEST)
row.add(body, BorderLayout.CENTER)
add(row, BorderLayout.CENTER)
wrap.setContent(row)
add(wrap, BorderLayout.CENTER)
}
override fun getListCellRendererComponent(
@@ -78,12 +81,11 @@ internal class ModePickerRenderer(
): JPanel {
val focus = selected || list.hasFocus() || focused
val fg = UIUtil.getListForeground(selected, focus)
val bg = if (selected) UIUtil.getListBackground(true, focus) else list.background
val weak = if (selected) fg else UiStyle.Colors.weak()
val warn = if (selected) fg else UiStyle.Colors.warning()
background = list.background
row.background = bg
wrap.update(list, selected, focus)
title.clear()
title.append(value.display, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
desc.clear()
@@ -119,6 +119,7 @@ class ModelPicker : JBLabel() {
emptyText.text = KiloBundle.message("model.picker.no.matches")
background = popupBackground
border = JBUI.Borders.empty(PopupUtil.getListInsets(false, false))
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
}
list.cellRenderer = ModelPickerRenderer(
model = model,
@@ -1,9 +1,11 @@
package ai.kilocode.client.session.ui.model
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.PickerRow
import ai.kilocode.client.ui.UiStyle
import com.intellij.icons.AllIcons
import com.intellij.ui.CollectionListModel
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.GroupHeaderSeparator
import com.intellij.ui.JBColor
import com.intellij.ui.SimpleColoredComponent
@@ -41,11 +43,20 @@ internal class ModelPickerRenderer(
fun isFavoriteClick(list: JList<*>, bounds: Rectangle, point: Point): Boolean {
val width = JBUI.scale(FAVORITE_CLICK_AREA_WIDTH)
return if (list.componentOrientation.isLeftToRight) {
point.x >= bounds.x + bounds.width - width
} else {
point.x <= bounds.x + width
val inset = favoriteInset(list)
if (list.componentOrientation.isLeftToRight) {
val right = bounds.x + bounds.width - inset
return point.x in (right - width)..right
}
val left = bounds.x + inset
return point.x in left..(left + width)
}
private fun favoriteInset(list: JList<*>): Int {
if (!ExperimentalUI.isNewUI()) return 0
val inner = JBUI.CurrentTheme.Popup.Selection.innerInsets()
val edge = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get()
return edge + if (list.componentOrientation.isLeftToRight) inner.right else inner.left
}
}
@@ -77,11 +88,12 @@ internal class ModelPickerRenderer(
add(head, BorderLayout.CENTER)
add(star, BorderLayout.EAST)
}
private val wrap = PickerRow()
init {
isOpaque = true
top.isOpaque = true
row.isOpaque = true
UiStyle.Components.transparent(row)
UiStyle.Components.transparent(check)
UiStyle.Components.transparent(title)
UiStyle.Components.transparent(head)
@@ -93,8 +105,9 @@ internal class ModelPickerRenderer(
UiStyle.Space.MD,
UiStyle.Space.LG + UiStyle.Space.SM,
)
wrap.setContent(row)
add(top, BorderLayout.NORTH)
add(row, BorderLayout.CENTER)
add(wrap, BorderLayout.CENTER)
}
override fun getListCellRendererComponent(
@@ -103,17 +116,16 @@ internal class ModelPickerRenderer(
index: Int,
selected: Boolean,
focused: Boolean,
): Component {
): JPanel {
val focus = selected || list.hasFocus() || focused
val fg = UIUtil.getListForeground(selected, focus)
val bg = if (selected) UIUtil.getListBackground(true, focus) else list.background
val weak = if (selected) fg else UiStyle.Colors.weak()
val current = model.items.getOrNull(index)
val section = if (current === value) modelPickerSectionTitle(model.items, index) else null
background = list.background
top.background = list.background
row.background = bg
wrap.update(list, selected, focus)
sep.caption = section
sep.setHideLine(index == 0)
top.isVisible = section != null
@@ -105,4 +105,5 @@ class ModePickerTest : BasePlatformTestCase() {
cell.getListCellRendererComponent(list, code, 1, false, false)
assertFalse(renderer.badgeVisible())
}
}
@@ -3,9 +3,11 @@ package ai.kilocode.client.session.ui.model
import ai.kilocode.rpc.dto.ModelSelectionDto
import com.intellij.icons.AllIcons
import com.intellij.ui.CollectionListModel
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.components.JBList
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.ui.EmptyIcon
import com.intellij.util.ui.JBUI
import java.awt.ComponentOrientation
import java.awt.Point
import java.awt.Rectangle
@@ -228,15 +230,28 @@ class ModelPickerTest : BasePlatformTestCase() {
fun `test favorite click area uses trailing edge in both orientations`() {
val list = JBList(listOf<ModelPickerRow>())
val bounds = Rectangle(10, 0, 100, 20)
val inset = favoriteInset(list)
assertTrue(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(100, 10)))
assertTrue(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(100 - inset, 10)))
assertFalse(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(20, 10)))
list.componentOrientation = ComponentOrientation.RIGHT_TO_LEFT
assertTrue(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(20, 10)))
val rtl = favoriteInset(list)
assertTrue(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(20 + rtl, 10)))
assertFalse(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(100, 10)))
}
fun `test favorite click area ignores popup selection inset outside row content`() {
val list = JBList(listOf<ModelPickerRow>())
val bounds = Rectangle(10, 0, 100, 20)
val inset = favoriteInset(list)
if (inset <= 0) return
assertFalse(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(bounds.x + bounds.width - 1, 10)))
assertTrue(ModelPickerRenderer.isFavoriteClick(list, bounds, Point(bounds.x + bounds.width - inset, 10)))
}
fun `test renderer shows free badge for free model`() {
val row = ModelPickerRow(ModelPicker.Item("auto", "Auto", "kilo", "Kilo", free = true), "Kilo", false)
val model = CollectionListModel(listOf(row))
@@ -256,4 +271,11 @@ class ModelPickerTest : BasePlatformTestCase() {
index: Double? = null,
free: Boolean = false,
) = ModelPicker.Item(id, display, provider, name, index, free = free)
private fun favoriteInset(list: JBList<*>): Int {
if (!ExperimentalUI.isNewUI()) return 0
val inner = JBUI.CurrentTheme.Popup.Selection.innerInsets()
val edge = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get()
return edge + if (list.componentOrientation.isLeftToRight) inner.right else inner.left
}
}