diff --git a/platform/platform-api/resources/messages/IdeBundle.properties b/platform/platform-api/resources/messages/IdeBundle.properties
index 065c2deceea7..a6a1a881b87e 100644
--- a/platform/platform-api/resources/messages/IdeBundle.properties
+++ b/platform/platform-api/resources/messages/IdeBundle.properties
@@ -2763,6 +2763,8 @@ to.create.new.file.tooltip=To create a new file, press {0}. Alternatively
general.module.type.description=A basic project that allows working with separate files and compiling Java and Kotlin classes.
main.toolbar.menu.button=Main Menu
+main.toolbar.expanded.menu.close=Collapse Menu
+
notification.group.system.clipboard=System clipboard unavailable
notification.group.required.plugins=Required plugins not loaded
notification.group.language.plugin=Recommended language plugin available
diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.kt b/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.kt
index cc133899fb01..2bf74c776072 100644
--- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.kt
+++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.kt
@@ -137,7 +137,7 @@ open class IdeRootPane internal constructor(frame: JFrame,
MacToolbarFrameHeader(frame = frame, root = this)
}
else {
- ToolbarFrameHeader(frame = frame, ideMenu = IdeMenuBar.createMenuBar())
+ ToolbarFrameHeader(frame = frame)
}
}
else {
@@ -148,7 +148,7 @@ open class IdeRootPane internal constructor(frame: JFrame,
customFrameTitlePane = customFrameTitlePane,
selectedEditorFilePath = selectedEditorFilePath,
)
- layeredPane.add(customFrameTitlePane.getComponent(), (JLayeredPane.DEFAULT_LAYER - 2) as Any)
+ layeredPane.add(customFrameTitlePane.getComponent(), (JLayeredPane.DEFAULT_LAYER - 3) as Any)
}
else {
helper = UndecoratedHelper
diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ExpandableMenu.kt b/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ExpandableMenu.kt
new file mode 100644
index 000000000000..547c3b544416
--- /dev/null
+++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ExpandableMenu.kt
@@ -0,0 +1,138 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.openapi.wm.impl.customFrameDecorations.header.toolbar
+
+import com.intellij.icons.ExpUiIcons
+import com.intellij.ide.IdeBundle
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.project.DumbAwareAction
+import com.intellij.openapi.util.SystemInfoRt
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.openapi.wm.impl.IdeMenuBar
+import com.intellij.openapi.wm.impl.customFrameDecorations.header.FrameHeader
+import com.intellij.ui.dsl.builder.Align
+import com.intellij.ui.dsl.builder.AlignY
+import com.intellij.ui.dsl.builder.EmptySpacingConfiguration
+import com.intellij.ui.dsl.builder.panel
+import java.awt.*
+import java.awt.event.ComponentAdapter
+import java.awt.event.ComponentEvent
+import javax.swing.*
+
+private const val ALPHA = (255 * 0.8).toInt()
+
+internal class ExpandableMenu(private val frameHeader: FrameHeader) {
+
+ val ideMenu = IdeMenuBar.createMenuBar()
+ lateinit var headerContent: JComponent
+ private var expandedMenuBar: JPanel? = null
+ private val shadowComponent = ShadowComponent()
+ private val rootPane: JRootPane?
+ get() = SwingUtilities.getRootPane(headerContent)
+
+ init {
+ frameHeader.addComponentListener(object : ComponentAdapter() {
+ override fun componentResized(e: ComponentEvent?) {
+ updateBounds()
+ }
+ })
+ }
+
+ fun isEnabled(): Boolean {
+ return SystemInfoRt.isWindows && Registry.`is`("ide.windows.main.menu.expand.horizontal")
+ }
+
+ // todo compact mode
+ // todo hint under menu
+ fun switchState(actionToShow: AnAction? = null) {
+ if (expandedMenuBar != null && actionToShow == null) {
+ removeExpandedMenuBar()
+ return
+ }
+
+ removeExpandedMenuBar()
+ val layeredPane = rootPane?.layeredPane ?: return
+
+ expandedMenuBar = panel {
+ customizeSpacingConfiguration(EmptySpacingConfiguration()) {
+ row {
+ cell(wrapComponent(createMenuButton(CloseExpandedMenuAction())))
+ cell(wrapComponent(ideMenu)).align(AlignY.FILL)
+ cell(shadowComponent).align(Align.FILL)
+ }
+ }
+ }.apply { isOpaque = false }
+
+ updateBounds()
+ updateColor()
+ layeredPane.add(expandedMenuBar!!, (JLayeredPane.DEFAULT_LAYER - 2) as Any)
+
+ var menu = ideMenu.getMenu(0)
+ if (actionToShow != null) {
+ for (i in 0..ideMenu.menuCount - 1) {
+ val m = ideMenu.getMenu(i)
+ if (m.mnemonic == actionToShow.templatePresentation.mnemonic) {
+ menu = m
+ break
+ }
+ }
+ }
+ val subElements = menu.popupMenu.subElements
+ if (subElements.isNotEmpty()) {
+ MenuSelectionManager.defaultManager().selectedPath = arrayOf(ideMenu, menu, menu.popupMenu, subElements[0])
+ }
+ }
+
+ fun updateColor() {
+ val color = frameHeader.background
+ expandedMenuBar?.background = color
+ @Suppress("UseJBColor")
+ shadowComponent.background = Color(color.red, color.green, color.blue, ALPHA)
+ }
+
+ private fun wrapComponent(component: JComponent): JPanel {
+ return JPanel(BorderLayout()).apply {
+ add(component, BorderLayout.CENTER)
+ background = null
+ }
+ }
+
+ private fun updateBounds() {
+ expandedMenuBar?.let {
+ val insets = headerContent.insets
+ val location = SwingUtilities.convertPoint(headerContent, Point(insets.left, insets.top), rootPane ?: return)
+ it.bounds = Rectangle(location.x, location.y,
+ headerContent.width - insets.left - insets.right,
+ headerContent.height - insets.top - insets.bottom)
+ }
+ }
+
+ private fun removeExpandedMenuBar() {
+ if (expandedMenuBar != null) {
+ rootPane?.layeredPane?.remove(expandedMenuBar)
+ expandedMenuBar = null
+
+ rootPane?.repaint()
+ }
+ }
+
+ private inner class CloseExpandedMenuAction : DumbAwareAction(
+ IdeBundle.messagePointer("main.toolbar.expanded.menu.close"), ExpUiIcons.General.Close) {
+
+ override fun actionPerformed(e: AnActionEvent) {
+ removeExpandedMenuBar()
+ }
+ }
+
+ private class ShadowComponent : JComponent() {
+ init {
+ isOpaque = false
+ }
+
+ override fun paint(g: Graphics?) {
+ g ?: return
+ g.color = background
+ g.fillRect(0, 0, width, height)
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/MainMenuButton.kt b/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/MainMenuButton.kt
index 538284734b11..ccb725ba1112 100644
--- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/MainMenuButton.kt
+++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/MainMenuButton.kt
@@ -4,7 +4,7 @@ package com.intellij.openapi.wm.impl.customFrameDecorations.header.toolbar
import com.intellij.icons.ExpUiIcons
import com.intellij.ide.DataManager
import com.intellij.ide.IdeBundle
-import com.intellij.ide.ui.UISettings.Companion.getInstance
+import com.intellij.ide.ui.UISettings
import com.intellij.ide.ui.customization.CustomActionsSchema
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.*
@@ -30,11 +30,12 @@ import java.awt.event.ActionListener
import java.awt.event.HierarchyEvent
import java.awt.event.KeyEvent
import javax.swing.JComponent
+import javax.swing.JRootPane
import javax.swing.KeyStroke
import javax.swing.SwingUtilities
@ApiStatus.Internal
-internal class MainMenuButton {
+internal class MainMenuButton(private val expandableMenu: ExpandableMenu?) {
private val menuAction = ShowMenuAction()
private var disposable: Disposable? = null
@@ -42,7 +43,7 @@ internal class MainMenuButton {
private var registeredKeyStrokes = mutableListOf()
val button: ActionButton = createMenuButton(menuAction)
- var rootPane: JComponent? = null
+ var rootPane: JRootPane? = null
set(value) {
if (field !== value) {
uninstall()
@@ -132,26 +133,32 @@ internal class MainMenuButton {
IdeBundle.messagePointer("main.toolbar.menu.button"),
ExpUiIcons.General.WindowsMenu_20x20) {
- override fun actionPerformed(e: AnActionEvent) = showPopup(e.dataContext)
+ override fun actionPerformed(e: AnActionEvent) {
+ if (expandableMenu?.isEnabled() == true) {
+ expandableMenu.switchState()
+ } else {
+ showPopup(e.dataContext)
+ }
+ }
+ }
- fun showPopup(context: DataContext, actionToShow: AnAction? = null) {
- val mainMenu = getMainMenuGroup()
- val popup = JBPopupFactory.getInstance()
- .createActionGroupPopup(null, mainMenu, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true,
- ActionPlaces.MAIN_MENU)
- .apply { isShowSubmenuOnHover = true }
- .apply { setMinimumSize(Dimension(JBUI.CurrentTheme.CustomFrameDecorations.menuPopupMinWidth(), 0)) }
- as ListPopupImpl
- popup.showUnderneathOf(button)
+ fun showPopup(context: DataContext, actionToShow: AnAction? = null) {
+ val mainMenu = getMainMenuGroup()
+ val popup = JBPopupFactory.getInstance()
+ .createActionGroupPopup(null, mainMenu, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true,
+ ActionPlaces.MAIN_MENU)
+ .apply { isShowSubmenuOnHover = true }
+ .apply { setMinimumSize(Dimension(JBUI.CurrentTheme.CustomFrameDecorations.menuPopupMinWidth(), 0)) }
+ as ListPopupImpl
+ popup.showUnderneathOf(button)
- if (actionToShow != null) {
- for (listStep in popup.listStep.values) {
- listStep as PopupFactoryImpl.ActionItem
- if (listStep.action.unwrap() === actionToShow.unwrap()) {
- SwingUtilities.invokeLater {
- // Wait popup showing
- popup.selectAndExpandValue(listStep)
- }
+ if (actionToShow != null) {
+ for (listStep in popup.listStep.values) {
+ listStep as PopupFactoryImpl.ActionItem
+ if (listStep.action.unwrap() === actionToShow.unwrap()) {
+ SwingUtilities.invokeLater {
+ // Wait popup showing
+ popup.selectAndExpandValue(listStep)
}
}
}
@@ -161,18 +168,22 @@ internal class MainMenuButton {
private inner class ShowSubMenuAction(private val actionToShow: AnAction) : ActionListener {
override fun actionPerformed(e: ActionEvent?) {
- if (!getInstance().disableMnemonics) {
- val component = IdeFocusManager.getGlobalInstance().focusOwner ?: button
- menuAction.showPopup(DataManager.getInstance().getDataContext(component), actionToShow)
+ if (!UISettings.getInstance().disableMnemonics) {
+ if (expandableMenu?.isEnabled() == true) {
+ expandableMenu.switchState(actionToShow)
+ } else {
+ val component = IdeFocusManager.getGlobalInstance().focusOwner ?: button
+ showPopup(DataManager.getInstance().getDataContext(component), actionToShow)
+ }
}
}
}
}
-private fun createMenuButton(action: AnAction): ActionButton {
+internal fun createMenuButton(action: AnAction): ActionButton {
val button = object : ActionButton(action, PresentationFactory().getPresentation(action),
- ActionPlaces.MAIN_MENU, {ActionToolbar.experimentalToolbarMinimumButtonSize() }) {
+ ActionPlaces.MAIN_MENU, { ActionToolbar.experimentalToolbarMinimumButtonSize() }) {
override fun getDataContext(): DataContext {
return DataManager.getInstance().dataContextFromFocusAsync.blockingGet(200) ?: super.getDataContext()
}
@@ -182,7 +193,7 @@ private fun createMenuButton(action: AnAction): ActionButton {
return button
}
-private fun getMainMenuGroup(): ActionGroup {
+internal fun getMainMenuGroup(): ActionGroup {
val mainMenuGroup = CustomActionsSchema.getInstance().getCorrectedAction(IdeActions.GROUP_MAIN_MENU)
mainMenuGroup as ActionGroup
return DefaultActionGroup(
@@ -191,7 +202,8 @@ private fun getMainMenuGroup(): ActionGroup {
// Wrap action groups to force them to be popup groups,
// otherwise they end up as separate items in the burger menu (IDEA-294669).
ActionGroupPopupWrapper(child)
- } else {
+ }
+ else {
LOG.error("A top-level child of the main menu is not an action group: $child")
null
}
diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ToolbarFrameHeader.kt b/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ToolbarFrameHeader.kt
index 0d67483db23f..032820e2d6a4 100644
--- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ToolbarFrameHeader.kt
+++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ToolbarFrameHeader.kt
@@ -40,13 +40,14 @@ private enum class ShowMode {
MENU, TOOLBAR
}
-internal class ToolbarFrameHeader(frame: JFrame, ideMenu: IdeMenuBar) : FrameHeader(frame), UISettingsListener, ToolbarHolder, MainFrameCustomHeader {
- private val myMenuBar = ideMenu
+internal class ToolbarFrameHeader(frame: JFrame) : FrameHeader(frame), UISettingsListener, ToolbarHolder, MainFrameCustomHeader {
+ private val myMenuBar = IdeMenuBar.createMenuBar()
private val menuBarHeaderTitle = SimpleCustomDecorationPath(frame, true).apply {
isOpaque = false
}
private val menuBarContainer = createMenuBarContainer()
- private val mainMenuButton = MainMenuButton()
+ private val expandableMenu = ExpandableMenu(this)
+ private val mainMenuButton = MainMenuButton(expandableMenu)
private var toolbar : MainToolbar? = null
private val myToolbarPlaceholder = createToolbarPlaceholder()
private val myHeaderContent = createHeaderContent()
@@ -86,6 +87,7 @@ internal class ToolbarFrameHeader(frame: JFrame, ideMenu: IdeMenuBar) : FrameHea
if (isCompact) "PATH" else "TOOLBAR"
init {
+ expandableMenu.headerContent = myHeaderContent
layout = GridBagLayout()
val gb = GridBag().anchor(WEST)
@@ -168,6 +170,7 @@ internal class ToolbarFrameHeader(frame: JFrame, ideMenu: IdeMenuBar) : FrameHea
override fun updateMenuActions(forceRebuild: Boolean) {
myMenuBar.updateMenuActions(forceRebuild)
+ expandableMenu.ideMenu.updateMenuActions(forceRebuild)
}
override fun getComponent(): JComponent = this
@@ -222,6 +225,12 @@ internal class ToolbarFrameHeader(frame: JFrame, ideMenu: IdeMenuBar) : FrameHea
override fun getHeaderBackground(active: Boolean) = CustomFrameDecorations.mainToolbarBackground(active)
+ override fun updateActive() {
+ super.updateActive()
+
+ expandableMenu.updateColor()
+ }
+
private fun getElementRect(comp: Component, rectProcessor: ((Rectangle) -> Unit)? = null): RelativeRectangle {
val rect = Rectangle(comp.size)
rectProcessor?.invoke(rect)
@@ -230,16 +239,16 @@ internal class ToolbarFrameHeader(frame: JFrame, ideMenu: IdeMenuBar) : FrameHea
private fun createHeaderContent(): JPanel {
val res = NonOpaquePanel(CardLayout())
- res.border = JBUI.Borders.empty()
+ res.border = JBUI.Borders.emptyLeft(JBUI.scale(16))
val menuPnl = NonOpaquePanel(GridBagLayout()).apply {
val gb = GridBag().anchor(WEST).nextLine()
- add(menuBarContainer, gb.next().insetLeft(JBUI.scale(16)).fillCellVertically().weighty(1.0))
+ add(menuBarContainer, gb.next().fillCellVertically().weighty(1.0))
add(Box.createHorizontalGlue(), gb.next().weightx(1.0).fillCell())
}
val toolbarPnl = NonOpaquePanel(GridBagLayout()).apply {
val gb = GridBag().anchor(WEST).nextLine()
- add(mainMenuButton.button, gb.next().insetLeft(JBUI.scale(16)))
+ add(mainMenuButton.button, gb.next())
add(myToolbarPlaceholder, gb.next().weightx(1.0).fillCell())
}
diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/headertoolbar/MainToolbar.kt b/platform/platform-impl/src/com/intellij/openapi/wm/impl/headertoolbar/MainToolbar.kt
index 07002f397379..211baf3789e2 100644
--- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/headertoolbar/MainToolbar.kt
+++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/headertoolbar/MainToolbar.kt
@@ -44,7 +44,7 @@ internal class MainToolbar: JPanel(HorizontalLayout(10)) {
init {
background = JBUI.CurrentTheme.CustomFrameDecorations.mainToolbarBackground(true)
isOpaque = true
- mainMenuButton = if (IdeRootPane.isMenuButtonInToolbar) MainMenuButton() else null
+ mainMenuButton = if (IdeRootPane.isMenuButtonInToolbar) MainMenuButton(null) else null
ClientProperty.put(this, IdeBackgroundUtil.NO_BACKGROUND, true)
}
diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties
index 3571b2b6f7a2..66c20f2e0c46 100644
--- a/platform/util/resources/misc/registry.properties
+++ b/platform/util/resources/misc/registry.properties
@@ -241,6 +241,9 @@ ide.tree.ui.assert.threads.description=Dynamically enables corresponding thread
ide.tree.ui.async.batch.size=1000
ide.tree.ui.async.batch.size.description=Number of virtual files to process in one read action
+ide.windows.main.menu.expand.horizontal=false
+ide.windows.main.menu.expand.horizontal.description=Expands hamburger menu horizontally
+
ide.macos.main.menu.alignment.options=[Native*|Aligned|Aligned in group|No icons]
ide.macos.main.menu.alignment.options.description=Native - align menu items as macOS does, Aligned - align menu items vertically inside a group, No icons - hide icons
ide.splitter.mouseZone=6