IJPL-161797 Implement the new toolbar compressing layout strategy

The new strategy no longer relies on the parent.
It works strictly top-to-bottom, looking inside child toolbars
for resizable components.

To make it work with the main toolbar, which isn't an ActionToolbar
itself, but a regular panel, its layout, HorizontalLayout, had to be
modified to accept an external function for calculating preferred sizes.

The new strategy first collects all resizable components, including
deeply nested ones, and then distributes the available size between them.
Then calculating the sizes of toolbars and their components becomes trivial.

GitOrigin-RevId: d11c47f010169b9175340ed9f0005822ddc4c346
This commit is contained in:
Sergei Tachenov
2024-10-08 11:54:49 +03:00
committed by intellij-monorepo-bot
parent a1c0fd3ded
commit fbdc860159
4 changed files with 151 additions and 79 deletions

View File

@@ -9,23 +9,28 @@ import org.jetbrains.annotations.ApiStatus.Internal
import java.awt.*
import java.util.*
import javax.swing.JComponent
import kotlin.collections.plusAssign
import kotlin.math.max
/**
* This strategy dynamically adjusts component sizes, ranging from their preferred size to their minimum size.
* Should the parent component lack sufficient space, a compress operation is triggered on its child components.
* Preferentially, the largest components are compressed first to optimize the use of available space.
* Note: for correct work, it's necessary to have a parent component for row with toolbar.
*/
@Internal
open class CompressingLayoutStrategy : ToolbarLayoutStrategy {
object CompressingLayoutStrategy : ToolbarLayoutStrategy {
override fun calculateBounds(toolbar: ActionToolbar): List<Rectangle> {
val toolbarComponent = toolbar.component
val componentsCount = toolbarComponent.componentCount
val bounds = List(componentsCount) { Rectangle() }
val preferredAndRealSize = getPreferredAndRealWidth(toolbarComponent.parent)
val componentCount = toolbarComponent.componentCount
calculateComponentSizes(preferredAndRealSize, toolbar, bounds)
val nonCompressibleWidth = getNonCompressibleWidth(toolbar.component)
val availableWidth = toolbar.component.width - nonCompressibleWidth
val resizableComponents = collectResizableComponents(toolbar)
val resizableComponentWidths = calculateResizableComponentWidths(availableWidth, resizableComponents)
val componentWidths = calculateComponentWidths(toolbar, resizableComponentWidths)
val bounds = List(componentCount) { Rectangle() }
calculateComponentSizes(toolbarComponent.components, componentWidths, bounds)
layoutComponents(toolbarComponent, bounds)
rightAlignComponents(toolbarComponent, bounds)
@@ -34,88 +39,109 @@ open class CompressingLayoutStrategy : ToolbarLayoutStrategy {
override fun calcPreferredSize(toolbar: ActionToolbar): Dimension {
val res = Dimension()
val toolbarComponent = toolbar.component
val preferredAndRealSize = getPreferredAndRealWidth(toolbarComponent.parent)
var toolbarWidthRatio = preferredAndRealSize.second / preferredAndRealSize.first
val componentSizes = calculateComponentSizes(toolbar, preferredAndRealSize)
toolbarWidthRatio = if (toolbarWidthRatio > 1) 1.0 else toolbarWidthRatio
val minButtonSize = ActionToolbar.experimentalToolbarMinimumButtonSize()
toolbar.component.components.forEach {
if (!it.isVisible || it !is JComponent) return@forEach
val size = componentSizes[it] ?: it.preferredSize.apply { width = (it.preferredSize.width * toolbarWidthRatio).toInt() }
size.height = max(size.height, minButtonSize.height)
res.height = experimentalToolbarMinimumButtonSize().height
for (component in toolbar.component.components) {
if (!component.isVisible) continue
val size = component.preferredSize
res.width += size.width
res.height = max(res.height, size.height)
}
JBInsets.addTo(res, toolbar.component.insets)
return res
}
private fun getPreferredAndRealWidth(mainToolbar: Container): Pair<Double, Double> {
var totalWidth = 0
for (i in 0 until mainToolbar.componentCount) {
val component = mainToolbar.getComponent(i)
if (component !is JComponent) continue
val toolbar = component as? ActionToolbar
if (toolbar != null && toolbar.layoutStrategy is CompressingLayoutStrategy) {
for (element in toolbar.component.components) {
if (!element.isVisible || element !is JComponent) continue
totalWidth += element.preferredSize.width
}
}
else {
totalWidth += component.preferredSize.width
}
}
val width = mainToolbar.width
return Pair((totalWidth).toDouble(), (width - getNonCompressibleWidth(mainToolbar)).toDouble())
}
protected open fun getNonCompressibleWidth(mainToolbar: Container): Int {
return mainToolbar.components.filterNot { it is ActionToolbar && it.layoutStrategy is CompressingLayoutStrategy }.sumOf { it.preferredSize.width}
}
override fun calcMinimumSize(toolbar: ActionToolbar): Dimension {
return JBUI.emptySize()
}
/**
* Distributes the available size between the given toolbars.
*
* Intended to be used by parents that are not toolbars themselves.
* If such a parent has several toolbar children, then it can't rely on their minimum and preferred sizes alone,
* because the size of every toolbar will only take into account what's inside it, but not what's inside other toolbars.
*
* To get the best result, the parent must distribute the size between the toolbars taking into account their contents.
* But because the logic of size distribution is encapsulated into this strategy, it has to be exposed as a public method.
*/
fun distributeSize(availableSize: Dimension, toolbars: List<ActionToolbar>): Map<ActionToolbar, Dimension> {
if (toolbars.isEmpty()) return emptyMap()
val resizableComponents = toolbars.associateWith { toolbar -> collectResizableComponents(toolbar) }
val nonCompressibleWidths = toolbars.associateWith { toolbar -> getNonCompressibleWidth(toolbar.component) }
val availableWidth = availableSize.width - nonCompressibleWidths.values.sum()
val resizableComponentWidths = calculateResizableComponentWidths(availableWidth, resizableComponents.values.flatten())
val height = toolbars.maxOf { toolbar -> toolbar.component.preferredSize.height }
.coerceAtLeast(experimentalToolbarMinimumButtonSize().height)
return toolbars.associateWith { toolbar ->
val width = if (toolbar.component.isVisible) {
calculateComponentWidths(toolbar, resizableComponentWidths).getValue(toolbar.component)
}
else {
0
}
Dimension(width, height)
}
}
}
private fun calculateComponentSizes(toolbar: ActionToolbar, preferredAndRealSize: Pair<Double, Double>): Map<Component, Dimension> {
val mainToolbar = toolbar.component.parent
val toolbarWidthDiff = preferredAndRealSize.first - preferredAndRealSize.second
return if (toolbarWidthDiff > 0) {
val components = mainToolbar.components.filter { it is ActionToolbar && it.layoutStrategy is CompressingLayoutStrategy }.flatMap {
(it as? JComponent)?.components?.toList() ?: listOf(it)
private fun getNonCompressibleWidth(component: Component): Int {
if (!component.isVisible) return 0
return when (component.kind) {
Kind.RESIZABLE_TOOLBAR -> {
component as JComponent
var result = component.insets.left + component.insets.right
for (component in component.components) {
result += getNonCompressibleWidth(component)
}
result
}
Kind.NON_RESIZABLE -> component.preferredSize.width
Kind.RESIZABLE_COMPONENT -> 0
}
}
private fun collectResizableComponents(toolbar: ActionToolbar): List<Component> {
if (!toolbar.component.isVisible || toolbar.component.kind != Kind.RESIZABLE_TOOLBAR) return emptyList()
val result = mutableListOf<Component>()
for (component in toolbar.component.components) {
if (!component.isVisible) continue
if (component is ActionToolbar) {
result += collectResizableComponents(component)
}
else if (component.kind == Kind.RESIZABLE_COMPONENT) {
result += component
}
}
return result
}
private enum class Kind {
RESIZABLE_TOOLBAR,
RESIZABLE_COMPONENT,
NON_RESIZABLE,
}
private val Component.kind: Kind get() =
if (!isVisible) {
Kind.NON_RESIZABLE
} else if (this is ActionToolbar) {
if (layoutStrategy is CompressingLayoutStrategy) {
Kind.RESIZABLE_TOOLBAR
}
else {
Kind.NON_RESIZABLE
}
calculateComponentWidths(preferredAndRealSize.second.toInt(), components).map { entry -> Pair(entry.key, Dimension(entry.value, entry.key.preferredSize.height)) }.toMap()
}
else {
mainToolbar.components.flatMap { (it as? Container)?.components?.toList() ?: listOf(it) }.associateWith { it.preferredSize }
if (minimumSize.width < preferredSize.width) {
Kind.RESIZABLE_COMPONENT
}
else {
Kind.NON_RESIZABLE
}
}
}
private fun calculateComponentSizes(
preferredAndRealSize: Pair<Double, Double>,
toolbar: ActionToolbar,
bounds: List<Rectangle>,
) {
val toolbarComponent = toolbar.component
var toolbarWidthRatio = preferredAndRealSize.second / preferredAndRealSize.first
toolbarWidthRatio = if (toolbarWidthRatio > 1) 1.0 else toolbarWidthRatio
val componentSizes = calculateComponentSizes(toolbar, preferredAndRealSize)
for (i in bounds.indices) {
val component: Component = toolbarComponent.getComponent(i)
val d: Dimension = componentSizes[component]
?: getChildPreferredSize(toolbarComponent, i).apply { width = (width * toolbarWidthRatio).toInt() }
bounds[i].size = d
}
}
private fun calculateComponentWidths(availableWidth: Int, components: List<Component>): Map<Component, Int> {
private fun calculateResizableComponentWidths(availableWidth: Int, components: List<Component>): Map<Component, Int> {
val preferredWidths = components.associateWith { it.preferredSize.width }
if (availableWidth >= preferredWidths.values.sum()) {
return preferredWidths
@@ -156,6 +182,42 @@ private fun calculateComponentWidths(availableWidth: Int, components: List<Compo
return calculatedWidths
}
private fun calculateComponentWidths(toolbar: ActionToolbar, resizableComponentWidths: Map<Component, Int>): Map<Component, Int> {
val result = hashMapOf<Component, Int>()
calculateComponentWidths(result, toolbar, resizableComponentWidths)
return result
}
private fun calculateComponentWidths(result: MutableMap<Component, Int>, toolbar: ActionToolbar, resizableComponentWidths: Map<Component, Int>) {
if (!toolbar.component.isVisible) return
var toolbarWidth = toolbar.component.insets.left + toolbar.component.insets.right
for (component in toolbar.component.components) {
if (!component.isVisible) continue
when (component.kind) {
Kind.RESIZABLE_TOOLBAR -> {
calculateComponentWidths(result, component as ActionToolbar, resizableComponentWidths)
}
Kind.RESIZABLE_COMPONENT -> result[component] = resizableComponentWidths.getValue(component)
Kind.NON_RESIZABLE -> result[component] = component.preferredSize.width
}
toolbarWidth += result.getValue(component)
}
result[toolbar.component] = toolbarWidth
}
private fun calculateComponentSizes(components: Array<Component>, componentWidths: Map<Component, Int>, bounds: List<Rectangle>) {
for ((i, component) in components.withIndex()) {
if (component.isVisible) {
bounds[i].width = componentWidths.getValue(component)
bounds[i].height = component.preferredSize.height
}
else {
bounds[i].width = 0
bounds[i].height = 0
}
}
}
private fun layoutComponents(toolbarComponent: JComponent, bounds: List<Rectangle>) {
val toolbarHeight = toolbarComponent.height
val minHeight = experimentalToolbarMinimumButtonSize().height

View File

@@ -18,7 +18,7 @@ public interface ToolbarLayoutStrategy {
* Preferentially, the largest components are compressed first to optimize the use of available space.
* Note: for correct work, it's necessary to have a parent component for row with toolbar.
*/
ToolbarLayoutStrategy COMPRESSING_STRATEGY = new CompressingLayoutStrategy();
ToolbarLayoutStrategy COMPRESSING_STRATEGY = CompressingLayoutStrategy.INSTANCE;
List<Rectangle> calculateBounds(@NotNull ActionToolbar toolbar);

View File

@@ -29,6 +29,9 @@ class HorizontalLayout private constructor(private val gap: JBValue,
LEFT, CENTER, RIGHT
}
@Internal
var preferredSizeFunction: (Component) -> Dimension = { LayoutUtil.getPreferredSize(it) }
private val leftGroup = ArrayList<Component>()
private val centerGroup = ArrayList<Component>()
private val rightGroup = ArrayList<Component>()
@@ -164,7 +167,7 @@ class HorizontalLayout private constructor(private val gap: JBValue,
continue
}
val size = LayoutUtil.getPreferredSize(component)
val size = preferredSizeFunction(component)
var y = 0
if (verticalAlignment == FILL) {
size.height = height
@@ -188,7 +191,7 @@ class HorizontalLayout private constructor(private val gap: JBValue,
var result: Dimension? = null
for (component in list) {
if (component.isVisible) {
result = joinDimension(result, gap, LayoutUtil.getPreferredSize(component))
result = joinDimension(result, gap, preferredSizeFunction(component))
}
}
return result

View File

@@ -120,6 +120,17 @@ class MainToolbar(
updateToolbarActions()
})
}
(layout as HorizontalLayout).apply {
preferredSizeFunction = { component ->
if (component is ActionToolbar) {
val availableSize = Dimension(this@MainToolbar.width - 4 * JBUI.scale(layoutGap), this@MainToolbar.height)
CompressingLayoutStrategy.distributeSize(availableSize, components.filterIsInstance<ActionToolbar>()).getValue(component)
}
else {
component.preferredSize
}
}
}
}
private fun updateToolbarActions() {
@@ -311,11 +322,7 @@ private fun createActionBar(group: ActionGroup, customizationGroup: ActionGroup?
toolbar.setMinimumButtonSize { ActionToolbar.experimentalToolbarMinimumButtonSize() }
toolbar.targetComponent = null
toolbar.layoutStrategy = object : CompressingLayoutStrategy() {
override fun getNonCompressibleWidth(mainToolbar: Container): Int {
return super.getNonCompressibleWidth(mainToolbar) + layoutGap * 4
}
}
toolbar.layoutStrategy = ToolbarLayoutStrategy.COMPRESSING_STRATEGY
val component = toolbar.component
component.border = JBUI.Borders.empty()
component.isOpaque = false