diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt index ae6e084..0d38dd6 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt @@ -1,7 +1,11 @@ package com.chylex.intellij.keyboardmaster.feature.vimNavigation +import com.intellij.ui.popup.WizardPopup import javax.swing.JComponent internal interface ComponentHolder { val component: JComponent + + val popup: WizardPopup? + get() = null } diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt index 779d569..804815f 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt @@ -23,10 +23,10 @@ internal interface KeyStrokeNode<T> { } fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> { - val keyStroke = when (keyEvent.id) { - KeyEvent.KEY_TYPED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv()) - KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false) - else -> return this + val keyStroke = when { + keyEvent.keyChar != KeyEvent.CHAR_UNDEFINED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv()) + keyEvent.id == KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false) + else -> return this } return keys[keyStroke] ?: this @@ -71,7 +71,7 @@ internal interface KeyStrokeNode<T> { } 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) if (extra != null) { @@ -82,7 +82,11 @@ internal interface KeyStrokeNode<T> { 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) }) } } } diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/PopupInterceptor.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/PopupInterceptor.kt new file mode 100644 index 0000000..cd2ad22 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/PopupInterceptor.kt @@ -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?) {} +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt index 0285869..957cbf2 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt @@ -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.VimTreeNavigation import com.intellij.openapi.application.ApplicationManager +import com.intellij.ui.UiInterceptors import com.intellij.util.ui.StartupUiUtil import java.awt.AWTEvent import java.awt.event.FocusEvent @@ -17,7 +18,10 @@ object VimNavigation { var isEnabled = false 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) { diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt index c996695..f3f11a0 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt @@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicBoolean import javax.swing.JComponent 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 { private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java) private val EXTRA_SHORTCUTS = setOf( @@ -35,16 +35,21 @@ internal class VimNavigationDispatcher<T : JComponent>(override val component: T var isSearching = AtomicBoolean(false) init { - registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(rootNode, EXTRA_SHORTCUTS), component, DISPOSABLE) + registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(getAllKeyStrokes()), component, DISPOSABLE) - SpeedSearchSupply.getSupply(component, true)?.addChangeListener { - if (it.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME && it.oldValue != null && it.newValue == null) { + val speedSearch = SpeedSearchSupply.getSupply(component, true) + speedSearch?.addChangeListener { + if (it.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME && !speedSearch.isPopupActive) { 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 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 } } - override fun getActionUpdateThread(): ActionUpdateThread { + final override fun getActionUpdateThread(): ActionUpdateThread { return ActionUpdateThread.BGT } } diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt index 2c25481..cb4feb5 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt @@ -23,7 +23,7 @@ internal object VimCommonNavigation { KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"), KeyStroke.getKeyStroke('r') to IdeaAction("SynchronizeCurrentFile"), KeyStroke.getKeyStroke('R') to IdeaAction("Synchronize"), - KeyStroke.getKeyStroke('q') to CloseParentToolWindow(), + KeyStroke.getKeyStroke('q') to CloseParentPopupOrToolWindow(), 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) { + val popup = holder.popup + if (popup != null) { + popup.cancel() + return + } + val project = actionEvent.project ?: return val toolWindowId = holder.component.getParentToolWindowId() ?: return ToolWindowManagerEx.getInstanceEx(project).hideToolWindow(toolWindowId, true) diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt index 07e3e96..ab3d63d 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt @@ -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.Parent 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.putUserData 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.KeyStroke @@ -28,8 +35,53 @@ internal object VimListNavigation { ) 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)) } } + + 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) } + } + } + } + } }