diff --git a/platform/platform-impl/resources/messages/DiagnosticBundle.properties b/platform/platform-impl/resources/messages/DiagnosticBundle.properties
index f983a5f7ae4f..2959d584457c 100644
--- a/platform/platform-impl/resources/messages/DiagnosticBundle.properties
+++ b/platform/platform-impl/resources/messages/DiagnosticBundle.properties
@@ -138,6 +138,9 @@ change.memory.apply=Save and Restart
change.memory.exit=Save and Exit
change.memory.low=The value should be greater than {0}
+freeze.popup.application.is.not.responding={0} is not responding
+freeze.popup.dump.threads.suggestion=Dump threads
+
notification.group.defender.config=Microsoft Defender may affect IDE
defender.config.prompt=To avoid performance issues, exclude the IDE and the project folders from the Real-Time Protection:\
{0}
\
diff --git a/platform/platform-impl/src/com/intellij/openapi/progress/util/SuvorovProgress.kt b/platform/platform-impl/src/com/intellij/openapi/progress/util/SuvorovProgress.kt
index 3aacbc91dd52..51313b467852 100644
--- a/platform/platform-impl/src/com/intellij/openapi/progress/util/SuvorovProgress.kt
+++ b/platform/platform-impl/src/com/intellij/openapi/progress/util/SuvorovProgress.kt
@@ -3,13 +3,19 @@ package com.intellij.openapi.progress.util
import com.intellij.CommonBundle
import com.intellij.diagnostic.LoadingState
+import com.intellij.diagnostic.PerformanceWatcher
import com.intellij.ide.IdeEventQueue
+import com.intellij.ide.actions.RevealFileAction
import com.intellij.openapi.Disposable
+import com.intellij.openapi.actionSystem.KeyboardShortcut
+import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.impl.fus.FreezeUiUsageCollector
+import com.intellij.openapi.progress.util.ui.NiceOverlayUi
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.locking.impl.getGlobalThreadingSupport
+import com.intellij.ui.KeyStrokeAdapter
import com.intellij.util.ui.AsyncProcessIcon
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.InternalCoroutinesApi
@@ -18,6 +24,8 @@ import org.jetbrains.annotations.ApiStatus
import java.awt.AWTEvent
import java.awt.KeyboardFocusManager
import java.awt.event.InvocationEvent
+import java.awt.event.KeyEvent
+import java.awt.event.MouseEvent
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicReference
import javax.swing.JFrame
@@ -78,11 +86,57 @@ object SuvorovProgress {
thisLogger().warn("Spinning progress would not work without enabled registry value `editor.allow.raw.access.on.edt`")
processInvocationEventsWithoutDialog(awaitedValue, Int.MAX_VALUE)
}
+ "NiceOverlay" -> showNiceOverlay(awaitedValue)
"Bar", "Overlay" -> showPotemkinProgress(awaitedValue, isBar = value == "Bar")
else -> throw IllegalArgumentException("Unknown value for registry key `ide.freeze.fake.progress.kind`: $value")
}
}
+ private fun showNiceOverlay(awaitedValue: Deferred<*>) {
+ val niceOverlay = NiceOverlayUi()
+
+ val disposable = Disposer.newDisposable()
+ val stealer = PotemkinProgress.startStealingInputEvents(
+ { event ->
+ var dumpThreads = false
+ if (event is MouseEvent && event.id == MouseEvent.MOUSE_CLICKED) {
+ event.consume()
+ val reaction = niceOverlay.mouseClicked(event.point)
+ when (reaction) {
+ NiceOverlayUi.ClickOutcome.DUMP_THREADS -> dumpThreads = true
+ NiceOverlayUi.ClickOutcome.CLOSED, NiceOverlayUi.ClickOutcome.NOTHING -> Unit
+ }
+ }
+ if (event is MouseEvent && event.id == MouseEvent.MOUSE_MOVED) {
+ event.consume()
+ niceOverlay.mouseMoved(event.point)
+ }
+ if (event is KeyEvent && niceOverlay.dumpThreadsButtonShortcut == KeyStrokeAdapter.getDefaultKeyStroke(event)?.let { KeyboardShortcut(it, null) }) {
+ event.consume()
+ dumpThreads = true
+ }
+ if (dumpThreads) {
+ ApplicationManager.getApplication().executeOnPooledThread(Runnable {
+ val dumpDir = PerformanceWatcher.getInstance().dumpThreads("freeze-popup", true, false)
+ if (dumpDir != null) {
+ RevealFileAction.openFile(dumpDir)
+ }
+ })
+ }
+ }, disposable)
+
+ try {
+ while (!awaitedValue.isCompleted) {
+ niceOverlay.redrawMainComponent()
+ stealer.dispatchEvents(0)
+ Thread.sleep(10)
+ }
+ }
+ finally {
+ Disposer.dispose(disposable)
+ }
+ }
+
private fun showPotemkinProgress(awaitedValue: Deferred<*>, isBar: Boolean) {
// some focus machinery may require Write-Intent read action
// we need to remove it from there
diff --git a/platform/platform-impl/src/com/intellij/openapi/progress/util/ui/NiceOverlayUi.kt b/platform/platform-impl/src/com/intellij/openapi/progress/util/ui/NiceOverlayUi.kt
new file mode 100644
index 000000000000..ae3c94cd3aa4
--- /dev/null
+++ b/platform/platform-impl/src/com/intellij/openapi/progress/util/ui/NiceOverlayUi.kt
@@ -0,0 +1,324 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.openapi.progress.util.ui
+
+import com.intellij.diagnostic.DiagnosticBundle
+import com.intellij.openapi.actionSystem.KeyboardShortcut
+import com.intellij.openapi.application.ApplicationInfo
+import com.intellij.openapi.keymap.KeymapUtil
+import com.intellij.openapi.ui.impl.ShadowBorderPainter
+import com.intellij.openapi.util.NlsSafe
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.openapi.wm.IdeFrame
+import com.intellij.ui.ColorUtil
+import com.intellij.ui.JBColor
+import com.intellij.ui.JreHiDpiUtil
+import com.intellij.ui.paint.PaintUtil
+import com.intellij.ui.scale.JBUIScale
+import com.intellij.util.ui.GraphicsUtil
+import com.intellij.util.ui.JBUI
+import com.intellij.util.ui.UIUtil
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.annotations.Nls
+import java.awt.*
+import java.awt.event.InputEvent
+import java.awt.event.KeyEvent
+import java.awt.image.BufferedImage
+import java.awt.image.MultiResolutionImage
+import javax.swing.JRootPane
+import javax.swing.KeyStroke
+import javax.swing.SwingUtilities
+import javax.swing.UIManager
+
+/**
+ * Implementation of Popup by UX-3484
+ * We have to use AWT here, as we cannot spin the EventQueue for Swing
+ */
+@ApiStatus.Internal
+class NiceOverlayUi {
+ private val rootPane: JRootPane = SwingUtilities.getRootPane(KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner)
+
+
+ private val mainText = DiagnosticBundle.message("freeze.popup.application.is.not.responding", ApplicationInfo.getInstance().versionName)
+ private val dumpThreadsButtonText = DiagnosticBundle.message("freeze.popup.dump.threads.suggestion")
+ val dumpThreadsButtonShortcut: KeyboardShortcut = KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_D, (if (SystemInfo.isMac) InputEvent.META_DOWN_MASK else InputEvent.CTRL_DOWN_MASK)), null)
+ private val dumpThreadsButtonShortcutText = KeymapUtil.getShortcutText(dumpThreadsButtonShortcut)
+
+
+ private val popupHeight: Int = JBUIScale.scale(32)
+ private val horizontalInset = JBUIScale.scale(12)
+ private val gapBetweenText1AndText2 = JBUIScale.scale(15)
+ private val gapBetweenText2AndText3 = JBUIScale.scale(3)
+ private val separatorInset = JBUIScale.scale(10)
+ private val closeIconLength = JBUIScale.scale(10)
+ private val border = JBUIScale.scale(8)
+ private val separatorHeight = JBUIScale.scale(20)
+ private val buttonMargin = JBUIScale.scale(4)
+ private val buttonHeight = JBUIScale.scale(20)
+ private val buttonArc = JBUIScale.scale(5)
+ private val font = JBUI.Fonts.label()
+ private val shadowSize: Int = JBUIScale.scale(3)
+
+
+ private val bgColor: Color = JBColor.namedColor("Notification.background")
+ private val borderColor: Color = JBColor.namedColor("Notification.borderColor")
+ private val fgColor: Color = ColorUtil.toAlpha(UIManager.getColor("Notification.foreground"), 220)
+ private val fgShortcutColor: Color = ColorUtil.toAlpha(fgColor, 100)
+ private val buttonHoverColor: Color = JBUI.CurrentTheme.ActionButton.pressedBackground()
+
+
+ private val currentFontMetrics: FontMetrics = GraphicsUtil.safelyGetGraphics(rootPane).getFontMetrics(font)
+ private val yOffsetOfTextInPopup: Int = popupHeight / 2 + currentFontMetrics.ascent / 2 - JBUIScale.scale(1)
+
+ private val popupWidth: Int = horizontalInset + getTextLength(mainText) + getTextLength(dumpThreadsButtonText) + getTextLength(dumpThreadsButtonShortcutText) + gapBetweenText1AndText2 + gapBetweenText2AndText3 + separatorInset + 1 + separatorInset + closeIconLength + horizontalInset
+
+ // offsets relative to the containing component
+ private val popupOffsetX: Int = rootPane.width / 2 - popupWidth / 2
+ private val popupOffsetY: Int = run {
+ val frame = SwingUtilities.getAncestorOfClass(IdeFrame::class.java, rootPane) as? IdeFrame
+ frame?.statusBar?.component?.location?.y ?: (rootPane.height / 2 - popupHeight / 2)
+ }
+
+ // regions of the buttons relative to the containing component
+ private val locationOfCross: Rectangle
+ private val locationOfThreadDumpButton: Rectangle
+
+ /**
+ * The users might want to close the freeze popup. In this case, we need to draw the contents of whatever is behind the popup
+ * We cannot ask Swing to repaint the region, as we cannot exit the EDT event we are currently in.
+ * We cannot repaint the region manually, as the underlying painting logic might try to access the read/write lock, and we would get a deadlock.
+ *
+ * But we know that the UI is fronzen, hence we do a trick: we take a screenshot of the region where the freeze popup is located,
+ * and draw it back when the user decides to close the popup.
+ */
+ private val screenshot: MultiResolutionImage
+
+ /**
+ * The location of popup including its shadow; we need it to replace it with the screehshot later
+ */
+ private val popupWithShadowLocation: Rectangle = Rectangle(popupOffsetX - shadowSize, popupOffsetY - shadowSize, popupWidth + shadowSize * 2, popupWidth + shadowSize * 2)
+
+ /**
+ * The "model" of the popup
+ */
+ private var closeButtonHovered: Boolean = false
+ private var threadDumpButtonHovered: Boolean = false
+ private var closed = false
+
+ init {
+ screenshot = takeScreenshot()
+
+ drawShadow()
+
+ val startOfThreadDumpText = popupOffsetX + horizontalInset + getTextLength(mainText) + gapBetweenText1AndText2
+ val offsetFromTop = popupOffsetY + (popupHeight - closeIconLength) / 2 - JBUIScale.scale(2)
+ locationOfThreadDumpButton = Rectangle(
+ startOfThreadDumpText - buttonMargin,
+ offsetFromTop - buttonMargin,
+ getTextLength(dumpThreadsButtonText) + gapBetweenText2AndText3 + getTextLength(dumpThreadsButtonShortcutText) + buttonMargin * 2,
+ buttonHeight)
+ locationOfCross = Rectangle(
+ startOfThreadDumpText + getTextLength(dumpThreadsButtonText) + gapBetweenText2AndText3 + getTextLength(dumpThreadsButtonShortcutText) + separatorInset + JBUIScale.scale(1) + separatorInset - buttonMargin,
+ offsetFromTop - buttonMargin,
+ closeIconLength + buttonMargin * 2,
+ buttonHeight)
+
+ redrawMainComponent()
+ }
+
+ private fun drawShadow() {
+
+ val backBuffer = drawPopupContents()
+
+ val originalGraphics = GraphicsUtil.safelyGetGraphics(rootPane)
+ try {
+ originalGraphics.translate(popupOffsetX, popupOffsetY)
+ val shadow = ShadowBorderPainter.createShadow(backBuffer, 0, 0, false, shadowSize)
+
+ // unfortunately, our shadow-handling code is not scale-aware
+ if (JreHiDpiUtil.isJreHiDPI(originalGraphics as Graphics2D)) {
+ val scaledGraphics = originalGraphics.create() as Graphics2D
+ val s: Float = 1 / JBUIScale.sysScale(originalGraphics)
+ scaledGraphics.scale(s.toDouble(), s.toDouble())
+ UIUtil.drawImage(scaledGraphics, shadow.getImage(), shadow.getX() + shadowSize + 1, shadow.getY() - shadowSize + 1, null)
+ scaledGraphics.dispose()
+ }
+ else {
+ UIUtil.drawImage(originalGraphics, shadow.image, shadow.x, shadow.y, null)
+ }
+ }
+ finally {
+ originalGraphics.dispose()
+ }
+ }
+
+ /**
+ * Redraws the freeze popup based on its current state.
+ */
+ fun redrawMainComponent() {
+ if (closed) {
+ return
+ }
+
+ val popupImage = drawPopupContents()
+
+ val originalGraphics = GraphicsUtil.safelyGetGraphics(rootPane)
+ try {
+ originalGraphics.translate(popupOffsetX, popupOffsetY)
+ UIUtil.drawImage(originalGraphics, popupImage, 0, 0, null)
+ }
+ finally {
+ originalGraphics.dispose()
+ }
+ }
+
+ private fun takeScreenshot(): MultiResolutionImage {
+ val window = SwingUtilities.getWindowAncestor(rootPane)
+ return Robot().createMultiResolutionScreenCapture(Rectangle(window.x + popupWithShadowLocation.x, window.y + popupWithShadowLocation.y, popupWithShadowLocation.width, popupWithShadowLocation.height))
+ }
+
+ /**
+ * @param point the point relative to the containing component
+ */
+ fun mouseMoved(point: Point) {
+ closeButtonHovered = locationOfCross.contains(point)
+ threadDumpButtonHovered = locationOfThreadDumpButton.contains(point)
+ }
+
+ enum class ClickOutcome {
+ NOTHING,
+ DUMP_THREADS,
+ CLOSED,
+ }
+
+ /**
+ * @param point the point relative to the containing component
+ */
+ fun mouseClicked(point: Point): ClickOutcome {
+ if (locationOfCross.contains(point)) {
+ closed = true
+ restoreScreenshot()
+ return ClickOutcome.CLOSED
+ }
+ else if (locationOfThreadDumpButton.contains(point)) {
+ return ClickOutcome.DUMP_THREADS
+ }
+ else {
+ return ClickOutcome.NOTHING
+ }
+ }
+
+ private fun restoreScreenshot() {
+ val innerGraphics = GraphicsUtil.safelyGetGraphics(rootPane)
+ try {
+ val variant = screenshot.getResolutionVariant(popupWithShadowLocation.width.toDouble() * JBUIScale.sysScale().toDouble(), popupWithShadowLocation.height.toDouble() * JBUIScale.sysScale().toDouble())
+ if (JreHiDpiUtil.isJreHiDPI(innerGraphics as Graphics2D)) {
+ val scaledGraphics = innerGraphics.create() as Graphics2D
+ val s: Float = 1 / JBUIScale.sysScale(innerGraphics)
+ scaledGraphics.scale(s.toDouble(), s.toDouble())
+ UIUtil.drawImage(scaledGraphics, variant, (popupWithShadowLocation.x * JBUIScale.sysScale().toDouble()).toInt(), (popupWithShadowLocation.y * JBUIScale.sysScale().toDouble()).toInt(), null)
+ scaledGraphics.dispose()
+ }
+ else {
+ UIUtil.drawImage(innerGraphics, variant, popupWithShadowLocation.x, popupWithShadowLocation.y, null)
+ }
+ }
+ finally {
+ innerGraphics.dispose()
+ }
+ }
+
+ /**
+ * Computes a Hi-DPI image of the opaque part of the freeze popup. The image does not include shadow.
+ */
+ private fun drawPopupContents(): BufferedImage {
+
+ val backBuffer = UIUtil.createImage(rootPane.graphicsConfiguration, popupWidth.toDouble(), popupHeight.toDouble(),
+ BufferedImage.TYPE_INT_ARGB, PaintUtil.RoundingMode.ROUND)
+ val graphics: Graphics2D = backBuffer.createGraphics()
+ try {
+ GraphicsUtil.setupAAPainting(graphics)
+ graphics.font = font
+
+ graphics.drawBackgroundShape()
+ graphics.drawBorder()
+ var accumulatingOffsetX = horizontalInset
+
+ val lengthOfText1 = SwingUtilities.computeStringWidth(currentFontMetrics, mainText)
+ val lengthOfText2 = SwingUtilities.computeStringWidth(currentFontMetrics, dumpThreadsButtonText)
+ val lengthOfText3 = SwingUtilities.computeStringWidth(currentFontMetrics, dumpThreadsButtonShortcutText)
+
+ graphics.drawMainText(accumulatingOffsetX, mainText)
+
+ accumulatingOffsetX += lengthOfText1 + gapBetweenText1AndText2
+
+ if (threadDumpButtonHovered) {
+ graphics.paintButtonBackground(accumulatingOffsetX, lengthOfText2 + gapBetweenText2AndText3 + lengthOfText3)
+ }
+
+ graphics.drawMainText(accumulatingOffsetX, dumpThreadsButtonText)
+ accumulatingOffsetX += lengthOfText2 + gapBetweenText2AndText3
+ accumulatingOffsetX += graphics.drawShortcut(accumulatingOffsetX, dumpThreadsButtonShortcutText)
+ accumulatingOffsetX += graphics.drawSeparator(accumulatingOffsetX)
+
+ graphics.color = fgColor
+
+ if (closeButtonHovered) {
+ graphics.paintButtonBackground(accumulatingOffsetX, closeIconLength)
+ }
+ graphics.drawCloseButton(accumulatingOffsetX)
+ }
+ finally {
+ graphics.dispose()
+ }
+
+ return backBuffer
+ }
+
+ private fun getTextLength(text: @Nls String): Int {
+ return SwingUtilities.computeStringWidth(currentFontMetrics, text)
+ }
+
+ private fun Graphics2D.drawBackgroundShape() {
+ color = bgColor
+ fillRoundRect(0, 0, popupWidth, popupHeight, border, border)
+ }
+
+ private fun Graphics2D.drawBorder() {
+ stroke = BasicStroke(JBUIScale.scale(1.0f))
+ color = borderColor
+ drawRoundRect(0, 0, popupWidth, popupHeight, border, border)
+ }
+
+ private fun Graphics2D.drawMainText(startX: Int, text: @Nls String) {
+ color = fgColor
+ drawString(text, startX.toFloat(), yOffsetOfTextInPopup.toFloat())
+ }
+
+ private fun Graphics2D.drawShortcut(startX: Int, shortcut: @NlsSafe String): /* length of the drawn string */ Int {
+ color = fgShortcutColor
+ drawString(shortcut, startX, yOffsetOfTextInPopup)
+ return SwingUtilities.computeStringWidth(currentFontMetrics, shortcut)
+ }
+
+ private fun Graphics2D.drawSeparator(startX: Int): /* length of separator including insets */ Int {
+ val actualStart = startX + separatorInset
+ color = ColorUtil.toAlpha(fgColor, 50)
+ stroke = BasicStroke(JBUIScale.scale(1.0f))
+ val offsetFromTop = (popupHeight - separatorHeight) / 2
+ drawLine(actualStart, offsetFromTop, actualStart, popupHeight - offsetFromTop)
+ return separatorInset + JBUIScale.scale(1) + separatorInset
+ }
+
+ private fun Graphics2D.drawCloseButton(startX: Int) {
+ color = fgColor
+ val offsetFromTop = (popupHeight - closeIconLength) / 2
+ drawLine(startX, offsetFromTop, startX + closeIconLength, popupHeight - offsetFromTop)
+ drawLine(startX + closeIconLength, offsetFromTop, startX, popupHeight - offsetFromTop)
+ }
+
+ private fun Graphics2D.paintButtonBackground(startX: Int, lengthOfButton: Int) {
+ color = buttonHoverColor
+ fillRoundRect(startX - buttonMargin, (popupHeight - buttonHeight) / 2, lengthOfButton + buttonMargin * 2, buttonHeight, buttonArc, buttonArc)
+ }
+
+}
\ No newline at end of file
diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties
index 2da3b2460187..af8313aeaf74 100644
--- a/platform/util/resources/misc/registry.properties
+++ b/platform/util/resources/misc/registry.properties
@@ -323,7 +323,7 @@ ide.preferred.scrollable.viewport.visible.rows.description=Specifies default num
ide.preferred.scrollable.viewport.extra.space=true
ide.preferred.scrollable.viewport.extra.space.description=Adds an extra space to show that a list can be scrolled
-ide.suvorov.progress.kind=[None|Bar|Spinning|Overlay*]
+ide.suvorov.progress.kind=[None|Bar|Spinning|Overlay*|NiceOverlay]
ide.suvorov.progress.kind.description=Kind of progress that is used to make an impression of live IDE when it is freezed on acquisition of locks
ide.suvorov.progress.showing.delay.ms=1000