From 1c604ef58a0f543a889bcebefff1c7f22b7a7a7e Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Tue, 30 Sep 2025 18:10:36 +0200 Subject: [PATCH] [gh] Dim comments when showing the on-hover outline #IJPL-200029 Fixed GitOrigin-RevId: 2c894f76709047b62fe26db035ee79c3589ef370 --- .../github/pullrequest/ui/GHPRInlayUtils.kt | 71 +++++++++++++++++-- .../ui/comment/GHPRHoverableReviewComment.kt | 2 + .../comment/GHPRHoverableReviewCommentImpl.kt | 22 ++++++ .../ui/diff/GHPRReviewDiffExtension.kt | 6 +- .../ui/editor/GHPRReviewInEditorController.kt | 1 + .../pullrequest/ui/editor/editorInlays.kt | 32 ++------- .../ui/editor/editorInlaysRenderers.kt | 43 ++++++++--- 7 files changed, 138 insertions(+), 39 deletions(-) create mode 100644 plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewCommentImpl.kt diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/GHPRInlayUtils.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/GHPRInlayUtils.kt index 5a9fc1249300..f2010e9b0f1f 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/GHPRInlayUtils.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/GHPRInlayUtils.kt @@ -2,10 +2,12 @@ package org.jetbrains.plugins.github.pullrequest.ui import com.intellij.collaboration.async.combineState +import com.intellij.collaboration.async.flatMapLatestEach import com.intellij.collaboration.async.launchNow import com.intellij.collaboration.ui.codereview.diff.DiffLineLocation import com.intellij.collaboration.ui.codereview.editor.CodeReviewCommentableEditorModel import com.intellij.collaboration.ui.codereview.editor.CodeReviewEditorGutterControlsModel +import com.intellij.collaboration.ui.codereview.editor.CodeReviewEditorInlaysModel import com.intellij.diff.util.LineRange import com.intellij.diff.util.Side import com.intellij.openapi.editor.Editor @@ -19,13 +21,18 @@ import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.wm.IdeGlassPaneUtil import com.intellij.platform.util.coroutines.childScope import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.jetbrains.plugins.github.pullrequest.ui.comment.CommentedCodeFrameRenderer import org.jetbrains.plugins.github.pullrequest.ui.editor.GHPREditorMappedComponentModel -import java.awt.Cursor -import java.awt.Point +import java.awt.* +import javax.swing.JComponent +import javax.swing.JLayer +import javax.swing.plaf.LayerUI import kotlin.math.abs internal object GHPRInlayUtils { @@ -78,9 +85,7 @@ internal object GHPRInlayUtils { finally { if (frameResizer != null) { val editorEx = editor as EditorEx - val gutterGlassComp = IdeGlassPaneUtil.find(editorEx.gutterComponentEx) editorEx.setCustomCursor(frameResizer, null) - gutterGlassComp.setCursor(null, frameResizer) editor.removeEditorMouseListener(frameResizer) editor.removeEditorMouseMotionListener(frameResizer) } @@ -106,7 +111,6 @@ internal object GHPRInlayUtils { Cursor.getDefaultCursor() } - override fun mouseDragged(e: EditorMouseEvent) { if (!isDraggingFrame || edge == null || oldRange == null) return e.consume() // to prevent selecting text while dragging @@ -203,4 +207,61 @@ internal object GHPRInlayUtils { TOP, BOTTOM } } + + + @OptIn(ExperimentalCoroutinesApi::class) + fun installInlaysDimming(cs: CoroutineScope, model: CodeReviewEditorInlaysModel<*>) { + cs.launchNow { + model.inlays + .map { it.filterIsInstance() } + .flatMapLatestEach { item -> + combine(item.shouldShowOutline, item.range) { shouldShowOutline, range -> + InlayState(item, shouldShowOutline, range) + } + } + .collectLatest { inlayStates -> + val rangesToDim = inlayStates + .filter { it.shouldShowOutline } + .mapNotNull { + val (_, lines) = it.range ?: return@mapNotNull null + lines.first.. + val (_, lines) = range ?: return@forEach + val onLine = lines.last + + vm.setDimmed(rangesToDim.any { dimRange -> dimRange.contains(onLine) }) + } + } + } + } + + private data class InlayState( + val inlay: GHPREditorMappedComponentModel, + val shouldShowOutline: Boolean, + val range: Pair?, + ) +} + +internal class FadeLayerUI : LayerUI() { + private var alpha: Float = 1f + + fun setAlpha(a: Float, jLayer: JLayer) { + alpha = a.coerceIn(0f, 1f) + jLayer.repaint() + } + + override fun paint(g: Graphics, c: JComponent) { + val g2 = g.create() as Graphics2D + try { + val old = g2.composite + g2.composite = AlphaComposite.SrcOver.derive(alpha) + super.paint(g2, c) + g2.composite = old + } + finally { + g2.dispose() + } + } } \ No newline at end of file diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewComment.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewComment.kt index a0e6ddd4b0f3..fdeb8af5b09d 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewComment.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewComment.kt @@ -6,4 +6,6 @@ import kotlinx.coroutines.flow.StateFlow interface GHPRHoverableReviewComment { val shouldShowOutline: StateFlow fun showOutline(isHovered: Boolean) + val isDimmed: StateFlow + fun setDimmed(isDimmed: Boolean) } \ No newline at end of file diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewCommentImpl.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewCommentImpl.kt new file mode 100644 index 000000000000..8bd9ab36de58 --- /dev/null +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/comment/GHPRHoverableReviewCommentImpl.kt @@ -0,0 +1,22 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.github.pullrequest.ui.comment + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class GHPRHoverableReviewCommentImpl : GHPRHoverableReviewComment { + private val _isHovered: MutableStateFlow = MutableStateFlow(false) + override val shouldShowOutline: StateFlow = _isHovered.asStateFlow() + + override fun showOutline(isHovered: Boolean) { + _isHovered.value = isHovered + } + + private val _isDimmed: MutableStateFlow = MutableStateFlow(false) + override val isDimmed: StateFlow = _isDimmed.asStateFlow() + + override fun setDimmed(isDimmed: Boolean) { + _isDimmed.value = isDimmed + } +} \ No newline at end of file diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt index 86a5e706d2e8..2c47aa7d4d47 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt @@ -100,11 +100,15 @@ internal class GHPRReviewDiffExtension : DiffExtension() { val (leftLine, rightLine) = lineToUnified(it) GHPRReviewUnifiedPosition(change, leftLine, rightLine) }.apply { + editor.putUserData(GHPRReviewDiffEditorModel.KEY, this) cs.launchNow { inlays - .mapStatefulToStateful { inlayModel -> GHPRInlayUtils.installInlayHoverOutline(this, editor, side, locationToLine, inlayModel) } + .mapStatefulToStateful { inlayModel -> + GHPRInlayUtils.installInlayHoverOutline(this, editor, side, locationToLine, inlayModel) + } .collect() } + GHPRInlayUtils.installInlaysDimming(cs, this@apply) } } } diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/GHPRReviewInEditorController.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/GHPRReviewInEditorController.kt index 7e3e1363e1ae..649ab4c407db 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/GHPRReviewInEditorController.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/GHPRReviewInEditorController.kt @@ -121,6 +121,7 @@ private suspend fun showReview(project: Project, settings: GithubPullRequestsPro launchNow { val userIcon = fileVm.iconProvider.getIcon(fileVm.currentUser.url, 16) editor.renderInlays(model.inlays, HashingUtil.mappingStrategy(GHPREditorMappedComponentModel::key)) { + GHPRInlayUtils.installInlaysDimming(this, model) launchNow { model.inlays .mapStatefulToStateful { inlayModel -> GHPRInlayUtils.installInlayHoverOutline(this, editor, Side.RIGHT, null, inlayModel) } diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlays.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlays.kt index b1b90cf7b14f..5296259868f1 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlays.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlays.kt @@ -12,36 +12,25 @@ import kotlinx.coroutines.flow.asStateFlow import org.jetbrains.plugins.github.ai.GHPRAICommentViewModel import org.jetbrains.plugins.github.pullrequest.ui.comment.GHPRCompactReviewThreadViewModel import org.jetbrains.plugins.github.pullrequest.ui.comment.GHPRHoverableReviewComment +import org.jetbrains.plugins.github.pullrequest.ui.comment.GHPRHoverableReviewCommentImpl import javax.swing.Icon internal sealed interface GHPREditorMappedComponentModel : CodeReviewInlayModel, GHPRHoverableReviewComment { val range: StateFlow?> abstract class Thread(val vm: VM) - : GHPREditorMappedComponentModel, Hideable { + : GHPREditorMappedComponentModel, Hideable, + GHPRHoverableReviewComment by GHPRHoverableReviewCommentImpl() { final override val key: Any = vm.id final override val hiddenState = MutableStateFlow(false) final override fun setHidden(hidden: Boolean) { hiddenState.value = hidden } - - private val _isHovered: MutableStateFlow = MutableStateFlow(false) - override val shouldShowOutline: StateFlow = _isHovered.asStateFlow() - - override fun showOutline(isHovered: Boolean) { - _isHovered.value = isHovered - } } - abstract class NewComment(val vm: VM) : GHPREditorMappedComponentModel { - private val _isHovered: MutableStateFlow = MutableStateFlow(false) - override val shouldShowOutline: StateFlow = _isHovered.asStateFlow() - - override fun showOutline(isHovered: Boolean) { - _isHovered.value = isHovered - } + abstract class NewComment(val vm: VM) + : GHPREditorMappedComponentModel, GHPRHoverableReviewComment by GHPRHoverableReviewCommentImpl() { abstract fun setRange(range: Pair?) - private val _isHidden: MutableStateFlow = MutableStateFlow(false) val isHidden: StateFlow = _isHidden.asStateFlow() fun isHidden(hidden: Boolean) { @@ -50,19 +39,12 @@ internal sealed interface GHPREditorMappedComponentModel : CodeReviewInlayModel, } abstract class AIComment(val vm: GHPRAICommentViewModel) - : GHPREditorMappedComponentModel, Hideable { + : GHPREditorMappedComponentModel, Hideable, GHPRHoverableReviewComment by GHPRHoverableReviewCommentImpl() { final override val key: Any = vm.key final override val hiddenState = MutableStateFlow(false) final override fun setHidden(hidden: Boolean) { hiddenState.value = hidden } - - private val _isHovered: MutableStateFlow = MutableStateFlow(false) - override val shouldShowOutline: StateFlow = _isHovered.asStateFlow() - - override fun showOutline(isHovered: Boolean) { - _isHovered.value = isHovered - } } } @@ -71,4 +53,4 @@ internal fun CoroutineScope.createRenderer(model: GHPREditorMappedComponentModel is GHPREditorMappedComponentModel.Thread<*> -> GHPRReviewThreadEditorInlayRenderer(this, model, model.vm) is GHPREditorMappedComponentModel.NewComment<*> -> GHPRNewCommentEditorInlayRenderer(this, model, model.vm) is GHPREditorMappedComponentModel.AIComment -> GHPRAICommentEditorInlayRenderer(userIcon, model.vm) - } + } \ No newline at end of file diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlaysRenderers.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlaysRenderers.kt index ee217ac1a277..e04ff3b9bd94 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlaysRenderers.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/editor/editorInlaysRenderers.kt @@ -4,17 +4,22 @@ package org.jetbrains.plugins.github.pullrequest.ui.editor import com.intellij.collaboration.async.launchNow import com.intellij.collaboration.ui.codereview.editor.CodeReviewComponentInlayRenderer import com.intellij.collaboration.ui.util.bindContent +import com.intellij.openapi.application.EDT import com.intellij.openapi.observable.util.addMouseHoverListener import com.intellij.ui.components.panels.Wrapper import com.intellij.ui.hover.HoverStateListener +import com.intellij.util.ui.launchOnShow import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.github.ai.GHPRAICommentViewModel import org.jetbrains.plugins.github.ai.GHPRAIReviewExtension +import org.jetbrains.plugins.github.pullrequest.ui.FadeLayerUI import org.jetbrains.plugins.github.pullrequest.ui.comment.GHPRCompactReviewThreadViewModel import org.jetbrains.plugins.github.pullrequest.ui.comment.GHPRHoverableReviewComment import java.awt.Component import javax.swing.Icon +import javax.swing.JLayer @ApiStatus.Internal class GHPRReviewThreadEditorInlayRenderer internal constructor( @@ -22,8 +27,19 @@ class GHPRReviewThreadEditorInlayRenderer internal constructor( hoverableVm: GHPRHoverableReviewComment, vm: GHPRCompactReviewThreadViewModel, ) : CodeReviewComponentInlayRenderer( - GHPRReviewEditorComponentsFactory.createThreadIn(cs, vm).apply { - addMouseHoverListener(null, MouseOverInlayListener(hoverableVm)) + run { + val fadeLayerUI = FadeLayerUI() + val layer = JLayer( + GHPRReviewEditorComponentsFactory.createThreadIn(cs, vm).apply { + addMouseHoverListener(null, MouseOverInlayListener(hoverableVm)) + }, fadeLayerUI + ) + layer.launchOnShow("Inlay.Dimming.${vm::javaClass.name}", Dispatchers.EDT) { + hoverableVm.isDimmed.collect { isDimmed: Boolean -> + fadeLayerUI.setAlpha(if (isDimmed) 0.5f else 1f, layer) + } + } + layer } ) @@ -34,13 +50,23 @@ class GHPRNewCommentEditorInlayRenderer internal constructor( hoverableVm: GHPRHoverableReviewComment, vm: GHPRReviewNewCommentEditorViewModel, ) : CodeReviewComponentInlayRenderer( - GHPRReviewEditorComponentsFactory.createNewCommentIn(cs, vm).also { comp -> - hoverableVm.showOutline(true) - cs.launchNow { - (hoverableVm as GHPREditorMappedComponentModel.NewComment<*>).isHidden.collect { - comp.isVisible = !it + run { + val fadeLayerUI = FadeLayerUI() + val layer = JLayer( + GHPRReviewEditorComponentsFactory.createNewCommentIn(cs, vm).also { + hoverableVm.showOutline(true) + }, fadeLayerUI) + layer.launchOnShow("Inlay.Dimming.${vm::javaClass.name}", Dispatchers.EDT) { + hoverableVm.isDimmed.collect { isDimmed: Boolean -> + fadeLayerUI.setAlpha(if (isDimmed) 0.5f else 1f, layer) } } + cs.launchNow { + (hoverableVm as GHPREditorMappedComponentModel.NewComment<*>).isHidden.collect { + layer.isVisible = !it + } + } + layer } ) @@ -52,8 +78,9 @@ internal class GHPRAICommentEditorInlayRenderer internal constructor(userIcon: I } }) + private class MouseOverInlayListener(val vm: GHPRHoverableReviewComment) : HoverStateListener() { override fun hoverChanged(component: Component, hovered: Boolean) { vm.showOutline(hovered) } -} \ No newline at end of file +}