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( data class FrameRequest(
val id: Long,
val composableFqName: String, val composableFqName: String,
val frameConfig: FrameConfig val frameConfig: FrameConfig
) )
@ -47,7 +48,9 @@ private data class RunningPreview(
get() = connection.isAlive && process.isAlive 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 log = PrintStreamLogger("SERVER")
private val previewSocket = newServerSocket() private val previewSocket = newServerSocket()
private val gradleCallbackSocket = 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 previewClasspath = AtomicReference<String>(null)
private val previewFqName = AtomicReference<String>(null) private val previewFqName = AtomicReference<String>(null)
private val previewFrameConfig = AtomicReference<FrameConfig>(null) private val previewFrameConfig = AtomicReference<FrameConfig>(null)
private val frameRequest = AtomicReference<FrameRequest>(null) private val inProcessRequest = AtomicReference<FrameRequest>(null)
private val shouldRequestFrame = AtomicBoolean(false) private val processedRequest = AtomicReference<FrameRequest>(null)
private val userRequestCount = AtomicLong(0)
private val runningPreview = AtomicReference<RunningPreview>(null) private val runningPreview = AtomicReference<RunningPreview>(null)
private val threads = arrayListOf<Thread>() private val threads = arrayListOf<Thread>()
@ -97,14 +101,12 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev
val frameConfig = previewFrameConfig.get() val frameConfig = previewFrameConfig.get()
if (classpath != null && frameConfig != null && fqName != null) { if (classpath != null && frameConfig != null && fqName != null) {
val request = FrameRequest(fqName, frameConfig) val request = FrameRequest(userRequestCount.get(), fqName, frameConfig)
if (shouldRequestFrame.get() && frameRequest.get() == null) { val prevRequest = processedRequest.get()
if (shouldRequestFrame.compareAndSet(true, false)) { if (inProcessRequest.get() == null && request != prevRequest) {
if (frameRequest.compareAndSet(null, request)) { if (inProcessRequest.compareAndSet(null, request)) {
sendPreviewRequest(classpath, request) previewListener.onNewRenderRequest(request)
} else { sendPreviewRequest(classpath, request)
shouldRequestFrame.compareAndSet(false, true)
}
} }
} }
} }
@ -114,10 +116,11 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev
private val receivePreviewResponseThread = repeatWhileAliveThread("receivePreviewResponse") { private val receivePreviewResponseThread = repeatWhileAliveThread("receivePreviewResponse") {
withLivePreviewConnection { withLivePreviewConnection {
receiveFrame { renderedFrame -> receiveFrame { renderedFrame ->
frameRequest.get()?.let { request -> inProcessRequest.get()?.let { request ->
frameRequest.compareAndSet(request, null) 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") { private val gradleCallbackThread = repeatWhileAliveThread("gradleCallback") {
tryAcceptConnection(gradleCallbackSocket, "GRADLE_CALLBACK")?.let { connection -> tryAcceptConnection(gradleCallbackSocket, "GRADLE_CALLBACK")?.let { connection ->
while (isAlive.get() && connection.isAlive) { while (isAlive.get() && connection.isAlive) {
connection.receiveConfigFromGradle( val config = connection.receiveConfigFromGradle()
onPreviewClasspath = { previewClasspath.set(it) }, if (config != null) {
onPreviewHostConfig = { previewHostConfig.set(it) }, previewClasspath.set(config.previewClasspath)
onPreviewFqName = { previewFqName.set(it) } previewFqName.set(config.previewFqName)
) previewHostConfig.set(config.previewHostConfig)
shouldRequestFrame.set(true) userRequestCount.incrementAndGet()
sendPreviewRequestThread.interrupt() sendPreviewRequestThread.interrupt()
}
} }
} }
} }
@ -191,7 +195,6 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev
override fun updateFrameConfig(frameConfig: FrameConfig) { override fun updateFrameConfig(frameConfig: FrameConfig) {
previewFrameConfig.set(frameConfig) previewFrameConfig.set(frameConfig)
shouldRequestFrame.set(true)
sendPreviewRequestThread.interrupt() 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 { try {
val classpath = previewClasspath.get() val classpath = previewClasspath.get()
val request = previewRequest.get() val request = previewRequest.get()
log { "request != null == ${request != null} && classpath != null == ${classpath != null}" }
if (classpath != null && request != null) { if (classpath != null && request != null) {
if (previewRequest.compareAndSet(request, null)) { if (previewRequest.compareAndSet(request, null)) {
val bytes = renderFrame(classpath, request) 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" } val possibleCandidates = previewFacade.methods.filter { it.name == "render" }
throw RuntimeException("Could not find method '$signature'. Possible candidates: \n${possibleCandidates.joinToString("\n") { "* ${it}" }}", e) 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 scaledWidth = frameConfig.scaledWidth
val scaledHeight = frameConfig.scaledHeight val scaledHeight = frameConfig.scaledHeight
val scale = frameConfig.scale 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 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( data class RenderedFrame(
val bytes: ByteArray, val bytes: ByteArray,
val width: Int, val width: Int,
@ -29,4 +34,10 @@ data class RenderedFrame(
result = 31 * result + height result = 31 * result + height
return result 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) sendUtf8StringData(previewFqName)
} }
internal fun RemoteConnection.receiveConfigFromGradle( data class ConfigFromGradle(
onPreviewClasspath: (String) -> Unit, val previewClasspath: String,
onPreviewFqName: (String) -> Unit, val previewFqName: String,
onPreviewHostConfig: (PreviewHostConfig) -> Unit val previewHostConfig: PreviewHostConfig
) { )
internal fun RemoteConnection.receiveConfigFromGradle(): ConfigFromGradle? {
var previewClasspath: String? = null
var previewFqName: String? = null
var previewHostConfig: PreviewHostConfig? = null
receiveCommand { (type, args) -> receiveCommand { (type, args) ->
when (type) { check(type == Command.Type.PREVIEW_CONFIG) {
Command.Type.PREVIEW_CLASSPATH -> "Expected ${Command.Type.PREVIEW_CONFIG}, got $type"
receiveUtf8StringData { onPreviewClasspath(it) } }
Command.Type.PREVIEW_FQ_NAME -> val javaExecutable = URLDecoder.decode(args[0], Charsets.UTF_8)
receiveUtf8StringData { onPreviewFqName(it) } receiveUtf8StringData { hostClasspath ->
Command.Type.PREVIEW_CONFIG -> { previewHostConfig = PreviewHostConfig(javaExecutable = javaExecutable, hostClasspath = hostClasspath)
val javaExecutable = URLDecoder.decode(args[0], Charsets.UTF_8) }
receiveUtf8StringData { hostClasspath -> }
val config = PreviewHostConfig(javaExecutable = javaExecutable, hostClasspath = hostClasspath) receiveCommand { (type, _) ->
onPreviewHostConfig(config) check(type == Command.Type.PREVIEW_CLASSPATH) {
} "Expected ${Command.Type.PREVIEW_CLASSPATH}, got $type"
}
else -> {
// todo
}
} }
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( internal fun RemoteConnection.sendPreviewRequest(
@ -81,9 +98,9 @@ internal fun RemoteConnection.sendPreviewRequest(
) { ) {
sendCommand(Command.Type.PREVIEW_CLASSPATH) sendCommand(Command.Type.PREVIEW_CLASSPATH)
sendData(previewClasspath.toByteArray(Charsets.UTF_8)) sendData(previewClasspath.toByteArray(Charsets.UTF_8))
val (fqName, frameConfig) = request val (id, fqName, frameConfig) = request
val (w, h, scale) = frameConfig 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) { if (scale != null) {
val scaleLong = java.lang.Double.doubleToRawLongBits(scale) val scaleLong = java.lang.Double.doubleToRawLongBits(scale)
args.add(scaleLong.toString()) args.add(scaleLong.toString())
@ -102,15 +119,17 @@ internal fun RemoteConnection.receivePreviewRequest(
} }
Command.Type.FRAME_REQUEST -> { Command.Type.FRAME_REQUEST -> {
val fqName = args.getOrNull(0) val fqName = args.getOrNull(0)
val w = args.getOrNull(1)?.toIntOrNull() val id = args.getOrNull(1)?.toLongOrNull()
val h = args.getOrNull(2)?.toIntOrNull() val w = args.getOrNull(2)?.toIntOrNull()
val scale = args.getOrNull(3)?.toLongOrNull()?.let { java.lang.Double.longBitsToDouble(it) } val h = args.getOrNull(3)?.toIntOrNull()
val scale = args.getOrNull(4)?.toLongOrNull()?.let { java.lang.Double.longBitsToDouble(it) }
if ( if (
fqName != null && fqName.isNotEmpty() fqName != null && fqName.isNotEmpty()
&& id != null
&& w != null && w > 0 && w != null && w > 0
&& h != null && h > 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 -> { else -> {

3
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt

@ -37,4 +37,7 @@ internal class PreviewPanel : JPanel() {
repaint() 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.Disposable
import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.FrameConfig import com.intellij.openapi.util.Disposer
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewManager import com.intellij.ui.components.JBLoadingPanel
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewManagerImpl import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.*
import java.awt.Dimension import java.awt.Dimension
import java.io.ByteArrayInputStream
import javax.imageio.ImageIO
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.event.AncestorEvent import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener import javax.swing.event.AncestorListener
@Service @Service
class PreviewStateService : Disposable { class PreviewStateService : Disposable {
private var myPanel: PreviewPanel? = null private val previewListener = CompositePreviewListener()
private val previewManager: PreviewManager = PreviewManagerImpl { frame -> private val previewManager: PreviewManager = PreviewManagerImpl(previewListener)
ByteArrayInputStream(frame.bytes).use { input ->
val image = ImageIO.read(input)
myPanel?.previewImage(image, Dimension(frame.width, frame.height))
}
}
val gradleCallbackPort: Int val gradleCallbackPort: Int
get() = previewManager.gradleCallbackPort get() = previewManager.gradleCallbackPort
private val myListener = object : AncestorListener { override fun dispose() {
private fun updateFrameSize(c: JComponent) { previewManager.close()
val frameConfig = FrameConfig( }
width = c.width,
height = c.height,
scale = c.graphicsConfiguration.defaultTransform.scaleX
)
previewManager.updateFrameConfig(frameConfig)
}
override fun ancestorAdded(event: AncestorEvent) { internal fun registerPreviewPanels(
updateFrameSize(event.component) 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) { internal fun buildStarted() {
updateFrameSize(event.component) previewListener.onNewBuildRequest()
}
} }
override fun dispose() { internal fun buildFinished(success: Boolean) {
myPanel?.removeAncestorListener(myListener) previewListener.onFinishedBuild(success)
previewManager.close() }
}
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) { override fun ancestorRemoved(event: AncestorEvent) {
myPanel = panel }
panel.addAncestorListener(myListener)
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.project.Project
import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.components.JBLoadingPanel
import java.awt.BorderLayout
class PreviewToolWindow : ToolWindowFactory, DumbAware { class PreviewToolWindow : ToolWindowFactory, DumbAware {
override fun isApplicable(project: Project): Boolean { override fun isApplicable(project: Project): Boolean {
@ -23,8 +25,10 @@ class PreviewToolWindow : ToolWindowFactory, DumbAware {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
toolWindow.contentManager.let { content -> toolWindow.contentManager.let { content ->
val panel = PreviewPanel() val panel = PreviewPanel()
content.addContent(content.factory.createContent(panel, null, false)) val loadingPanel = JBLoadingPanel(BorderLayout(), project)
project.service<PreviewStateService>().registerPreviewPanel(panel) 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.taskNames = listOf("configureDesktopPreview")
settings.vmOptions = gradleVmOptions settings.vmOptions = gradleVmOptions
settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id
val gradleCallbackPort = project.service<PreviewStateService>().gradleCallbackPort val previewService = project.service<PreviewStateService>()
val gradleCallbackPort = previewService.gradleCallbackPort
settings.scriptParameters = settings.scriptParameters =
listOf( listOf(
"-Pcompose.desktop.preview.target=$fqName", "-Pcompose.desktop.preview.target=$fqName",
"-Pcompose.desktop.preview.ide.port=$gradleCallbackPort" "-Pcompose.desktop.preview.ide.port=$gradleCallbackPort"
).joinToString(" ") ).joinToString(" ")
SwingUtilities.invokeLater {
ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate {
previewService.buildStarted()
}
}
runTask( runTask(
settings, settings,
DefaultRunExecutor.EXECUTOR_ID, DefaultRunExecutor.EXECUTOR_ID,
@ -48,11 +54,10 @@ class RunPreviewAction(
GradleConstants.SYSTEM_ID, GradleConstants.SYSTEM_ID,
object : TaskCallback { object : TaskCallback {
override fun onSuccess() { override fun onSuccess() {
SwingUtilities.invokeLater { previewService.buildFinished(success = true)
ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate { }
}
} }
override fun onFailure() { override fun onFailure() {
previewService.buildFinished(success = false)
} }
}, },
ProgressExecutionMode.IN_BACKGROUND_ASYNC, ProgressExecutionMode.IN_BACKGROUND_ASYNC,

Loading…
Cancel
Save