mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
fix(jetbrains): keep settings row active during popups
This commit is contained in:
+6
-7
@@ -10,6 +10,7 @@ import ai.kilocode.client.settings.base.settingsListCellBounds
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.ui.SimpleListCellRenderer
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
@@ -38,18 +39,16 @@ internal sealed interface LevelChoice {
|
||||
* cell; tests substitute a picker that resolves a choice directly.
|
||||
*/
|
||||
internal fun interface LevelPicker {
|
||||
fun show(anchor: JComponent, at: Point, choices: List<LevelChoice>, choose: (LevelChoice) -> Unit)
|
||||
fun popup(choices: List<LevelChoice>, choose: (LevelChoice) -> Unit): JBPopup?
|
||||
}
|
||||
|
||||
internal object PopupLevelPicker : LevelPicker {
|
||||
override fun show(anchor: JComponent, at: Point, choices: List<LevelChoice>, choose: (LevelChoice) -> Unit) {
|
||||
override fun popup(choices: List<LevelChoice>, choose: (LevelChoice) -> Unit): JBPopup =
|
||||
JBPopupFactory.getInstance()
|
||||
.createPopupChooserBuilder(choices)
|
||||
.setRenderer(SimpleListCellRenderer.create("") { levelChoiceLabel(it) })
|
||||
.setItemChosenCallback(choose)
|
||||
.createPopup()
|
||||
.show(RelativePoint(anchor, at))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun levelChoiceLabel(choice: LevelChoice): String = when (choice) {
|
||||
@@ -142,9 +141,9 @@ internal class SettingsInlineList(
|
||||
val item = item(key) ?: return
|
||||
val idx = index(key) ?: return
|
||||
val bounds = settingsListCellBounds(view.list, idx, idx == view.list.selectedIndex)[LEVEL_CELL] ?: return
|
||||
picker.show(view.list, Point(bounds.x, bounds.y + bounds.height), choices(item.row)) { choice ->
|
||||
choose(key, choice)
|
||||
}
|
||||
val popup = picker.popup(choices(item.row)) { choice -> choose(key, choice) } ?: return
|
||||
trackPopup(popup)
|
||||
popup.show(RelativePoint(view.list, Point(bounds.x, bounds.y + bounds.height)))
|
||||
}
|
||||
|
||||
private fun item(key: String): PermissionItem? {
|
||||
|
||||
+7
@@ -8,6 +8,7 @@ import com.intellij.openapi.actionSystem.ActionToolbar
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.SearchTextField
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
@@ -62,6 +63,12 @@ internal abstract class SettingsInlineListPanel(
|
||||
view.filter(query)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
protected fun trackPopup(popup: JBPopup) {
|
||||
checkEdt()
|
||||
view.trackPopup(popup)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun setItems(items: List<SettingsListItem>, enabled: Boolean) {
|
||||
checkEdt()
|
||||
|
||||
+5
-1
@@ -68,7 +68,7 @@ internal class SettingsListRenderer(
|
||||
selected: Boolean,
|
||||
focused: Boolean,
|
||||
): JPanel {
|
||||
val active = selected && list.hasFocus()
|
||||
val active = selected && (list.hasFocus() || (list as? SettingsListActive)?.active() == true)
|
||||
val fg = UIUtil.getListForeground(active, active || focused)
|
||||
val weak = if (active) fg else UiStyle.Colors.weak()
|
||||
val current = model.items.getOrNull(index)
|
||||
@@ -132,6 +132,10 @@ internal class SettingsListRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
internal interface SettingsListActive {
|
||||
fun active(): Boolean
|
||||
}
|
||||
|
||||
internal class SettingsListActionCell : JBLabel() {
|
||||
var cellId: String = ""
|
||||
private set
|
||||
|
||||
+29
-12
@@ -2,11 +2,13 @@ package ai.kilocode.client.settings.base
|
||||
|
||||
import ai.kilocode.client.session.ui.model.ModelSearch
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.openapi.ui.popup.JBPopupListener
|
||||
import com.intellij.openapi.ui.popup.LightweightWindowEvent
|
||||
import com.intellij.ui.CollectionListModel
|
||||
import com.intellij.ui.ScrollingUtil
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.xml.util.XmlStringUtil
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.FocusAdapter
|
||||
@@ -24,17 +26,8 @@ internal class SettingsListView(
|
||||
private val onCell: (String, String) -> Unit,
|
||||
) : BaseContentPanel() {
|
||||
private val model = CollectionListModel<SettingsListItem>()
|
||||
internal val list = object : JBList<SettingsListItem>(model) {
|
||||
override fun getToolTipText(event: MouseEvent): String? {
|
||||
if (!cfg.description) return null
|
||||
val idx = locationToIndex(event.point)
|
||||
if (idx < 0) return null
|
||||
val bounds = getCellBounds(idx, idx) ?: return null
|
||||
if (!bounds.contains(event.point)) return null
|
||||
val note = model.getElementAt(idx).description?.takeIf { it.isNotBlank() } ?: return null
|
||||
val text = note.lines().joinToString("<br>") { XmlStringUtil.escapeString(it) }
|
||||
return XmlStringUtil.wrapInHtml(text)
|
||||
}
|
||||
internal val list: JBList<SettingsListItem> = object : JBList<SettingsListItem>(model), SettingsListActive {
|
||||
override fun active(): Boolean = popups > 0
|
||||
}.apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
setExpandableItemsEnabled(false)
|
||||
@@ -43,6 +36,7 @@ internal class SettingsListView(
|
||||
private var items = emptyList<SettingsListItem>()
|
||||
private var filter = ""
|
||||
private var press: Press? = null
|
||||
private var popups = 0
|
||||
internal var onSelect: (() -> Unit)? = null
|
||||
|
||||
fun setEmptyText(text: String) {
|
||||
@@ -136,6 +130,29 @@ internal class SettingsListView(
|
||||
list.repaint()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun trackPopup(popup: JBPopup) {
|
||||
checkEdt()
|
||||
var tracked = false
|
||||
fun activate() {
|
||||
if (tracked) return
|
||||
tracked = true
|
||||
popups++
|
||||
list.repaint()
|
||||
}
|
||||
popup.addListener(object : JBPopupListener {
|
||||
override fun beforeShown(event: LightweightWindowEvent) = activate()
|
||||
|
||||
override fun onClosed(event: LightweightWindowEvent) {
|
||||
if (!tracked) return
|
||||
tracked = false
|
||||
popups = maxOf(0, popups - 1)
|
||||
list.repaint()
|
||||
}
|
||||
})
|
||||
if (popup.isVisible) activate()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun filter(query: String) {
|
||||
checkEdt()
|
||||
|
||||
+4
-1
@@ -40,7 +40,10 @@ class AutoApproveSettingsUiTest : BasePlatformTestCase() {
|
||||
private lateinit var workspaces: KiloWorkspaceService
|
||||
private var ui: AutoApproveSettingsUi? = null
|
||||
private var pick: (List<LevelChoice>) -> LevelChoice = { it.first() }
|
||||
private val picker = LevelPicker { _, _, choices, choose -> choose(pick(choices)) }
|
||||
private val picker = LevelPicker { choices, choose ->
|
||||
choose(pick(choices))
|
||||
null
|
||||
}
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
+3
-6
@@ -4,6 +4,7 @@ import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.settings.base.SettingsListItem
|
||||
import ai.kilocode.client.settings.base.settingsListCellBounds
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.ui.UIUtil
|
||||
@@ -148,14 +149,10 @@ class SettingsInlineListTest : BasePlatformTestCase() {
|
||||
var offered: List<LevelChoice> = emptyList()
|
||||
private set
|
||||
|
||||
override fun show(
|
||||
anchor: JComponent,
|
||||
at: Point,
|
||||
choices: List<LevelChoice>,
|
||||
choose: (LevelChoice) -> Unit,
|
||||
) {
|
||||
override fun popup(choices: List<LevelChoice>, choose: (LevelChoice) -> Unit): JBPopup? {
|
||||
offered = choices
|
||||
choose(select(choices))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-20
@@ -19,7 +19,7 @@ import java.awt.event.MouseEvent
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class SettingsListViewTest : BasePlatformTestCase() {
|
||||
fun `test list owns formatted description tooltip`() {
|
||||
fun `test list does not duplicate description in tooltip`() {
|
||||
edt {
|
||||
val view = SettingsListView("Empty") { _, _ -> }
|
||||
val row = item("with", "Alpha", "Use <safe> text\nAcross lines")
|
||||
@@ -31,25 +31,7 @@ class SettingsListViewTest : BasePlatformTestCase() {
|
||||
val bounds = view.list.getCellBounds(0, 0)
|
||||
val tip = view.list.getToolTipText(event(view.list, Point(bounds.x + 4, bounds.y + 4)))
|
||||
|
||||
assertNotNull(tip)
|
||||
assertTrue(tip, tip!!.startsWith("<html>"))
|
||||
assertTrue(tip, tip.contains("Use <safe> text"))
|
||||
assertTrue(tip, tip.contains("<br>Across lines"))
|
||||
}
|
||||
}
|
||||
|
||||
fun `test list description tooltip ignores blank rows and outside points`() {
|
||||
edt {
|
||||
val view = SettingsListView("Empty") { _, _ -> }
|
||||
view.update(listOf(item("without", "Beta", null)))
|
||||
view.list.size = Dimension(320, 80)
|
||||
view.list.doLayout()
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
val bounds = view.list.getCellBounds(0, 0)
|
||||
|
||||
assertNull(view.list.getToolTipText(event(view.list, Point(bounds.x + 4, bounds.y + 4))))
|
||||
assertNull(view.list.getToolTipText(event(view.list, Point(4, bounds.y + bounds.height + 20))))
|
||||
assertNull(tip)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +234,22 @@ class SettingsListViewTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun `test active popup paints selected row as active without focus`() {
|
||||
edt {
|
||||
val row = item("with", "Alpha", "Description")
|
||||
val model = CollectionListModel<SettingsListItem>(listOf(row))
|
||||
val list = object : JBList<SettingsListItem>(model), SettingsListActive {
|
||||
override fun active(): Boolean = true
|
||||
}
|
||||
val renderer = SettingsListRenderer(model, SettingsListConfig.Equal)
|
||||
|
||||
renderer.getListCellRendererComponent(list, row, 0, true, false)
|
||||
|
||||
val desc = components(renderer).filterIsInstance<JBLabel>().single { it.text == "Description" }
|
||||
assertEquals(UIUtil.getListForeground(true, true), desc.foreground)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test preserve no scroll keeps scroll position after row change`() {
|
||||
edt {
|
||||
val view = SettingsListView("Empty") { _, _ -> }
|
||||
|
||||
Reference in New Issue
Block a user