mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
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:
committed by
intellij-monorepo-bot
parent
a1c0fd3ded
commit
fbdc860159
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user