IDEA-317375 Horizontal menu on Windows

GitOrigin-RevId: a29edbee39c8af8999921aed896e7b44426a1036
This commit is contained in:
Pavel Porvatov
2023-04-20 15:41:47 +00:00
committed by intellij-monorepo-bot
parent d326d9200c
commit ae7c631932
7 changed files with 201 additions and 37 deletions
@@ -2763,6 +2763,8 @@ to.create.new.file.tooltip=To create a new file, press <b>{0}</b>. 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
@@ -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
@@ -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)
}
}
}
@@ -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<KeyStroke>()
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
}
@@ -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())
}
@@ -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)
}
@@ -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