Browse Source

Better preview progress indication (#901)

* Indicate preview progress with loading panel

* Avoid sending repeated preview requests
pull/905/head
Alexey Tsvetkov 3 years ago committed by GitHub
parent
commit
f15f4236de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 51
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt
  2. 47
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt
  3. 3
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt
  4. 11
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt
  5. 71
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt
  6. 3
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt
  7. 104
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt
  8. 8
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt
  9. 13
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt

51
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<PreviewListener>()
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)
}
}

47
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<String>(null)
private val previewFqName = AtomicReference<String>(null)
private val previewFrameConfig = AtomicReference<FrameConfig>(null)
private val frameRequest = AtomicReference<FrameRequest>(null)
private val shouldRequestFrame = AtomicBoolean(false)
private val inProcessRequest = AtomicReference<FrameRequest>(null)
private val processedRequest = AtomicReference<FrameRequest>(null)
private val userRequestCount = AtomicLong(0)
private val runningPreview = AtomicReference<RunningPreview>(null)
private val threads = arrayListOf<Thread>()
@ -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()
}

3
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

11
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)
}

71
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 -> {

3
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()
}

104
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()
}
}

8
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<PreviewStateService>().registerPreviewPanel(panel)
val loadingPanel = JBLoadingPanel(BorderLayout(), project)
loadingPanel.add(panel, BorderLayout.CENTER)
content.addContent(content.factory.createContent(loadingPanel, null, false))
project.service<PreviewStateService>().registerPreviewPanels(panel, loadingPanel)
}
}

13
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<PreviewStateService>().gradleCallbackPort
val previewService = project.service<PreviewStateService>()
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,

Loading…
Cancel
Save