From ae7c6319321920c8963864283d12baaecca4fa81 Mon Sep 17 00:00:00 2001 From: Pavel Porvatov Date: Thu, 20 Apr 2023 15:37:59 +0200 Subject: [PATCH] IDEA-317375 Horizontal menu on Windows GitOrigin-RevId: a29edbee39c8af8999921aed896e7b44426a1036 --- .../resources/messages/IdeBundle.properties | 2 + .../intellij/openapi/wm/impl/IdeRootPane.kt | 4 +- .../header/toolbar/ExpandableMenu.kt | 138 ++++++++++++++++++ .../header/toolbar/MainMenuButton.kt | 68 +++++---- .../header/toolbar/ToolbarFrameHeader.kt | 21 ++- .../wm/impl/headertoolbar/MainToolbar.kt | 2 +- .../util/resources/misc/registry.properties | 3 + 7 files changed, 201 insertions(+), 37 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/openapi/wm/impl/customFrameDecorations/header/toolbar/ExpandableMenu.kt 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