From f15f4236de8a592c53ccc4ff0313aef99c7a378a Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov <654232+AlexeyTsvetkov@users.noreply.github.com> Date: Mon, 19 Jul 2021 14:44:24 +0300 Subject: [PATCH] Better preview progress indication (#901) * Indicate preview progress with loading panel * Avoid sending repeated preview requests --- .../ui/tooling/preview/rpc/PreviewListener.kt | 51 +++++++++ .../ui/tooling/preview/rpc/PreviewManager.kt | 47 ++++---- .../tooling/preview/rpc/RemotePreviewHost.kt | 3 +- .../ui/tooling/preview/rpc/RenderedFrame.kt | 11 ++ .../ui/tooling/preview/rpc/commands.kt | 71 +++++++----- .../desktop/ide/preview/PreviewPanel.kt | 3 + .../ide/preview/PreviewStateService.kt | 104 ++++++++++++------ .../desktop/ide/preview/PreviewToolWindow.kt | 8 +- .../desktop/ide/preview/RunPreviewAction.kt | 13 ++- 9 files changed, 221 insertions(+), 90 deletions(-) create mode 100644 gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt new file mode 100644 index 0000000000..017823df3f --- /dev/null +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ui.tooling.preview.rpc + +interface PreviewListener { + fun onNewBuildRequest() + fun onFinishedBuild(success: Boolean) + fun onNewRenderRequest(previewRequest: FrameRequest) + fun onRenderedFrame(frame: RenderedFrame) +} + +open class PreviewListenerBase : PreviewListener { + override fun onNewBuildRequest() {} + override fun onFinishedBuild(success: Boolean) {} + + override fun onNewRenderRequest(previewRequest: FrameRequest) {} + override fun onRenderedFrame(frame: RenderedFrame) {} +} + +class CompositePreviewListener : PreviewListener { + private val listeners = arrayListOf() + + override fun onNewBuildRequest() { + forEachListener { it.onNewBuildRequest() } + } + + override fun onFinishedBuild(success: Boolean) { + forEachListener { it.onFinishedBuild(success) } + } + + override fun onNewRenderRequest(previewRequest: FrameRequest) { + forEachListener { it.onNewRenderRequest(previewRequest) } + } + + override fun onRenderedFrame(frame: RenderedFrame) { + forEachListener { it.onRenderedFrame(frame) } + } + + @Synchronized + fun addListener(listener: PreviewListener) { + listeners.add(listener) + } + + @Synchronized + private fun forEachListener(fn: (PreviewListener) -> Unit) { + listeners.forEach(fn) + } +} diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt index 7ceeae5aeb..0965f5219e 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt @@ -29,6 +29,7 @@ data class FrameConfig(val width: Int, val height: Int, val scale: Double?) { } data class FrameRequest( + val id: Long, val composableFqName: String, val frameConfig: FrameConfig ) @@ -47,7 +48,9 @@ private data class RunningPreview( get() = connection.isAlive && process.isAlive } -class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : PreviewManager { +class PreviewManagerImpl( + private val previewListener: PreviewListener = PreviewListenerBase() +) : PreviewManager { private val log = PrintStreamLogger("SERVER") private val previewSocket = newServerSocket() private val gradleCallbackSocket = newServerSocket() @@ -59,8 +62,9 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev private val previewClasspath = AtomicReference(null) private val previewFqName = AtomicReference(null) private val previewFrameConfig = AtomicReference(null) - private val frameRequest = AtomicReference(null) - private val shouldRequestFrame = AtomicBoolean(false) + private val inProcessRequest = AtomicReference(null) + private val processedRequest = AtomicReference(null) + private val userRequestCount = AtomicLong(0) private val runningPreview = AtomicReference(null) private val threads = arrayListOf() @@ -97,14 +101,12 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev val frameConfig = previewFrameConfig.get() if (classpath != null && frameConfig != null && fqName != null) { - val request = FrameRequest(fqName, frameConfig) - if (shouldRequestFrame.get() && frameRequest.get() == null) { - if (shouldRequestFrame.compareAndSet(true, false)) { - if (frameRequest.compareAndSet(null, request)) { - sendPreviewRequest(classpath, request) - } else { - shouldRequestFrame.compareAndSet(false, true) - } + val request = FrameRequest(userRequestCount.get(), fqName, frameConfig) + val prevRequest = processedRequest.get() + if (inProcessRequest.get() == null && request != prevRequest) { + if (inProcessRequest.compareAndSet(null, request)) { + previewListener.onNewRenderRequest(request) + sendPreviewRequest(classpath, request) } } } @@ -114,10 +116,11 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev private val receivePreviewResponseThread = repeatWhileAliveThread("receivePreviewResponse") { withLivePreviewConnection { receiveFrame { renderedFrame -> - frameRequest.get()?.let { request -> - frameRequest.compareAndSet(request, null) + inProcessRequest.get()?.let { request -> + processedRequest.set(request) + inProcessRequest.compareAndSet(request, null) } - onNewFrame(renderedFrame) + previewListener.onRenderedFrame(renderedFrame) } } } @@ -125,13 +128,14 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev private val gradleCallbackThread = repeatWhileAliveThread("gradleCallback") { tryAcceptConnection(gradleCallbackSocket, "GRADLE_CALLBACK")?.let { connection -> while (isAlive.get() && connection.isAlive) { - connection.receiveConfigFromGradle( - onPreviewClasspath = { previewClasspath.set(it) }, - onPreviewHostConfig = { previewHostConfig.set(it) }, - onPreviewFqName = { previewFqName.set(it) } - ) - shouldRequestFrame.set(true) - sendPreviewRequestThread.interrupt() + val config = connection.receiveConfigFromGradle() + if (config != null) { + previewClasspath.set(config.previewClasspath) + previewFqName.set(config.previewFqName) + previewHostConfig.set(config.previewHostConfig) + userRequestCount.incrementAndGet() + sendPreviewRequestThread.interrupt() + } } } } @@ -191,7 +195,6 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev override fun updateFrameConfig(frameConfig: FrameConfig) { previewFrameConfig.set(frameConfig) - shouldRequestFrame.set(true) sendPreviewRequestThread.interrupt() } diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt index f11f144fd0..9f5d6a5318 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt @@ -62,6 +62,7 @@ internal class PreviewHost(private val log: PreviewLogger, connection: RemoteCon try { val classpath = previewClasspath.get() val request = previewRequest.get() + log { "request != null == ${request != null} && classpath != null == ${classpath != null}" } if (classpath != null && request != null) { if (previewRequest.compareAndSet(request, null)) { val bytes = renderFrame(classpath, request) @@ -126,7 +127,7 @@ internal class PreviewHost(private val log: PreviewLogger, connection: RemoteCon val possibleCandidates = previewFacade.methods.filter { it.name == "render" } throw RuntimeException("Could not find method '$signature'. Possible candidates: \n${possibleCandidates.joinToString("\n") { "* ${it}" }}", e) } - val (fqName, frameConfig) = request + val (id, fqName, frameConfig) = request val scaledWidth = frameConfig.scaledWidth val scaledHeight = frameConfig.scaledHeight val scale = frameConfig.scale diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt index 838c7f9cfe..93c04aa5c4 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt @@ -5,6 +5,11 @@ package org.jetbrains.compose.desktop.ui.tooling.preview.rpc +import java.awt.Dimension +import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import javax.imageio.ImageIO + data class RenderedFrame( val bytes: ByteArray, val width: Int, @@ -29,4 +34,10 @@ data class RenderedFrame( result = 31 * result + height return result } + + val image: BufferedImage + get() = ByteArrayInputStream(bytes).use { ImageIO.read(it) } + + val dimension: Dimension + get() = Dimension(width, height) } \ No newline at end of file diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt index deba6ab06b..54501bf180 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt @@ -50,29 +50,46 @@ fun RemoteConnection.sendConfigFromGradle( sendUtf8StringData(previewFqName) } -internal fun RemoteConnection.receiveConfigFromGradle( - onPreviewClasspath: (String) -> Unit, - onPreviewFqName: (String) -> Unit, - onPreviewHostConfig: (PreviewHostConfig) -> Unit -) { +data class ConfigFromGradle( + val previewClasspath: String, + val previewFqName: String, + val previewHostConfig: PreviewHostConfig +) + +internal fun RemoteConnection.receiveConfigFromGradle(): ConfigFromGradle? { + var previewClasspath: String? = null + var previewFqName: String? = null + var previewHostConfig: PreviewHostConfig? = null + receiveCommand { (type, args) -> - when (type) { - Command.Type.PREVIEW_CLASSPATH -> - receiveUtf8StringData { onPreviewClasspath(it) } - Command.Type.PREVIEW_FQ_NAME -> - receiveUtf8StringData { onPreviewFqName(it) } - Command.Type.PREVIEW_CONFIG -> { - val javaExecutable = URLDecoder.decode(args[0], Charsets.UTF_8) - receiveUtf8StringData { hostClasspath -> - val config = PreviewHostConfig(javaExecutable = javaExecutable, hostClasspath = hostClasspath) - onPreviewHostConfig(config) - } - } - else -> { - // todo - } + check(type == Command.Type.PREVIEW_CONFIG) { + "Expected ${Command.Type.PREVIEW_CONFIG}, got $type" + } + val javaExecutable = URLDecoder.decode(args[0], Charsets.UTF_8) + receiveUtf8StringData { hostClasspath -> + previewHostConfig = PreviewHostConfig(javaExecutable = javaExecutable, hostClasspath = hostClasspath) + } + } + receiveCommand { (type, _) -> + check(type == Command.Type.PREVIEW_CLASSPATH) { + "Expected ${Command.Type.PREVIEW_CLASSPATH}, got $type" } + receiveUtf8StringData { previewClasspath = it } } + receiveCommand { (type, _) -> + check(type == Command.Type.PREVIEW_FQ_NAME) { + "Expected ${Command.Type.PREVIEW_FQ_NAME}, got $type" + } + receiveUtf8StringData { previewFqName = it } + } + + return if (previewClasspath != null && previewFqName != null && previewHostConfig != null) { + ConfigFromGradle( + previewClasspath = previewClasspath!!, + previewFqName = previewFqName!!, + previewHostConfig = previewHostConfig!! + ) + } else null } internal fun RemoteConnection.sendPreviewRequest( @@ -81,9 +98,9 @@ internal fun RemoteConnection.sendPreviewRequest( ) { sendCommand(Command.Type.PREVIEW_CLASSPATH) sendData(previewClasspath.toByteArray(Charsets.UTF_8)) - val (fqName, frameConfig) = request + val (id, fqName, frameConfig) = request val (w, h, scale) = frameConfig - val args = arrayListOf(fqName, w.toString(), h.toString()) + val args = arrayListOf(fqName, id.toString(), w.toString(), h.toString()) if (scale != null) { val scaleLong = java.lang.Double.doubleToRawLongBits(scale) args.add(scaleLong.toString()) @@ -102,15 +119,17 @@ internal fun RemoteConnection.receivePreviewRequest( } Command.Type.FRAME_REQUEST -> { val fqName = args.getOrNull(0) - val w = args.getOrNull(1)?.toIntOrNull() - val h = args.getOrNull(2)?.toIntOrNull() - val scale = args.getOrNull(3)?.toLongOrNull()?.let { java.lang.Double.longBitsToDouble(it) } + val id = args.getOrNull(1)?.toLongOrNull() + val w = args.getOrNull(2)?.toIntOrNull() + val h = args.getOrNull(3)?.toIntOrNull() + val scale = args.getOrNull(4)?.toLongOrNull()?.let { java.lang.Double.longBitsToDouble(it) } if ( fqName != null && fqName.isNotEmpty() + && id != null && w != null && w > 0 && h != null && h > 0 ) { - onFrameRequest(FrameRequest(fqName, FrameConfig(width = w, height = h, scale = scale))) + onFrameRequest(FrameRequest(id, fqName, FrameConfig(width = w, height = h, scale = scale))) } } else -> { diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt index ce440cb78c..0087a2d610 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt @@ -37,4 +37,7 @@ internal class PreviewPanel : JPanel() { repaint() } + + override fun getPreferredSize(): Dimension? = + imageDimension ?: super.getPreferredSize() } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt index 0eb5291af5..dd8652f0f0 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt @@ -7,57 +7,91 @@ package org.jetbrains.compose.desktop.ide.preview import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service -import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.FrameConfig -import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewManager -import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewManagerImpl +import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBLoadingPanel +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.* import java.awt.Dimension -import java.io.ByteArrayInputStream -import javax.imageio.ImageIO import javax.swing.JComponent import javax.swing.event.AncestorEvent import javax.swing.event.AncestorListener @Service class PreviewStateService : Disposable { - private var myPanel: PreviewPanel? = null - private val previewManager: PreviewManager = PreviewManagerImpl { frame -> - ByteArrayInputStream(frame.bytes).use { input -> - val image = ImageIO.read(input) - myPanel?.previewImage(image, Dimension(frame.width, frame.height)) - } - } + private val previewListener = CompositePreviewListener() + private val previewManager: PreviewManager = PreviewManagerImpl(previewListener) val gradleCallbackPort: Int get() = previewManager.gradleCallbackPort - private val myListener = object : AncestorListener { - private fun updateFrameSize(c: JComponent) { - val frameConfig = FrameConfig( - width = c.width, - height = c.height, - scale = c.graphicsConfiguration.defaultTransform.scaleX - ) - previewManager.updateFrameConfig(frameConfig) - } + override fun dispose() { + previewManager.close() + } - override fun ancestorAdded(event: AncestorEvent) { - updateFrameSize(event.component) - } + internal fun registerPreviewPanels( + previewPanel: PreviewPanel, + loadingPanel: JBLoadingPanel + ) { + val previewResizeListener = PreviewResizeListener(previewManager) + previewPanel.addAncestorListener(previewResizeListener) + Disposer.register(this) { previewPanel.removeAncestorListener(previewResizeListener) } - override fun ancestorRemoved(event: AncestorEvent) { - } + previewListener.addListener(PreviewPanelUpdater(previewPanel)) + previewListener.addListener(LoadingPanelUpdater(loadingPanel)) + } - override fun ancestorMoved(event: AncestorEvent) { - updateFrameSize(event.component) - } + internal fun buildStarted() { + previewListener.onNewBuildRequest() } - override fun dispose() { - myPanel?.removeAncestorListener(myListener) - previewManager.close() + internal fun buildFinished(success: Boolean) { + previewListener.onFinishedBuild(success) + } +} + +private class PreviewResizeListener(private val previewManager: PreviewManager) : AncestorListener { + private fun updateFrameSize(c: JComponent) { + val frameConfig = FrameConfig( + width = c.width, + height = c.height, + scale = c.graphicsConfiguration.defaultTransform.scaleX + ) + previewManager.updateFrameConfig(frameConfig) + } + + override fun ancestorAdded(event: AncestorEvent) { + updateFrameSize(event.component) + } - internal fun registerPreviewPanel(panel: PreviewPanel) { - myPanel = panel - panel.addAncestorListener(myListener) + override fun ancestorRemoved(event: AncestorEvent) { + } + + override fun ancestorMoved(event: AncestorEvent) { + updateFrameSize(event.component) } } + +private class PreviewPanelUpdater(private val panel: PreviewPanel) : PreviewListenerBase() { + override fun onRenderedFrame(frame: RenderedFrame) { + panel.previewImage(frame.image, frame.dimension) + } +} + +private class LoadingPanelUpdater(private val panel: JBLoadingPanel) : PreviewListenerBase() { + override fun onNewBuildRequest() { + panel.setLoadingText("Building project") + panel.startLoading() + } + + override fun onFinishedBuild(success: Boolean) { + panel.stopLoading() + } + + override fun onNewRenderRequest(previewRequest: FrameRequest) { + panel.setLoadingText("Rendering preview") + panel.startLoading() + } + + override fun onRenderedFrame(frame: RenderedFrame) { + panel.stopLoading() + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt index 4f08d278c4..b75fd4f6b6 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt @@ -9,6 +9,8 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.ui.components.JBLoadingPanel +import java.awt.BorderLayout class PreviewToolWindow : ToolWindowFactory, DumbAware { override fun isApplicable(project: Project): Boolean { @@ -23,8 +25,10 @@ class PreviewToolWindow : ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { toolWindow.contentManager.let { content -> val panel = PreviewPanel() - content.addContent(content.factory.createContent(panel, null, false)) - project.service().registerPreviewPanel(panel) + val loadingPanel = JBLoadingPanel(BorderLayout(), project) + loadingPanel.add(panel, BorderLayout.CENTER) + content.addContent(content.factory.createContent(loadingPanel, null, false)) + project.service().registerPreviewPanels(panel, loadingPanel) } } diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt index ea6f26d668..566da69f4c 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt @@ -35,12 +35,18 @@ class RunPreviewAction( settings.taskNames = listOf("configureDesktopPreview") settings.vmOptions = gradleVmOptions settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id - val gradleCallbackPort = project.service().gradleCallbackPort + val previewService = project.service() + val gradleCallbackPort = previewService.gradleCallbackPort settings.scriptParameters = listOf( "-Pcompose.desktop.preview.target=$fqName", "-Pcompose.desktop.preview.ide.port=$gradleCallbackPort" ).joinToString(" ") + SwingUtilities.invokeLater { + ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate { + previewService.buildStarted() + } + } runTask( settings, DefaultRunExecutor.EXECUTOR_ID, @@ -48,11 +54,10 @@ class RunPreviewAction( GradleConstants.SYSTEM_ID, object : TaskCallback { override fun onSuccess() { - SwingUtilities.invokeLater { - ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate { } - } + previewService.buildFinished(success = true) } override fun onFailure() { + previewService.buildFinished(success = false) } }, ProgressExecutionMode.IN_BACKGROUND_ASYNC,