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 f035da1e0982..b412b5b239dd 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 @@ -2,9 +2,16 @@ package com.intellij.openapi.progress.util import com.intellij.CommonBundle +import com.intellij.diagnostic.LoadingState import com.intellij.openapi.application.impl.getGlobalThreadingSupport +import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry +import com.intellij.util.ui.AsyncProcessIcon import org.jetbrains.annotations.ApiStatus +import java.awt.KeyboardFocusManager +import javax.swing.JFrame +import javax.swing.SwingUtilities /** * [PotemkinProgress] done right. @@ -19,15 +26,40 @@ import org.jetbrains.annotations.ApiStatus * ``` * * In this situation, we have a freeze, because EDT is blocked on a lock in a single event. - * Instead of blocking, we can show a "modal" progress bar, and provide an impression that IDE is not dead. + * Instead of blocking, we can show a "modal" progress and provide an impression that IDE is not dead. * - * This progress bar starts when EDT is going to be blocked on the RWI lock, and finished when the required lock gets acquired. + * This progress starts when EDT is going to be blocked on the RWI lock, and finished when the required lock gets acquired. */ @ApiStatus.Internal object SuvorovProgress { @JvmStatic fun dispatchEventsUntilConditionCompletes(shouldTerminate: () -> Boolean) { + val value = if (!LoadingState.COMPONENTS_LOADED.isOccurred) { + "None" + } + else { + Registry.get("ide.freeze.fake.progress.kind").selectedOption + } + when (value) { + "None" -> { + while (!shouldTerminate()) { + sleep() + } + } + "Spinning" -> if (Registry.`is`("editor.allow.raw.access.on.edt")) { + showSpinningProgress(shouldTerminate) + } + else { + thisLogger().warn("Spinning progress would not work without enabled registry value `editor.allow.raw.access.on.edt`") + showBarProgress(shouldTerminate) + } + "Bar" -> showBarProgress(shouldTerminate) + else -> throw IllegalArgumentException("Unknown value for registry key `ide.freeze.fake.progress.kind`: $value") + } + } + + private fun showBarProgress(shouldTerminate: () -> Boolean) { // some focus machinery may require Write-Intent read action // we need to remove it from there getGlobalThreadingSupport().relaxPreventiveLockingActions { @@ -35,13 +67,13 @@ object SuvorovProgress { // At this point, we have too many events that acquire the Write-Intent read lock, // so we need to have a strict control over events that are executing on EDT to avoid stack overflow of SuvorovProgresses val potemkinProgress = PotemkinProgress(CommonBundle.message("title.long.non.interactive.progress"), null, null, null) - potemkinProgress.setAllowThreadDumpButton() potemkinProgress.start() try { do { potemkinProgress.interact() - Thread.sleep(10); // avoid touching the progress too much - } while (!shouldTerminate()) + sleep(); // avoid touching the progress too much + } + while (!shouldTerminate()) } finally { // we cannot acquire WI on closing @@ -52,4 +84,67 @@ object SuvorovProgress { } } } + + private fun showSpinningProgress(shouldTerminate: () -> Boolean) { + getGlobalThreadingSupport().relaxPreventiveLockingActions { + + val icon = AsyncProcessIcon.createBig("Suvorov progress") + val window = SwingUtilities.getRootPane(KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner) + + if (window == null) { + while (!shouldTerminate()) { + sleep() + } + return@relaxPreventiveLockingActions + } + + icon.size = icon.preferredSize + icon.isVisible = true + + val disposer = Disposer.newDisposable() + val stealer = PotemkinProgress.startStealingInputEvents({ event -> + val source = event.source + // we want to permit resizing and moving the IDE window + if (source is JFrame) { + source.dispatchEvent(event) + } + }, disposer) + + val host = window.layeredPane + host.add(icon) + // Swing tries its best to not draw anything that may not be on screen. + // We need to trick it to mandatory drawing, and for this reason we make the host components opaque. + val oldHostVisibile = host.isVisible + val oldHostOpaque = host.isOpaque + host.isVisible = true + host.isOpaque = true + + icon.updateUI() + icon.setBounds((window.width - icon.width) / 2, (window.height - icon.height) / 2, icon.width, icon.height) + icon.resume() + + try { + do { + icon.validate() + icon.tickAnimation() + + stealer.dispatchEvents(0) + sleep() // avoid touching the progress too much + } + while (!shouldTerminate()) + } + finally { + icon.suspend() + host.isVisible = oldHostVisibile + host.isOpaque = oldHostOpaque + icon.isVisible = false + Disposer.dispose(disposer) + host.remove(icon) + } + } + } + + private fun sleep() { + Thread.sleep(5) + } } diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 700885344fde..d3b0ee7abc46 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -323,6 +323,9 @@ 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.freeze.fake.progress.kind=[None*|Bar|Spinning] +ide.freeze.fake.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.tooltip.animationCycle=150 ide.tooltip.initialDelay=1200 ide.tooltip.initialDelay.highlighter=150