diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/ReloadClassesWorker.java b/java/debugger/impl/src/com/intellij/debugger/impl/ReloadClassesWorker.java index ef6ba6c0988c..b0119f69f4b3 100644 --- a/java/debugger/impl/src/com/intellij/debugger/impl/ReloadClassesWorker.java +++ b/java/debugger/impl/src/com/intellij/debugger/impl/ReloadClassesWorker.java @@ -137,10 +137,12 @@ class ReloadClassesWorker { final int partiallyRedefinedClassesCount = redefineProcessor.getPartiallyRedefinedClassesCount(); if (partiallyRedefinedClassesCount == 0) { - myProgress.addMessage( - myDebuggerSession, MessageCategory.INFORMATION, - JavaDebuggerBundle.message("status.classes.reloaded", redefineProcessor.getProcessedClassesCount()) - ); + if (!Registry.is("debugger.hotswap.floating.toolbar")) { + myProgress.addMessage( + myDebuggerSession, MessageCategory.INFORMATION, + JavaDebuggerBundle.message("status.classes.reloaded", redefineProcessor.getProcessedClassesCount()) + ); + } } else { final String message = JavaDebuggerBundle.message( diff --git a/platform/xdebugger-api/resources/messages/XDebuggerBundle.properties b/platform/xdebugger-api/resources/messages/XDebuggerBundle.properties index b18ee7905217..32cda4476d4d 100644 --- a/platform/xdebugger-api/resources/messages/XDebuggerBundle.properties +++ b/platform/xdebugger-api/resources/messages/XDebuggerBundle.properties @@ -334,3 +334,4 @@ xdebugger.visualized.text.name.jwt=JWT xdebugger.visualized.text.name.url=URL (decoded) xdebugger.hotswap.code.changed=Code changed: +xdebugger.hotswap.status.success=Code has been reloaded diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapSessionManager.kt b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapSessionManager.kt index f3d520fec687..f7391e481895 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapSessionManager.kt +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapSessionManager.kt @@ -54,6 +54,7 @@ class HotSwapSession(val project: Project, internal val provider: HotSwapProv internal val coroutineScope = parentScope.childScope("HotSwapSession $this") private val hasActiveChanges = AtomicBoolean() private lateinit var changesCollector: SourceFileChangesCollector + @Volatile internal var currentStatus: HotSwapVisibleStatus = HotSwapVisibleStatus.NO_CHANGES private set(value) { @@ -91,6 +92,7 @@ class HotSwapSession(val project: Project, internal val provider: HotSwapProv fun createStatusListener() = object : HotSwapResultListener { override fun onCompleted() { completeHotSwap() + HotSwapStatusNotificationManager.getInstance(project).showSuccessNotification(coroutineScope) } override fun onFailed() { diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapStatusNotificationManager.kt b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapStatusNotificationManager.kt new file mode 100644 index 000000000000..331bd79f8586 --- /dev/null +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapStatusNotificationManager.kt @@ -0,0 +1,70 @@ +// 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.xdebugger.impl.hotswap + +import com.intellij.execution.multilaunch.design.components.RoundedCornerBorder +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.BalloonImpl +import com.intellij.ui.JBColor +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.JBUI +import com.intellij.xdebugger.XDebuggerBundle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.jetbrains.annotations.ApiStatus +import java.awt.BorderLayout +import java.awt.Point +import javax.swing.JPanel +import javax.swing.SwingConstants +import kotlin.time.Duration.Companion.seconds + +@ApiStatus.Internal +@Service(Service.Level.PROJECT) +class HotSwapStatusNotificationManager(private val project: Project) : Disposable.Default { + companion object { + fun getInstance(project: Project): HotSwapStatusNotificationManager = project.service() + } + + fun showSuccessNotification(scope: CoroutineScope, disposable: Disposable? = null) { + scope.launch(Dispatchers.EDT) { + val frame = WindowManager.getInstance().getFrame(project) ?: return@launch + val factory = JBPopupFactory.getInstance() ?: return@launch + + val balloon = factory.createBalloonBuilder(SuccessfulHotSwapComponent()) + .setBorderColor(JBColor.border()) + .setCornerRadius(JBUI.scale(RADIUS)) + .setBorderInsets(JBUI.emptyInsets()) + .setFadeoutTime(NOTIFICATION_TIME_SECONDS.seconds.inWholeMilliseconds) + .setBlockClicksThroughBalloon(true) + .setHideOnAction(false) + .setHideOnClickOutside(false) + .apply { if (disposable != null) setDisposable(disposable) } + .createBalloon() + + if (balloon is BalloonImpl) { + balloon.setShowPointer(false) + } + balloon.show(RelativePoint(frame, Point(frame.width / 2, 2 * frame.height / 3)), Balloon.Position.below) + } + } +} + +private const val RADIUS = 10 +private const val NOTIFICATION_TIME_SECONDS = 3 + +private class SuccessfulHotSwapComponent : JPanel(BorderLayout()) { + init { + isOpaque = false + border = RoundedCornerBorder(JBUI.scale(RADIUS)) + add(JBLabel(XDebuggerBundle.message("xdebugger.hotswap.status.success"), AllIcons.Status.Success, SwingConstants.CENTER)) + } +}