diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PickerRow.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PickerRow.kt new file mode 100644 index 00000000000..e5e24fb567d --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PickerRow.kt @@ -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() + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt index 552718a9bfb..67bcdd6349c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt @@ -79,6 +79,7 @@ class ModePicker : JBLabel() { .setCancelKeyEnabled(true) .setResizable(false) .setMovable(false) + .setAutoselectOnMouseMove(true) .setItemChosenCallback { value -> selected = value refresh() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt index 370776340fe..fbb3ba6c667 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt @@ -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() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt index 4b662a7c95e..1b9e249389a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt @@ -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, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index bad5ee3503f..fea99069103 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/mode/ModePickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/mode/ModePickerTest.kt index 059b5762b1b..03802dd2565 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/mode/ModePickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/mode/ModePickerTest.kt @@ -105,4 +105,5 @@ class ModePickerTest : BasePlatformTestCase() { cell.getListCellRendererComponent(list, code, 1, false, false) assertFalse(renderer.badgeVisible()) } + } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt index d484da761db..56dac1a9553 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt @@ -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()) 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()) + 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 + } }