1
0
mirror of https://github.com/chylex/IntelliJ-Keyboard-Master.git synced 2025-04-30 05:34:05 +02:00

Implement vim-style navigation for list-based popups

This commit is contained in:
chylex 2024-05-04 20:39:12 +02:00
parent 6e38809ef2
commit d3989a0e87
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
7 changed files with 111 additions and 17 deletions

View File

@ -1,7 +1,11 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.intellij.ui.popup.WizardPopup
import javax.swing.JComponent import javax.swing.JComponent
internal interface ComponentHolder { internal interface ComponentHolder {
val component: JComponent val component: JComponent
val popup: WizardPopup?
get() = null
} }

View File

@ -23,10 +23,10 @@ internal interface KeyStrokeNode<T> {
} }
fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> { fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> {
val keyStroke = when (keyEvent.id) { val keyStroke = when {
KeyEvent.KEY_TYPED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv()) keyEvent.keyChar != KeyEvent.CHAR_UNDEFINED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv())
KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false) keyEvent.id == KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false)
else -> return this else -> return this
} }
return keys[keyStroke] ?: this return keys[keyStroke] ?: this
@ -71,7 +71,7 @@ internal interface KeyStrokeNode<T> {
} }
companion object { companion object {
fun <T> getAllShortcuts(root: Parent<T>, extra: Set<KeyStroke>? = null): CustomShortcutSet { fun <T> getAllKeyStrokes(root: Parent<T>, extra: Set<KeyStroke>? = null): Set<KeyStroke> {
val allKeyStrokes = HashSet(root.allKeyStrokes) val allKeyStrokes = HashSet(root.allKeyStrokes)
if (extra != null) { if (extra != null) {
@ -82,7 +82,11 @@ internal interface KeyStrokeNode<T> {
allKeyStrokes.add(KeyStroke.getKeyStroke(c)) allKeyStrokes.add(KeyStroke.getKeyStroke(c))
} }
return CustomShortcutSet(*allKeyStrokes.map2Array { KeyboardShortcut(it, null) }) return allKeyStrokes
}
fun getAllShortcuts(keyStrokes: Set<KeyStroke>): CustomShortcutSet {
return CustomShortcutSet(*keyStrokes.map2Array { KeyboardShortcut(it, null) })
} }
} }
} }

View File

@ -0,0 +1,19 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimListNavigation
import com.intellij.ui.UiInterceptors.PersistentUiInterceptor
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.popup.AbstractPopup
import com.intellij.ui.popup.list.ListPopupImpl
internal object PopupInterceptor : PersistentUiInterceptor<AbstractPopup>(AbstractPopup::class.java) {
override fun shouldIntercept(component: AbstractPopup): Boolean {
if (component is ListPopupImpl) {
VimListNavigation.install(component.list, component)
}
return false
}
override fun doIntercept(component: AbstractPopup, owner: RelativePoint?) {}
}

View File

