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
This commit is contained in:
Sergei Tachenov
2024-03-29 11:41:05 +00:00
committed by intellij-monorepo-bot
parent d879451de7
commit c7e91d822f
4 changed files with 73 additions and 17 deletions
@@ -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;
@@ -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<ScrollPaneScrolledState>
@@ -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
@@ -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<Side>) {
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<Side, ScrollableContentBorder>,
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<Side, ScrollableContentBorder>,
state: ScrollPaneScrolledState,
state: ScrolledState,
) {
var changed = false
for ((side, border) in borders) {
@@ -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
}
}