From a018819d3a2f264f91ce6426049fca559291e5bf Mon Sep 17 00:00:00 2001 From: Sergei Tachenov Date: Thu, 22 Feb 2024 18:45:58 +0200 Subject: [PATCH] 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 --- .../openapi/ui/SimpleToolWindowPanel.java | 103 ++++++++++++++---- .../intellij/ui/ScrollableContentBorder.kt | 36 ++++++ .../openapi/wm/impl/ToolWindowImpl.kt | 61 ++++++++--- .../ui/src/com/intellij/ui/SideBorder.java | 4 +- 4 files changed, 165 insertions(+), 39 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 abd9427ac319..a75662ca1dda 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/SimpleToolWindowPanel.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/SimpleToolWindowPanel.java @@ -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 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(); } -} \ No newline at end of file + + 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; + } + +} diff --git a/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt b/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt index 864c01cf4eba..cd094de985b0 100644 --- a/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt +++ b/platform/platform-api/src/com/intellij/ui/ScrollableContentBorder.kt @@ -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 = Key.create("HEADER_WITH_BORDER_ABOVE") + + @ApiStatus.Internal + @JvmField + val TOOLBAR_WITH_BORDER_ABOVE: Key = Key.create("TOOLBAR_WITH_BORDER_ABOVE") + + @ApiStatus.Internal + @JvmField + val TOOLBAR_WITH_BORDER_LEFT: Key = Key.create("TOOLBAR_WITH_BORDER_LEFT") + + private val TARGET_COMPONENT: Key> = Key.create("ScrollableContentBorder.TARGET_COMPONENT") + @JvmStatic @JvmOverloads fun setup(scrollPane: JScrollPane, @@ -44,6 +74,8 @@ class ScrollableContentBorder private constructor( sides: Set, 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() } } 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 099419f64ce2..90d71c3f41cd 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 @@ -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() - } - } -} \ No newline at end of file diff --git a/platform/util/ui/src/com/intellij/ui/SideBorder.java b/platform/util/ui/src/com/intellij/ui/SideBorder.java index 1ca17930d445..e303e0a1c793 100644 --- a/platform/util/ui/src/com/intellij/ui/SideBorder.java +++ b/platform/util/ui/src/com/intellij/ui/SideBorder.java @@ -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);