@ -5,6 +5,7 @@ import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimLi
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTableNavigation import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTableNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTreeNavigation import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTreeNavigation
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.ui.UiInterceptors
import com.intellij.util.ui.StartupUiUtil import com.intellij.util.ui.StartupUiUtil
import java.awt.AWTEvent import java.awt.AWTEvent
import java.awt.event.FocusEvent import java.awt.event.FocusEvent
@ -17,7 +18,10 @@ object VimNavigation {
var isEnabled = false var isEnabled = false
fun register() { fun register() {
StartupUiUtil.addAwtListener(::handleEvent, AWTEvent.FOCUS_EVENT_MASK, ApplicationManager.getApplication().getService(PluginDisposableService::class.java)) val disposable = ApplicationManager.getApplication().getService(PluginDisposableService::class.java)
StartupUiUtil.addAwtListener(::handleEvent, AWTEvent.FOCUS_EVENT_MASK, disposable)
UiInterceptors.registerPersistent(disposable, PopupInterceptor)
} }
private fun handleEvent(event: AWTEvent) { private fun handleEvent(event: AWTEvent) {

View File

@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.KeyStroke import javax.swing.KeyStroke
internal class VimNavigationDispatcher<T : JComponent>(override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>) : DumbAwareAction(), ComponentHolder { internal open class VimNavigationDispatcher<T : JComponent>(final override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>) : DumbAwareAction(), ComponentHolder {
companion object { companion object {
private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java) private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java)
private val EXTRA_SHORTCUTS = setOf( private val EXTRA_SHORTCUTS = setOf(
@ -35,16 +35,21 @@ internal class VimNavigationDispatcher<T : JComponent>(override val component: T
var isSearching = AtomicBoolean(false) var isSearching = AtomicBoolean(false)
init { init {
registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(rootNode, EXTRA_SHORTCUTS), component, DISPOSABLE) registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(getAllKeyStrokes()), component, DISPOSABLE)
SpeedSearchSupply.getSupply(component, true)?.addChangeListener { val speedSearch = SpeedSearchSupply.getSupply(component, true)
if (it.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME && it.oldValue != null && it.newValue == null) { speedSearch?.addChangeListener {
if (it.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME && !speedSearch.isPopupActive) {
isSearching.set(false) isSearching.set(false)
} }
} }
} }
override fun actionPerformed(e: AnActionEvent) { protected fun getAllKeyStrokes(): Set<KeyStroke> {
return KeyStrokeNode.getAllKeyStrokes(rootNode, EXTRA_SHORTCUTS)
}
final override fun actionPerformed(e: AnActionEvent) {
val keyEvent = e.inputEvent as? KeyEvent ?: return val keyEvent = e.inputEvent as? KeyEvent ?: return
if (keyEvent.id == KeyEvent.KEY_PRESSED && handleSpecialKeyPress(keyEvent, e.dataContext)) { if (keyEvent.id == KeyEvent.KEY_PRESSED && handleSpecialKeyPress(keyEvent, e.dataContext)) {
@ -89,11 +94,11 @@ internal class VimNavigationDispatcher<T : JComponent>(override val component: T
} }
} }
override fun update(e: AnActionEvent) { final override fun update(e: AnActionEvent) {
e.presentation.isEnabled = !isSearching.get() || e.inputEvent.let { it is KeyEvent && it.id == KeyEvent.KEY_PRESSED && it.keyCode == KeyEvent.VK_ENTER } e.presentation.isEnabled = !isSearching.get() || e.inputEvent.let { it is KeyEvent && it.id == KeyEvent.KEY_PRESSED && it.keyCode == KeyEvent.VK_ENTER }
} }
override fun getActionUpdateThread(): ActionUpdateThread { final override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT return ActionUpdateThread.BGT
} }
} }

View File

@ -23,7 +23,7 @@ internal object VimCommonNavigation {
KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"), KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"),
KeyStroke.getKeyStroke('r') to IdeaAction("SynchronizeCurrentFile"), KeyStroke.getKeyStroke('r') to IdeaAction("SynchronizeCurrentFile"),
KeyStroke.getKeyStroke('R') to IdeaAction("Synchronize"), KeyStroke.getKeyStroke('R') to IdeaAction("Synchronize"),
KeyStroke.getKeyStroke('q') to CloseParentToolWindow(), KeyStroke.getKeyStroke('q') to CloseParentPopupOrToolWindow(),
KeyStroke.getKeyStroke('/') to StartSearch(), KeyStroke.getKeyStroke('/') to StartSearch(),
) )
) )
@ -40,8 +40,14 @@ internal object VimCommonNavigation {
} }
} }
private class CloseParentToolWindow<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> { private class CloseParentPopupOrToolWindow<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> {
override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) { override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val popup = holder.popup
if (popup != null) {
popup.cancel()
return
}
val project = actionEvent.project ?: return val project = actionEvent.project ?: return
val toolWindowId = holder.component.getParentToolWindowId() ?: return val toolWindowId = holder.component.getParentToolWindowId() ?: return
ToolWindowManagerEx.getInstanceEx(project).hideToolWindow(toolWindowId, true) ToolWindowManagerEx.getInstanceEx(project).hideToolWindow(toolWindowId, true)

View File

@ -3,9 +3,16 @@ package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.getUserData import com.intellij.openapi.ui.getUserData
import com.intellij.openapi.ui.putUserData import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.ui.popup.WizardPopup
import com.intellij.ui.speedSearch.SpeedSearch
import com.intellij.ui.speedSearch.SpeedSearchSupply
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import javax.swing.AbstractAction
import javax.swing.JList import javax.swing.JList
import javax.swing.KeyStroke import javax.swing.KeyStroke
@ -28,8 +35,53 @@ internal object VimListNavigation {
) )
fun install(component: JList<*>) { fun install(component: JList<*>) {
if (component.getUserData(KEY) == null) { if (component.getUserData(KEY) == null && component.javaClass.enclosingClass.let { it == null || !WizardPopup::class.java.isAssignableFrom(it) }) {
component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE)) component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
} }
} }
fun install(component: JList<*>, popup: WizardPopup) {
if (component.getUserData(KEY) == null) {
component.putUserData(KEY, VimPopupListNavigationDispatcher(component, popup))
}
}
@Suppress("serial")
private class VimPopupListNavigationDispatcher(component: JList<*>, override val popup: WizardPopup) : VimNavigationDispatcher<JList<*>>(component, ROOT_NODE) {
init {
val speedSearch = SpeedSearchSupply.getSupply(component, true) as? SpeedSearch
if (speedSearch != null) {
installSpeedSearch(speedSearch, popup)
}
}
private fun installSpeedSearch(speedSearch: SpeedSearch, popup: WizardPopup) {
val pauseAction = PauseSpeedSearchAction(this, speedSearch)
for (keyStroke in getAllKeyStrokes()) {
if (keyStroke.keyEventType != KeyEvent.KEY_TYPED) {
continue
}
val keyCode = KeyEvent.getExtendedKeyCodeForChar(keyStroke.keyChar.code)
if (keyCode != KeyEvent.VK_UNDEFINED) {
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(keyCode, 0), pauseAction)
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(keyCode, KeyEvent.SHIFT_DOWN_MASK), pauseAction)
}
}
// WizardPopup only checks key codes against its input map, but key codes may be undefined for some characters.
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(KeyEvent.CHAR_UNDEFINED, 0), pauseAction)
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(KeyEvent.CHAR_UNDEFINED, KeyEvent.SHIFT_DOWN_MASK), pauseAction)
}
private class PauseSpeedSearchAction(private val dispatcher: VimNavigationDispatcher<JList<*>>, private val speedSearch: SpeedSearch) : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
if (!dispatcher.isSearching.get()) {
speedSearch.setEnabled(false)
ApplicationManager.getApplication().invokeLater { speedSearch.setEnabled(true) }
}
}
}
}
} }