IJPL-58454 Implement selecting the main menu bar on Windows by pressing Alt

GitOrigin-RevId: 3058dfa79b65cbd98815eddf543fb51892186668
This commit is contained in:
Dmitry Drobotov
2026-02-14 10:49:13 +00:00
committed by intellij-monorepo-bot
parent 530092ee14
commit 5cf014eaf2
4 changed files with 169 additions and 6 deletions
@@ -70,5 +70,11 @@
defaultValue="60"
description="The interval between too-small window size checks on Windows and attempts to restore the size. Set to zero to disable"
/>
<registryKey
key="ide.windows.main.menu.focus.on.alt"
defaultValue="false"
description="Enable Alt key to focus the main menu bar on Windows"
restartRequired="true"
/>
</extensions>
</idea-plugin>
@@ -1287,7 +1287,9 @@ private class WindowsAltSuppressor : IdeEventQueue.NonLockedEventDispatcher {
if (uiSettings == null ||
!SystemInfoRt.isWindows ||
!Registry.`is`("actionSystem.win.suppressAlt", true) ||
!(uiSettings.hideToolStripes || uiSettings.presentationMode)) {
// Need to handle Alt to show hidden tool stripes by double Alt or to focus the main menu
!(uiSettings.hideToolStripes || uiSettings.presentationMode) &&
!Registry.`is`("ide.windows.main.menu.focus.on.alt", false)) {
return false
}
@@ -30,6 +30,8 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.accessibility.AccessibleContext
import javax.accessibility.AccessibleState
import javax.swing.JComponent
import javax.swing.JFrame
import javax.swing.JLayeredPane
@@ -102,7 +104,7 @@ internal class ExpandableMenu(
ideMenuHelper.updateUI()
}
fun switchState(actionMenuToShow: ActionMenu? = null, itemInd: Int = 0) {
fun switchState(actionMenuToShow: ActionMenu? = null, itemInd: Int = 0, selectOnlyHeaderMenu: Boolean = false) {
if (isShowing() && actionMenuToShow == null) {
hideExpandedMenuBar()
return
@@ -130,11 +132,11 @@ internal class ExpandableMenu(
// The first menu usage has no selection in the menu. Fix it by invokeLater
ApplicationManager.getApplication().invokeLater {
selectMenu(actionMenu = actionMenuToShow, itemInd = itemInd)
selectMenu(actionMenu = actionMenuToShow, itemInd = itemInd, selectOnlyHeaderMenu = selectOnlyHeaderMenu)
}
}
private fun selectMenu(actionMenu: ActionMenu? = null, itemInd: Int) {
private fun selectMenu(actionMenu: ActionMenu? = null, itemInd: Int, selectOnlyHeaderMenu: Boolean = false) {
var menu = ideMenu.getMenu(itemInd)
if (actionMenu != null) {
for (m in ideMenu.rootMenuItems) {
@@ -148,8 +150,11 @@ internal class ExpandableMenu(
menu ?: return
val subElements = menu.popupMenu.subElements
if (subElements.isEmpty()) {
if (subElements.isEmpty() || selectOnlyHeaderMenu) {
MenuSelectionManager.defaultManager().selectedPath = arrayOf<MenuElement>(ideMenu, menu)
ApplicationManager.getApplication().invokeLater {
menu.accessibleContext.firePropertyChange(AccessibleContext.ACCESSIBLE_STATE_PROPERTY, null, AccessibleState.SELECTED)
}
}
else {
MenuSelectionManager.defaultManager().selectedPath = arrayOf<MenuElement>(ideMenu, menu, menu.popupMenu, subElements[0])
@@ -5,13 +5,42 @@ import com.intellij.ide.ui.UISettings
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.impl.ActionMenu
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.wm.impl.IdeFrameDecorator
import com.intellij.openapi.wm.impl.headertoolbar.MainMenuWithButton
import com.intellij.openapi.wm.impl.headertoolbar.MergedMainMenu
import com.intellij.ui.AppUIUtil
import com.intellij.util.concurrency.ThreadingAssertions
import com.intellij.util.ui.accessibility.ScreenReader
import kotlinx.coroutines.job
import java.awt.AWTEvent
import java.awt.KeyEventPostProcessor
import java.awt.KeyboardFocusManager
import java.awt.Toolkit
import java.awt.event.AWTEventListener
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import javax.accessibility.AccessibleContext
import javax.accessibility.AccessibleState
import javax.swing.MenuElement
import javax.swing.MenuSelectionManager
import javax.swing.SwingUtilities
internal class JMenuBasedIdeMenuBarHelper(flavor: IdeMenuFlavor, menuBar: IdeJMenuBar.JMenuBarImpl) : IdeMenuBarHelper(flavor, menuBar) {
init {
if (WinAltKeyProcessor.isEnabled()) {
WinAltKeyProcessor.register(this)
menuBar.coroutineScope.coroutineContext.job.invokeOnCompletion {
ApplicationManager.getApplication().invokeLater {
WinAltKeyProcessor.unregister(this)
}
}
}
}
override fun isUpdateForbidden() = MenuSelectionManager.defaultManager().selectedPath.isNotEmpty()
override suspend fun doUpdateVisibleActions(newVisibleActions: List<ActionGroup>, forceRebuild: Boolean) {
@@ -68,4 +97,125 @@ internal class JMenuBasedIdeMenuBarHelper(flavor: IdeMenuFlavor, menuBar: IdeJMe
menuBar.frame.validate()
}
}
}
internal fun activateMenu() {
val menuBarComponent = menuBar.component
if (menuBarComponent is MergedMainMenu) {
val mainMenuWithButton = menuBarComponent.parent as? MainMenuWithButton
if (mainMenuWithButton?.mainMenuButton?.button?.isShowing == true) {
mainMenuWithButton.mainMenuButton.expandableMenu?.switchState(selectOnlyHeaderMenu = true)
return
}
}
val ideMenuBar = menuBarComponent as? IdeJMenuBar ?: return
val firstMenu = ideMenuBar.rootMenuItems.firstOrNull() ?: return
MenuSelectionManager.defaultManager().selectedPath = arrayOf<MenuElement>(menuBarComponent, firstMenu)
ApplicationManager.getApplication().invokeLater {
firstMenu.accessibleContext.firePropertyChange(AccessibleContext.ACCESSIBLE_STATE_PROPERTY, null, AccessibleState.SELECTED)
}
}
internal fun shouldProcessAltRelease(e: KeyEvent): Boolean {
val menuBarComponent = menuBar.component
val visibleComponent = if (menuBarComponent.isShowing) menuBarComponent
else {
val button = ((menuBarComponent as? MergedMainMenu)?.parent as? MainMenuWithButton)?.mainMenuButton?.button
if (button?.isShowing == true) button else return false
}
val window = SwingUtilities.getWindowAncestor(visibleComponent) ?: return false
if (SwingUtilities.getWindowAncestor(e.component) != window && e.component != window) return false
if (MenuSelectionManager.defaultManager().selectedPath.isNotEmpty()) return false
if (ScreenReader.isActive() && AppUIUtil.isInFullScreen(window)) return false
return true
}
}
internal object WinAltKeyProcessor : KeyEventPostProcessor, AWTEventListener {
private val helpers = mutableListOf<JMenuBasedIdeMenuBarHelper>()
private var registered = false
private var altPressed = false
private var altPressedOnly = false
@JvmStatic
fun isEnabled(): Boolean = SystemInfoRt.isWindows && Registry.`is`("ide.windows.main.menu.focus.on.alt", false)
fun register(helper: JMenuBasedIdeMenuBarHelper) {
helpers.add(helper)
if (!registered) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventPostProcessor(this)
Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_EVENT_MASK)
registered = true
}
}
fun unregister(helper: JMenuBasedIdeMenuBarHelper) {
helpers.remove(helper)
if (helpers.isEmpty() && registered) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventPostProcessor(this)
Toolkit.getDefaultToolkit().removeAWTEventListener(this)
registered = false
}
}
override fun eventDispatched(event: AWTEvent) {
if (!isEnabled()) {
return
}
// Prevent focusing the menu when hold Alt + mouse click shortcuts are used
if (event is MouseEvent && event.id == MouseEvent.MOUSE_PRESSED && altPressed) {
altPressedOnly = false
}
}
override fun postProcessKeyEvent(e: KeyEvent): Boolean {
if (!isEnabled()) {
return false
}
if (e.keyCode != KeyEvent.VK_ALT) {
if (altPressed && e.id == KeyEvent.KEY_PRESSED) {
altPressedOnly = false
}
return false
}
val settings = UISettings.instanceOrNull
// To avoid conflicts with the double Alt shortcut to show hidden tool stripes
if (settings == null || settings.hideToolStripes || settings.presentationMode) {
return false
}
when (e.id) {
KeyEvent.KEY_PRESSED -> {
if (!altPressed) {
altPressed = true
altPressedOnly = true
}
}
KeyEvent.KEY_RELEASED -> {
if (altPressed && altPressedOnly
&& e.modifiersEx and (InputEvent.SHIFT_DOWN_MASK or InputEvent.CTRL_DOWN_MASK or InputEvent.META_DOWN_MASK) == 0) {
for (helper in helpers) {
if (helper.shouldProcessAltRelease(e)) {
ApplicationManager.getApplication().invokeLater {
helper.activateMenu()
}
break
}
}
}
altPressed = false
altPressedOnly = false
}
}
return false
}
}