diff --git a/platform/platform-api/src/com/intellij/ui/components/DefaultScrollBarUI.java b/platform/platform-api/src/com/intellij/ui/components/DefaultScrollBarUI.java index 9ec831738516..f7557a988b04 100644 --- a/platform/platform-api/src/com/intellij/ui/components/DefaultScrollBarUI.java +++ b/platform/platform-api/src/com/intellij/ui/components/DefaultScrollBarUI.java @@ -8,6 +8,7 @@ import com.intellij.ui.scale.JBUIScale; import com.intellij.util.MathUtil; import com.intellij.util.ui.*; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ChangeEvent; @@ -38,7 +39,7 @@ class DefaultScrollBarUI extends ScrollBarUI { private int myCachedValue; private int myOldValue; - protected final ScrollBarAnimationBehavior myAnimationBehavior = createWrapAnimationBehaviour(); + protected @Nullable ScrollBarAnimationBehavior myAnimationBehavior = null; DefaultScrollBarUI() { this(ScrollSettings.isThumbSmallIfOpaque() ? 13 : 10, 14, 10); @@ -73,7 +74,9 @@ class DefaultScrollBarUI extends ScrollBarUI { } void toggle(boolean isOn) { - myAnimationBehavior.onToggle(isOn); + if (myAnimationBehavior != null) { + myAnimationBehavior.onToggle(isOn); + } } static boolean isOpaque(Component c) { @@ -88,7 +91,7 @@ class DefaultScrollBarUI extends ScrollBarUI { } boolean isTrackClickable() { - return isOpaque(myScrollBar) || myAnimationBehavior.getTrackFrame() > 0; + return isOpaque(myScrollBar) || (myAnimationBehavior != null && myAnimationBehavior.getTrackFrame() > 0); } boolean isTrackExpandable() { @@ -148,7 +151,7 @@ class DefaultScrollBarUI extends ScrollBarUI { private int getTrackOffset(int offset) { if (!isTrackExpandable()) return offset; - float value = myAnimationBehavior.getTrackFrame(); + float value = myAnimationBehavior == null ? 0 :myAnimationBehavior.getTrackFrame(); if (value <= 0) return offset; if (value >= 1) return 0; return (int)(.5f + offset * (1 - value)); @@ -174,6 +177,8 @@ class DefaultScrollBarUI extends ScrollBarUI { @Override public void installUI(JComponent c) { + myAnimationBehavior = createWrapAnimationBehaviour(); + myScrollBar = (JScrollBar)c; ScrollBarPainter.setBackground(c); myScrollBar.setOpaque(false); @@ -188,7 +193,10 @@ class DefaultScrollBarUI extends ScrollBarUI { @Override public void uninstallUI(JComponent c) { + assert myAnimationBehavior != null; myAnimationBehavior.onUninstall(); + myAnimationBehavior = null; + myScrollTimer.stop(); myScrollBar.removeFocusListener(myListener); myScrollBar.removePropertyChangeListener(myListener); @@ -344,7 +352,9 @@ class DefaultScrollBarUI extends ScrollBarUI { } } myOldValue = value; - if (animate) myAnimationBehavior.onThumbMove(); + if (animate && myAnimationBehavior != null) { + myAnimationBehavior.onThumbMove(); + } } private int getValue() { @@ -373,9 +383,13 @@ class DefaultScrollBarUI extends ScrollBarUI { private void updateMouse(int x, int y) { if (isTrackContains(x, y)) { - if (!isOverTrack) myAnimationBehavior.onTrackHover(isOverTrack = true); + if (!isOverTrack && myAnimationBehavior != null) { + myAnimationBehavior.onTrackHover(isOverTrack = true); + } boolean hover = isThumbContains(x, y); - if (isOverThumb != hover) myAnimationBehavior.onThumbHover(isOverThumb = hover); + if (isOverThumb != hover && myAnimationBehavior != null) { + myAnimationBehavior.onThumbHover(isOverThumb = hover); + } } else { updateMouseExit(); @@ -383,8 +397,12 @@ class DefaultScrollBarUI extends ScrollBarUI { } private void updateMouseExit() { - if (isOverThumb) myAnimationBehavior.onThumbHover(isOverThumb = false); - if (isOverTrack) myAnimationBehavior.onTrackHover(isOverTrack = false); + if (isOverThumb && myAnimationBehavior != null) { + myAnimationBehavior.onThumbHover(isOverThumb = false); + } + if (isOverTrack && myAnimationBehavior != null) { + myAnimationBehavior.onTrackHover(isOverTrack = false); + } } private boolean redispatchIfTrackNotClickable(MouseEvent event) { @@ -546,7 +564,9 @@ class DefaultScrollBarUI extends ScrollBarUI { repaint(); } if ("opaque".equals(name) || "visible".equals(name)) { - myAnimationBehavior.onReset(); + if (myAnimationBehavior != null) { + myAnimationBehavior.onReset(); + } myTrack.bounds.setBounds(0, 0, 0, 0); myThumb.bounds.setBounds(0, 0, 0, 0); } @@ -565,7 +585,9 @@ class DefaultScrollBarUI extends ScrollBarUI { int minY = Math.min(myThumb.bounds.y, thumbPos); int maxY = Math.max(myThumb.bounds.y, thumbPos) + myThumb.bounds.height; myThumb.bounds.y = thumbPos; - myAnimationBehavior.onThumbMove(); + if (myAnimationBehavior != null) { + myAnimationBehavior.onThumbMove(); + } repaint(myThumb.bounds.x, minY, myThumb.bounds.width, maxY - minY); } } @@ -577,7 +599,9 @@ class DefaultScrollBarUI extends ScrollBarUI { int minX = Math.min(myThumb.bounds.x, thumbPos); int maxX = Math.max(myThumb.bounds.x, thumbPos) + myThumb.bounds.width; myThumb.bounds.x = thumbPos; - myAnimationBehavior.onThumbMove(); + if (myAnimationBehavior != null) { + myAnimationBehavior.onThumbMove(); + } repaint(minX, myThumb.bounds.y, maxX - minX, myThumb.bounds.height); } } diff --git a/platform/platform-api/src/com/intellij/ui/components/ScrollBarAnimationBehavior.kt b/platform/platform-api/src/com/intellij/ui/components/ScrollBarAnimationBehavior.kt index 18093a6e290f..926776a0b042 100644 --- a/platform/platform-api/src/com/intellij/ui/components/ScrollBarAnimationBehavior.kt +++ b/platform/platform-api/src/com/intellij/ui/components/ScrollBarAnimationBehavior.kt @@ -1,7 +1,14 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:OptIn(FlowPreview::class) + package com.intellij.ui.components -import com.intellij.util.SingleAlarm +import com.intellij.openapi.application.EDT +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce import javax.swing.JScrollBar internal abstract class ScrollBarAnimationBehavior(@JvmField protected val trackAnimator: TwoWayAnimator, @@ -52,7 +59,23 @@ internal class MacScrollBarAnimationBehavior( thumbAnimator: TwoWayAnimator, ) : DefaultScrollBarAnimationBehavior(trackAnimator, thumbAnimator) { private var isTrackHovered: Boolean = false - private val hideThumbAlarm = SingleAlarm(task = { thumbAnimator.start(forward = false) }, delay = 700) + @Suppress("SSBasedInspection") + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("IdeRootPane")) + private val hideThumbRequests = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + init { + coroutineScope.launch { + hideThumbRequests + .debounce(700) + .collectLatest { start -> + if (start) { + withContext(Dispatchers.EDT) { + thumbAnimator.start(forward = false) + } + } + } + } + } override fun onTrackHover(hovered: Boolean) { isTrackHovered = hovered @@ -74,19 +97,13 @@ internal class MacScrollBarAnimationBehavior( override fun onThumbMove() { val scrollBar = scrollBarComputable() if (scrollBar != null && scrollBar.isShowing() && !DefaultScrollBarUI.isOpaque(scrollBar)) { - if (!isTrackHovered && thumbAnimator.value == 0f) trackAnimator.rewind(false) - thumbAnimator.rewind(true) - hideThumbAlarm.cancelAllRequests() - if (!isTrackHovered) { - hideThumbAlarm.request() + if (!isTrackHovered && thumbAnimator.value == 0f) { + trackAnimator.rewind(false) } + thumbAnimator.rewind(true) + check(hideThumbRequests.tryEmit(!isTrackHovered)) } } - - override fun onUninstall() { - hideThumbAlarm.cancelAllRequests() - super.onUninstall() - } } internal open class ToggleableScrollBarAnimationBehaviorDecorator( diff --git a/platform/platform-api/src/com/intellij/ui/components/TabMacScrollBarUI.kt b/platform/platform-api/src/com/intellij/ui/components/TabMacScrollBarUI.kt index dd1fd8bff27d..31cbc0da6787 100644 --- a/platform/platform-api/src/com/intellij/ui/components/TabMacScrollBarUI.kt +++ b/platform/platform-api/src/com/intellij/ui/components/TabMacScrollBarUI.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.ui.components import com.intellij.ui.MixedColorProducer @@ -8,8 +8,11 @@ import java.awt.Graphics2D import java.awt.Insets import javax.swing.JComponent -internal class TabMacScrollBarUI(thickness: Int, thicknessMax: Int, thicknessMin: Int) : ThinMacScrollBarUI(thickness, thicknessMax, - thicknessMin) { +internal class TabMacScrollBarUI( + thickness: Int, + thicknessMax: Int, + thicknessMin: Int, +) : ThinMacScrollBarUI(thickness, thicknessMax, thicknessMin) { private var isHovered: Boolean = false private val defaultColorProducer: MixedColorProducer = MixedColorProducer( ScrollBarPainter.getColor({ myScrollBar }, ScrollBarPainter.TABS_TRANSPARENT_THUMB_BACKGROUND), @@ -39,7 +42,7 @@ internal class TabMacScrollBarUI(thickness: Int, thicknessMax: Int, thicknessMin override fun paintThumb(g: Graphics2D?, c: JComponent?) { - if (myAnimationBehavior.thumbFrame > 0) { + if (myAnimationBehavior != null && myAnimationBehavior!!.thumbFrame > 0) { paint(myThumb, g, c, !isHovered) } } diff --git a/platform/platform-api/src/com/intellij/ui/components/TabScrollBarUI.kt b/platform/platform-api/src/com/intellij/ui/components/TabScrollBarUI.kt index a22db87927a0..13bde8ebb5b8 100644 --- a/platform/platform-api/src/com/intellij/ui/components/TabScrollBarUI.kt +++ b/platform/platform-api/src/com/intellij/ui/components/TabScrollBarUI.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.ui.components import com.intellij.ui.MixedColorProducer @@ -39,7 +39,7 @@ internal class TabScrollBarUI(thickness: Int, thicknessMax: Int, thicknessMin: I override fun paintThumb(g: Graphics2D?, c: JComponent?) { - if (myAnimationBehavior.thumbFrame > 0) { + if (myAnimationBehavior != null && myAnimationBehavior!!.thumbFrame > 0) { paint(myThumb, g, c, !isHovered) } }