From c7e91d822f564013d908300a9fdaffddeadf6328 Mon Sep 17 00:00:00 2001 From: Sergei Tachenov Date: Thu, 28 Mar 2024 12:12:35 +0200 Subject: [PATCH] IDEA-317864 Add ScrollableContentBorder implementation for complicated cases Track scroll panes automatically as they're added and add borders if a scroll pane touching the edge is scrolled. Needed for complicated tool windows like Build, where it's hard to figure out the exact scroll panes to track, because they are added to a splitter, which may or may not have other components and it may change at any moment. So instead track scroll panes and check their location to see if scrolling should add a border. GitOrigin-RevId: 1dae2bf8b5ce43beb54de84a59a58e78721fb3eb --- .../openapi/ui/SimpleToolWindowPanel.java | 4 +- .../src/com/intellij/ui/ScrollPaneTracker.kt | 31 +++++++---- .../intellij/ui/ScrollableContentBorder.kt | 53 +++++++++++++++++-- .../openapi/wm/impl/ToolWindowImpl.kt | 2 +- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/platform/platform-api/src/com/intellij/openapi/ui/SimpleToolWindowPanel.java b/platform/platform-api/src/com/intellij/openapi/ui/SimpleToolWindowPanel.java index a75662ca1dda..317cd9dfd4ff 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/SimpleToolWindowPanel.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/SimpleToolWindowPanel.java @@ -69,7 +69,7 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick }); if (ExperimentalUI.isNewUI()) { - myScrollPaneTracker = new ScrollPaneTracker(this, this::isInContent, () -> { + myScrollPaneTracker = new ScrollPaneTracker(this, this::isInContent, tracker -> { updateScrolledState(); return Unit.INSTANCE; }); @@ -223,7 +223,7 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick var newState = false; for (ScrollPaneScrolledState scrollPaneState : myScrollPaneTracker.getScrollPaneStates()) { var scrollPane = scrollPaneState.getScrollPane(); - boolean scrolled = myVertical ? !scrollPaneState.isVerticalAtStart() : !scrollPaneState.isHorizontalAtStart(); + boolean scrolled = myVertical ? !scrollPaneState.getState().isVerticalAtStart() : !scrollPaneState.getState().isHorizontalAtStart(); if (isTouchingToolbar(scrollPane) && scrolled) { newState = true; break; diff --git a/platform/platform-api/src/com/intellij/ui/ScrollPaneTracker.kt b/platform/platform-api/src/com/intellij/ui/ScrollPaneTracker.kt index 504cacc967ce..19aa0c27f104 100644 --- a/platform/platform-api/src/com/intellij/ui/ScrollPaneTracker.kt +++ b/platform/platform-api/src/com/intellij/ui/ScrollPaneTracker.kt @@ -19,6 +19,11 @@ import javax.swing.JScrollPane @Internal data class ScrollPaneScrolledState( val scrollPane: JScrollPane, + val state: ScrolledState, +) + +@Internal +data class ScrolledState( val isHorizontalAtStart: Boolean, val isHorizontalAtEnd: Boolean, val isVerticalAtStart: Boolean, @@ -47,7 +52,7 @@ data class ScrollPaneScrolledState( class ScrollPaneTracker( container: Component, private val filter: (JScrollPane) -> Boolean, - private val callback: () -> Unit, + private val callback: (ScrollPaneTracker) -> Unit, ) { val scrollPaneStates: List @@ -75,9 +80,9 @@ class ScrollPaneTracker( private fun registerScrollPane(scrollPane: JScrollPane) { myTrackers.add(ScrollPaneScrolledStateTracker(scrollPane) { - callback() + callback(this) }) - callback() + callback(this) } private fun unregisterScrollPane(scrollPane: JScrollPane) { @@ -113,10 +118,12 @@ class ScrollPaneScrolledStateTracker(val scrollPane: JScrollPane, private val ca var state: ScrollPaneScrolledState = ScrollPaneScrolledState( scrollPane, - isHorizontalAtStart = true, - isHorizontalAtEnd = true, - isVerticalAtStart = true, - isVerticalAtEnd = true, + ScrolledState( + isHorizontalAtStart = true, + isHorizontalAtEnd = true, + isVerticalAtStart = true, + isVerticalAtEnd = true, + ) ); private set private val scrollBarListener = PropertyChangeListener { e -> @@ -149,10 +156,12 @@ class ScrollPaneScrolledStateTracker(val scrollPane: JScrollPane, private val ca private fun fireCallback(fireAnyway: Boolean) { val newState = ScrollPaneScrolledState( scrollPane, - isHorizontalAtStart = horizontalListener.isAtStart, - isHorizontalAtEnd = horizontalListener.isAtEnd, - isVerticalAtStart = verticalListener.isAtStart, - isVerticalAtEnd = verticalListener.isAtEnd, + ScrolledState( + isHorizontalAtStart = horizontalListener.isAtStart, + isHorizontalAtEnd = horizontalListener.isAtEnd, + isVerticalAtStart = verticalListener.isAtStart, + isVerticalAtEnd = verticalListener.isAtEnd, + ) ) if (fireAnyway || newState != state) { state = newState diff --git a/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt b/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt index cd094de985b0..5354af22099c 100644 --- a/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt +++ b/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt @@ -13,6 +13,7 @@ import java.beans.PropertyChangeListener import java.lang.ref.WeakReference import javax.swing.JComponent import javax.swing.JScrollPane +import javax.swing.SwingUtilities import javax.swing.border.Border @ApiStatus.Experimental @@ -78,8 +79,8 @@ class ScrollableContentBorder private constructor( val borders = sides.associateWith { side -> ScrollableContentBorder(side.toMask()) } - val tracker = ScrollPaneScrolledStateTracker(scrollPane) { state -> - updateBorderVisibility(targetComponent, borders, state) + val tracker = ScrollPaneScrolledStateTracker(scrollPane) { scrollPaneState -> + updateBorderVisibility(targetComponent, borders, scrollPaneState.state) } targetComponent.border = if (borders.size == 1) borders.values.single() else JBUI.Borders.compound(*borders.values.toTypedArray()) @@ -92,6 +93,52 @@ class ScrollableContentBorder private constructor( }) } + @JvmStatic + fun setup(container: JComponent, side: Side) { + setup(container, setOf(side)) + } + + @JvmStatic + fun setup(container: JComponent, sides: Set) { + val borders = sides.associateWith { side -> ScrollableContentBorder(side.toMask()) } + container.border = if (borders.size == 1) borders.values.single() else JBUI.Borders.compound(*borders.values.toTypedArray()) + ScrollPaneTracker(container, { true }) { tracker -> + updateScrollPaneStates(container, borders, tracker) + } + } + + private fun updateScrollPaneStates( + container: JComponent, + borders: Map, + tracker: ScrollPaneTracker, + ) { + for (scrollPaneState in tracker.scrollPaneStates) { + ClientProperty.put(scrollPaneState.scrollPane, TARGET_COMPONENT, WeakReference(container)) + } + val state = ScrolledState( + isHorizontalAtStart = tracker.noneMatch { atLeft(container, it.scrollPane) && !it.state.isHorizontalAtStart }, + isHorizontalAtEnd = tracker.noneMatch { atRight(container, it.scrollPane) && !it.state.isHorizontalAtEnd }, + isVerticalAtStart = tracker.noneMatch { atTop(container, it.scrollPane) && !it.state.isVerticalAtStart }, + isVerticalAtEnd = tracker.noneMatch { atBottom(container, it.scrollPane) && !it.state.isVerticalAtEnd }, + ) + updateBorderVisibility(container, borders, state) + } + + private fun ScrollPaneTracker.noneMatch(predicate: (ScrollPaneScrolledState) -> Boolean): Boolean = + scrollPaneStates.none { it.scrollPane.isShowing && predicate(it) } + + private fun atLeft(container: JComponent, scrollPane: JScrollPane): Boolean = + SwingUtilities.convertPoint(scrollPane.parent, scrollPane.location, container).x == container.insets.left + + private fun atRight(container: JComponent, scrollPane: JScrollPane): Boolean = + SwingUtilities.convertRectangle(scrollPane.parent, scrollPane.bounds, container).let { it.x + it.width } == container.width - container.insets.right + + private fun atTop(container: JComponent, scrollPane: JScrollPane): Boolean = + SwingUtilities.convertPoint(scrollPane.parent, scrollPane.location, container).y == container.insets.top + + private fun atBottom(container: JComponent, scrollPane: JScrollPane): Boolean = + SwingUtilities.convertRectangle(scrollPane.parent, scrollPane.bounds, container).let { it.y + it.height } == container.height - container.insets.bottom + private fun isOneSideBorder(sideBorder: Border): Boolean { if (sideBorder !is SideBorder) return false @@ -108,7 +155,7 @@ class ScrollableContentBorder private constructor( private fun updateBorderVisibility( targetComponent: JComponent, borders: Map, - state: ScrollPaneScrolledState, + state: ScrolledState, ) { var changed = false for ((side, border) in borders) { diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowImpl.kt b/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowImpl.kt index 90d71c3f41cd..5c5137164217 100644 --- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowImpl.kt +++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowImpl.kt @@ -239,7 +239,7 @@ internal class ToolWindowImpl(val toolWindowManager: ToolWindowManagerImpl, var newState = false for (scrollPaneState in tracker.scrollPaneStates) { val scrollPane = scrollPaneState.scrollPane - if (isTouchingHeader(scrollPane) && !scrollPaneState.isVerticalAtStart) { + if (isTouchingHeader(scrollPane) && !scrollPaneState.state.isVerticalAtStart) { newState = true } }