IDEA-317864 Use the new scroll pane tracker for toolbars and headers

Specifically, in SimpleToolWindowPanel and ToolWindowImpl for
painting the border and the tool window header.

To avoid double borders with ScrollableContentBorder, introduce
client properties that indicate that there's already a border nearby,
so there's no need to paint another one.

Because ScrollableContentBorder can be installed on a different
component than the scroll pane itself, also introduce a weak reference
property to indicate which target component the border is installed
on, so that all checks work properly even if the scroll pane is located
elsewhere.

GitOrigin-RevId: 602998a9346ce2503e56bc9823fb53bc6af8c248
This commit is contained in:
Sergei Tachenov
2024-03-27 18:01:32 +00:00
committed by intellij-monorepo-bot
parent 322e2e9db2
commit a018819d3a
4 changed files with 165 additions and 39 deletions
@@ -5,21 +5,19 @@ import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.util.Key;
import com.intellij.ui.ClientProperty;
import com.intellij.ui.ExperimentalUI;
import com.intellij.ui.JBColor;
import com.intellij.ui.*;
import com.intellij.ui.components.JBPanelWithEmptyText;
import com.intellij.ui.paint.LinePainter2D;
import com.intellij.ui.switcher.QuickActionProvider;
import com.intellij.util.containers.JBIterable;
import com.intellij.util.ui.UIUtil;
import kotlin.Unit;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.AdjustmentListener;
import java.awt.event.ContainerAdapter;
import java.awt.event.ContainerEvent;
import java.util.Collections;
@@ -27,6 +25,7 @@ import java.util.List;
public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements QuickActionProvider, DataProvider {
public static final Key<Boolean> SCROLLED_STATE = Key.create("ScrolledState");
private static final int GAP = 1;
private JComponent myToolbar;
private JComponent myContent;
@@ -35,6 +34,8 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick
protected boolean myVertical;
private boolean myProvideQuickActions = true;
private @Nullable ScrollPaneTracker myScrollPaneTracker;
public SimpleToolWindowPanel(boolean vertical) {
this(vertical, false);
}
@@ -66,6 +67,28 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick
}
}
});
if (ExperimentalUI.isNewUI()) {
myScrollPaneTracker = new ScrollPaneTracker(this, this::isInContent, () -> {
updateScrolledState();
return Unit.INSTANCE;
});
}
}
private boolean isInContent(@NotNull Component component) {
// This check is just in case we have something scrollable in the toolbar.
var parent = component;
while (parent != null) {
if (parent == myToolbar) {
return false;
}
else if (parent == myContent) {
return true;
}
parent = parent.getParent();
}
return false;
}
public boolean isVertical() {
@@ -90,7 +113,7 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick
}
private void updateLayout() {
setLayout(new BorderLayout(myVertical ? 0 : 1, myVertical ? 1 : 0));
setLayout(new BorderLayout(myVertical ? 0 : GAP, myVertical ? GAP : 0));
}
public void setToolbar(@Nullable JComponent c) {
@@ -147,24 +170,12 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick
public void setContent(@NotNull JComponent c) {
if (myContent != null) {
ClientProperty.remove(myContent, SCROLLED_STATE);
remove(myContent);
}
myContent = c;
if (ExperimentalUI.isNewUI()) {
JScrollPane scrollPane = UIUtil.findComponentOfType(myContent, JScrollPane.class);
AdjustmentListener listener = event -> {
ClientProperty.put(myContent, SCROLLED_STATE, event.getAdjustable().getValue() != 0);
repaint();
};
if (scrollPane != null) {
scrollPane.getVerticalScrollBar().addAdjustmentListener(listener);
scrollPane.getHorizontalScrollBar().addAdjustmentListener(listener);
}
}
add(c, BorderLayout.CENTER);
if (myBorderless) {
@@ -182,11 +193,8 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick
if (myToolbar != null && myToolbar.getParent() == this && myContent != null && myContent.getParent() == this) {
g.setColor(JBColor.border());
if (ExperimentalUI.isNewUI()) {
//don't draw line for scrolled content
if (Boolean.FALSE.equals(ClientProperty.get(myContent, SCROLLED_STATE))) {
if (ExperimentalUI.isNewUI() && !isScrolled()) {
return;
}
}
if (myVertical) {
int y = (int)myToolbar.getBounds().getMaxY();
@@ -206,4 +214,53 @@ public class SimpleToolWindowPanel extends JBPanelWithEmptyText implements Quick
}
return toolbars.flatten(ActionToolbar::getActions).toList();
}
}
private void updateScrolledState() {
if (myContent == null || myScrollPaneTracker == null) {
return;
}
var oldState = isScrolled();
var newState = false;
for (ScrollPaneScrolledState scrollPaneState : myScrollPaneTracker.getScrollPaneStates()) {
var scrollPane = scrollPaneState.getScrollPane();
boolean scrolled = myVertical ? !scrollPaneState.isVerticalAtStart() : !scrollPaneState.isHorizontalAtStart();
if (isTouchingToolbar(scrollPane) && scrolled) {
newState = true;
break;
}
}
if (newState != oldState) {
ClientProperty.put(myContent, SCROLLED_STATE, newState);
repaint();
}
var key = myVertical ? ScrollableContentBorder.TOOLBAR_WITH_BORDER_ABOVE : ScrollableContentBorder.TOOLBAR_WITH_BORDER_LEFT;
for (ScrollPaneScrolledState scrollPaneState : myScrollPaneTracker.getScrollPaneStates()) {
var targetComponent = ScrollableContentBorder.getTargetComponent(scrollPaneState.getScrollPane());
if (targetComponent == null) {
continue;
}
var hadToolbarWithBorder = ClientProperty.isTrue(targetComponent, key);
var hasToolbarWithBorder = isTouchingToolbar(targetComponent) && (!ExperimentalUI.isNewUI() || newState);
if (hasToolbarWithBorder != hadToolbarWithBorder) {
ClientProperty.put(targetComponent, key, hasToolbarWithBorder);
targetComponent.repaint();
}
}
}
private boolean isScrolled() {
return ClientProperty.isTrue(myContent, SCROLLED_STATE);
}
private boolean isTouchingToolbar(JComponent component) {
var toolbar = getToolbar();
if (toolbar == null || !toolbar.isVisible() || !component.isShowing()) {
return false;
}
var expectedCoordinate = (myVertical ? toolbar.getHeight() : toolbar.getWidth()) + GAP;
var paneLocation = SwingUtilities.convertPoint(component.getParent(), component.getLocation(), this);
var actualCoordinate = myVertical ? paneLocation.getY() : paneLocation.getX();
return expectedCoordinate == actualCoordinate;
}
}
@@ -1,12 +1,16 @@
// 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.ui
import com.intellij.openapi.util.Key
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import org.jetbrains.annotations.ApiStatus
import java.awt.Color
import java.awt.Component
import java.awt.Graphics
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener
import java.lang.ref.WeakReference
import javax.swing.JComponent
import javax.swing.JScrollPane
import javax.swing.border.Border
@@ -29,7 +33,33 @@ class ScrollableContentBorder private constructor(
fun isVisible(): Boolean = lineColor != UIUtil.TRANSPARENT_COLOR
override fun paintBorder(c: Component, g: Graphics, x: Int, y: Int, width: Int, height: Int) {
val alreadyHasBorder = when (mySideMask) {
TOP -> ClientProperty.isTrue(c, HEADER_WITH_BORDER_ABOVE) || ClientProperty.isTrue(c, TOOLBAR_WITH_BORDER_ABOVE)
LEFT -> ClientProperty.isTrue(c, TOOLBAR_WITH_BORDER_LEFT)
else -> false
}
if (!alreadyHasBorder) {
super.paintBorder(c, g, x, y, width, height)
}
}
companion object {
@ApiStatus.Internal
@JvmField
val HEADER_WITH_BORDER_ABOVE: Key<Boolean> = Key.create("HEADER_WITH_BORDER_ABOVE")
@ApiStatus.Internal
@JvmField
val TOOLBAR_WITH_BORDER_ABOVE: Key<Boolean> = Key.create("TOOLBAR_WITH_BORDER_ABOVE")
@ApiStatus.Internal
@JvmField
val TOOLBAR_WITH_BORDER_LEFT: Key<Boolean> = Key.create("TOOLBAR_WITH_BORDER_LEFT")
private val TARGET_COMPONENT: Key<WeakReference<JComponent>> = Key.create("ScrollableContentBorder.TARGET_COMPONENT")
@JvmStatic
@JvmOverloads
fun setup(scrollPane: JScrollPane,
@@ -44,6 +74,8 @@ class ScrollableContentBorder private constructor(
sides: Set<Side>,
targetComponent: JComponent = scrollPane) {
ClientProperty.put(scrollPane, TARGET_COMPONENT, WeakReference(targetComponent))
val borders = sides.associateWith { side -> ScrollableContentBorder(side.toMask()) }
val tracker = ScrollPaneScrolledStateTracker(scrollPane) { state ->
@@ -55,6 +87,7 @@ class ScrollableContentBorder private constructor(
override fun propertyChange(evt: PropertyChangeEvent?) {
targetComponent.removePropertyChangeListener("border", this)
tracker.detach()
ClientProperty.put(scrollPane, TARGET_COMPONENT, null)
}
})
}
@@ -66,6 +99,9 @@ class ScrollableContentBorder private constructor(
sequenceOf(top, bottom, right, left).filter { it != 0 }.count() == 1
}
}
@JvmStatic
fun getTargetComponent(scrollPane: JScrollPane): JComponent? = ClientProperty.get(scrollPane, TARGET_COMPONENT)?.get()
}
}
@@ -32,10 +32,7 @@ import com.intellij.toolWindow.FocusTask
import com.intellij.toolWindow.InternalDecoratorImpl
import com.intellij.toolWindow.ToolWindowEventSource
import com.intellij.toolWindow.ToolWindowProperty
import com.intellij.ui.ClientProperty
import com.intellij.ui.ComponentTreeWatcher
import com.intellij.ui.LayeredIcon
import com.intellij.ui.UIBundle
import com.intellij.ui.*
import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentManager
import com.intellij.ui.content.ContentManagerEvent
@@ -98,6 +95,7 @@ internal class ToolWindowImpl(val toolWindowManager: ToolWindowManagerImpl,
internal var decorator: InternalDecoratorImpl? = null
private set
private var scrollPaneTracker: ScrollPaneTracker? = null
private var hideOnEmptyContent = false
var isPlaceholderMode: Boolean = false
@@ -149,7 +147,6 @@ internal class ToolWindowImpl(val toolWindowManager: ToolWindowManagerImpl,
private class UpdateBackgroundContentManager(private val decorator: InternalDecoratorImpl?) : ContentManagerListener {
override fun contentAdded(event: ContentManagerEvent) {
InternalDecoratorImpl.setBackgroundRecursively(event.content.component, JBUI.CurrentTheme.ToolWindow.background())
addAdjustListener(decorator, event.content.component)
}
}
@@ -208,6 +205,11 @@ internal class ToolWindowImpl(val toolWindowManager: ToolWindowManagerImpl,
}
override fun unprocessComponent(component: Component) = Unit
}.register(decorator)
if (ExperimentalUI.isNewUI()) {
scrollPaneTracker = ScrollPaneTracker(decorator, { true }) {
updateScrolledState()
}
}
toolWindowFocusWatcher = ToolWindowFocusWatcher(toolWindow = this, component = decorator)
contentManager.addContentManagerListener(object : ContentManagerListener {
@@ -230,6 +232,45 @@ internal class ToolWindowImpl(val toolWindowManager: ToolWindowManagerImpl,
ToggleToolbarAction.updateToolbarsVisibility(this, PropertiesComponent.getInstance(project))
}
private fun updateScrolledState() {
val decorator = this.decorator ?: return
val tracker = scrollPaneTracker ?: return
val oldState = ClientProperty.isTrue(decorator, SimpleToolWindowPanel.SCROLLED_STATE)
var newState = false
for (scrollPaneState in tracker.scrollPaneStates) {
val scrollPane = scrollPaneState.scrollPane
if (isTouchingHeader(scrollPane) && !scrollPaneState.isVerticalAtStart) {
newState = true
}
}
if (oldState != newState) {
ClientProperty.put(decorator, SimpleToolWindowPanel.SCROLLED_STATE, newState)
decorator.header.repaint()
}
for (scrollPaneState in tracker.scrollPaneStates) {
val targetComponent = ScrollableContentBorder.getTargetComponent(scrollPaneState.scrollPane) ?: continue
val hadHeaderWithBorder = ClientProperty.isTrue(targetComponent, ScrollableContentBorder.HEADER_WITH_BORDER_ABOVE)
val hasHeaderWithBorder = isTouchingHeader(targetComponent) && (anchor == ToolWindowAnchor.BOTTOM || newState)
if (hasHeaderWithBorder != hadHeaderWithBorder) {
ClientProperty.put(targetComponent, ScrollableContentBorder.HEADER_WITH_BORDER_ABOVE, hasHeaderWithBorder)
targetComponent.repaint()
}
}
}
private fun isTouchingHeader(component: JComponent): Boolean {
val decorator = this.decorator
if (decorator == null || !component.isShowing) {
return false
}
else {
val header = decorator.header
val headerBounds = SwingUtilities.convertRectangle(header.parent, header.bounds, decorator)
val paneLocation = SwingUtilities.convertPoint(component.parent, component.location, decorator)
return paneLocation.y == headerBounds.y + headerBounds.height
}
}
fun onMovedOrResized() {
moveOrResizeAlarm.cancelAndRequest()
}
@@ -593,7 +634,6 @@ internal class ToolWindowImpl(val toolWindowManager: ToolWindowManagerImpl,
if (toolWindowManager.isNewUi) {
InternalDecoratorImpl.setBackgroundRecursively(component = contentManager.value.component, bg = JBUI.CurrentTheme.ToolWindow.background())
addAdjustListener(decorator = decorator, component = contentManager.value.component)
}
}
@@ -888,12 +928,3 @@ private class ToolWindowFocusWatcher(private val toolWindow: ToolWindowImpl, com
})
}
}
private fun addAdjustListener(decorator: InternalDecoratorImpl?, component: JComponent) {
UIUtil.findComponentOfType(component, JScrollPane::class.java)?.verticalScrollBar?.addAdjustmentListener { event ->
decorator?.let {
ClientProperty.put(it, SimpleToolWindowPanel.SCROLLED_STATE, event.adjustable?.value != 0)
it.header.repaint()
}
}
}
@@ -3,6 +3,7 @@
package com.intellij.ui;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.ApiStatus;
import javax.swing.border.LineBorder;
import java.awt.*;
@@ -18,7 +19,8 @@ public class SideBorder extends LineBorder {
@MagicConstant(flags = {NONE, LEFT, TOP, RIGHT, BOTTOM, ALL})
public @interface SideMask {}
private final int mySideMask;
@ApiStatus.Internal
protected final int mySideMask;
public SideBorder(Color color, @SideMask int mask) {
this(color, mask, 1);