Skip to content

Commit e0ba5c2

Browse files
committed
feat(gui): improve keyboard accessibility for action buttons #352
- Replace JButton with ActionButton for better keyboard support - Add focus listener and ENTER key action to buttons- Implement compound border for focused state- Improve code structure and readability
1 parent 6b40d4c commit e0ba5c2

File tree

1 file changed

+83
-21
lines changed

1 file changed

+83
-21
lines changed

core/src/main/kotlin/cc/unitmesh/devti/gui/planner/PlannerResultSummary.kt

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
11
package cc.unitmesh.devti.gui.planner
22

3+
import cc.unitmesh.devti.inline.AutoDevLineBorder
34
import cc.unitmesh.devti.util.relativePath
45
import com.intellij.icons.AllIcons
6+
import com.intellij.openapi.actionSystem.ActionToolbar
7+
import com.intellij.openapi.actionSystem.AnAction
8+
import com.intellij.openapi.actionSystem.AnActionEvent
9+
import com.intellij.openapi.actionSystem.impl.ActionButton
510
import com.intellij.openapi.fileEditor.FileEditorManager
611
import com.intellij.openapi.project.Project
712
import com.intellij.openapi.vcs.changes.Change
813
import com.intellij.openapi.vcs.changes.ui.RollbackWorker
914
import com.intellij.ui.HyperlinkLabel
15+
import com.intellij.ui.JBColor
1016
import com.intellij.ui.components.JBLabel
1117
import com.intellij.ui.components.JBScrollPane
1218
import com.intellij.util.ui.JBUI
1319
import com.intellij.util.ui.UIUtil
20+
import io.modelcontextprotocol.kotlin.sdk.UnknownReference
21+
import org.jetbrains.annotations.NotNull
1422
import java.awt.BorderLayout
1523
import java.awt.FlowLayout
1624
import java.awt.GridLayout
17-
import javax.swing.Box
18-
import javax.swing.BoxLayout
19-
import javax.swing.Icon
20-
import javax.swing.JButton
21-
import javax.swing.JPanel
25+
import java.awt.event.ActionEvent
26+
import java.awt.event.FocusEvent
27+
import java.awt.event.FocusListener
28+
import java.awt.event.KeyEvent
29+
import java.beans.PropertyChangeEvent
30+
import java.beans.PropertyChangeListener
31+
import javax.swing.*
32+
import javax.swing.border.Border
2233
import javax.swing.event.HyperlinkEvent
2334
import javax.swing.event.HyperlinkListener
2435

@@ -189,10 +200,10 @@ class PlannerResultSummary(
189200
}
190201
})
191202
}
192-
203+
193204
add(fileLabel)
194205
add(Box.createHorizontalStrut(5))
195-
206+
196207
val pathLabel = JBLabel(filePath).apply {
197208
foreground = UIUtil.getLabelDisabledForeground()
198209
toolTipText = filePath
@@ -205,15 +216,14 @@ class PlannerResultSummary(
205216
preferredSize = JBUI.size(100, preferredSize.height)
206217
maximumSize = JBUI.size(Int.MAX_VALUE, preferredSize.height)
207218
}
208-
219+
209220
add(pathLabel)
210221
add(Box.createHorizontalGlue()) // This pushes the action buttons to the right
211-
212-
// Action buttons
222+
213223
val actionsPanel = JPanel().apply {
214224
isOpaque = false
215225
layout = BoxLayout(this, BoxLayout.X_AXIS)
216-
226+
217227
val viewButton = createActionButton(
218228
AllIcons.Actions.Preview,
219229
"View changes"
@@ -228,7 +238,7 @@ class PlannerResultSummary(
228238
add(Box.createHorizontalStrut(2))
229239
add(discardButton)
230240
}
231-
241+
232242
add(actionsPanel)
233243
}
234244
}
@@ -237,14 +247,66 @@ class PlannerResultSummary(
237247
icon: Icon,
238248
tooltip: String,
239249
action: () -> Unit
240-
): JButton = JButton().apply {
241-
this.icon = icon
242-
toolTipText = tooltip
243-
isBorderPainted = false
244-
isContentAreaFilled = false
245-
isFocusPainted = false
246-
margin = JBUI.emptyInsets()
247-
preferredSize = JBUI.size(20, 20)
248-
addActionListener { action() }
250+
): JComponent {
251+
val anAction = object : AnAction(tooltip, tooltip, icon) {
252+
override fun actionPerformed(e: AnActionEvent) {
253+
action()
254+
}
255+
}
256+
return KeyboardAccessibleActionButton(anAction)
257+
}
258+
259+
private class KeyboardAccessibleActionButton(@NotNull action: AnAction) : ActionButton(
260+
action,
261+
action.templatePresentation.clone(),
262+
"unknown",
263+
ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE
264+
) {
265+
init {
266+
isFocusable = true
267+
inputMap.put(KeyStroke.getKeyStroke("ENTER"), "executeAction")
268+
actionMap.put("executeAction", object : AbstractAction() {
269+
override fun actionPerformed(e: ActionEvent) {
270+
click()
271+
}
272+
})
273+
val focusListener = AccessibleFocusListener()
274+
addPropertyChangeListener("border", focusListener)
275+
addFocusListener(focusListener)
276+
}
277+
278+
override fun processKeyEvent(e: KeyEvent?) {
279+
if (e != null && e.keyCode == KeyEvent.VK_ENTER && e.id == KeyEvent.KEY_PRESSED) {
280+
click()
281+
} else {
282+
super.processKeyEvent(e)
283+
}
284+
}
285+
286+
private inner class AccessibleFocusListener : FocusListener, PropertyChangeListener {
287+
private var originalBorder: Border? = null
288+
private var focusedBorder: Border? = null
289+
290+
override fun focusGained(e: FocusEvent?) {
291+
val insideBorder = AutoDevLineBorder(JBColor.namedColor("Focus.borderColor", JBColor.BLUE), 1, true, 4)
292+
focusedBorder = BorderFactory.createCompoundBorder(originalBorder, insideBorder)
293+
border = focusedBorder
294+
repaint()
295+
}
296+
297+
override fun focusLost(e: FocusEvent?) {
298+
border = originalBorder
299+
repaint()
300+
}
301+
302+
override fun propertyChange(evt: PropertyChangeEvent?) {
303+
if (originalBorder == null && evt?.propertyName == "border") {
304+
val newBorder = evt.newValue as? Border
305+
if (newBorder != null && newBorder != focusedBorder) {
306+
originalBorder = newBorder
307+
}
308+
}
309+
}
310+
}
249311
}
250312
}

0 commit comments

Comments
 (0)