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);