From 8cbe482c149a9d971e586487ee6d06ef2f41a763 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 3 Jun 2026 12:46:45 +0200 Subject: [PATCH] fix: dont loose selection when opening&closing cluster list (#23869) Signed-off-by: Andre Dietisheim --- .../gateway/view/ui/FilteringComboBox.kt | 42 +++++++++++++++---- .../gateway/view/ui/FilteringComboBoxTest.kt | 26 ++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBox.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBox.kt index 512312cb..1a13ade2 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBox.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBox.kt @@ -13,8 +13,6 @@ package com.redhat.devtools.gateway.view.ui import com.intellij.openapi.util.Key import java.awt.event.ActionListener -import java.awt.event.FocusAdapter -import java.awt.event.FocusEvent import java.awt.event.ItemEvent import java.awt.event.KeyAdapter import java.awt.event.KeyEvent @@ -26,6 +24,7 @@ import javax.swing.text.JTextComponent object FilteringComboBox { private val popupOpened = PopupOpened() + private val popupSelection = PopupSelection() fun create( toString: (T?) -> String = { it.toString() }, @@ -53,6 +52,7 @@ object FilteringComboBox { toString: (T) -> String ): PopupMenuListener = object : PopupMenuListener { override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) { + popupSelection.backup(comboBox) val allItems = comboBox.filteringModel().getAllItems() val editorText = getEditorComponent(comboBox)?.text ?: "" val visible = if (popupOpened.isProgrammatic(comboBox)) { @@ -65,8 +65,13 @@ object FilteringComboBox { popupOpened.reset(comboBox) } - override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {} - override fun popupMenuCanceled(e: PopupMenuEvent?) {} + override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) { + popupSelection.restore(comboBox) + } + + override fun popupMenuCanceled(e: PopupMenuEvent?) { + popupSelection.restore(comboBox) + } } private fun onListItemRendered(toString: (T) -> String): ListCellRenderer = @@ -121,7 +126,7 @@ object FilteringComboBox { ) { popupOpened.setProgrammatic(true, comboBox) val editor = getEditorComponent(comboBox) - val selection = Selection(comboBox.editor.editorComponent as? JTextComponent).backup() + val JTextSelection = JTextSelection(comboBox.editor.editorComponent as? JTextComponent).backup() val currentTextInEditor = editor?.text ?: "" comboBox.filteringModel().showOnly(items) @@ -129,7 +134,8 @@ object FilteringComboBox { editor?.text = currentTextInEditor // Restore the selection after the model and text are set - selection.restore() + JTextSelection.restore() + popupSelection.backup(comboBox) comboBox.selectedIndex = -1 // prevent pre-selection if (items.isNotEmpty() && !comboBox.isPopupVisible) { @@ -224,12 +230,12 @@ object FilteringComboBox { } } - private class Selection(private val component: JTextComponent?) { + private class JTextSelection(private val component: JTextComponent?) { private var selectionStart = -1 private var selectionEnd = -1 - fun backup(): Selection { + fun backup(): JTextSelection { if (component == null) { return this } @@ -247,6 +253,26 @@ object FilteringComboBox { } } + private class PopupSelection { + + private val key = Key.create("${FilteringComboBox.javaClass.name}Selection") + + fun backup(comboBox: JComboBox<*>) { + if (comboBox.getClientProperty(key) == null) { + // only backup once (multiple selection changes happen in normal flow) + comboBox.putClientProperty(key, comboBox.selectedItem) + } + } + + fun restore(comboBox: JComboBox<*>) { + val backedUp = comboBox.getClientProperty(key) ?: return + comboBox.putClientProperty(key, null) + if (comboBox.selectedIndex == -1) { + comboBox.selectedItem = backedUp + } + } + } + private class PopupOpened { private val key = Key.create("isPopupProgrammatic") diff --git a/src/test/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBoxTest.kt b/src/test/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBoxTest.kt index b9df645a..af0f1ac7 100644 --- a/src/test/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBoxTest.kt +++ b/src/test/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBoxTest.kt @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.BeforeEach import java.awt.event.KeyEvent import javax.swing.* +import javax.swing.event.PopupMenuEvent class FilteringComboBoxTest { @@ -552,6 +553,31 @@ class FilteringComboBoxTest { assertThat(comboBox.itemCount).isEqualTo(0) } + @Test + fun `#popupMenu does preserve selection when popup is closed without choosing`() { + // given + items.forEach { comboBox.addItem(it) } + SwingUtilities.invokeAndWait { + comboBox.selectedItem = items[2] + } + Thread.sleep(100) + val popupEvent = PopupMenuEvent(comboBox) + + // when - open popup then close without selecting (same as clicking outside) + SwingUtilities.invokeAndWait { + comboBox.popupMenuListeners.forEach { listener -> + listener.popupMenuWillBecomeVisible(popupEvent) + listener.popupMenuWillBecomeInvisible(popupEvent) + } + } + + // then + SwingUtilities.invokeAndWait { + assertThat(comboBox.selectedItem).isEqualTo(items[2]) + assertThat(comboBox.selectedIndex).isGreaterThanOrEqualTo(0) + } + } + @Test fun `#PopupOpened does handle programmatic popup opening vs user opening`() { // given