Browse Source

Idea floating panel (#927)

* Implement floating editor toolbar for running preview

* Minor: remove accidentally committed debug logging
pull/929/head
Alexey Tsvetkov 3 years ago committed by GitHub
parent
commit
a0a8a32302
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt
  2. 4
      idea-plugin/examples/desktop-project/build.gradle.kts
  3. 92
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt
  4. 67
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt
  5. 27
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt
  6. 11
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt
  7. 11
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt
  8. 68
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt
  9. 42
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt
  10. 58
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt
  11. 3
      idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt
  12. 8
      idea-plugin/src/main/resources/META-INF/plugin.xml

1
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt

@ -62,7 +62,6 @@ 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)

4
idea-plugin/examples/desktop-project/build.gradle.kts

@ -2,9 +2,9 @@ import org.jetbrains.compose.compose
plugins { plugins {
// __KOTLIN_COMPOSE_VERSION__ // __KOTLIN_COMPOSE_VERSION__
kotlin("jvm") version "1.5.10" kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__ // __LATEST_COMPOSE_RELEASE_VERSION__
id("org.jetbrains.compose") version "0.5.0-build229" id("org.jetbrains.compose") version "0.5.0-build262"
} }
repositories { repositories {

92
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt

@ -0,0 +1,92 @@
/*
* 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.ide.preview
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode
import com.intellij.openapi.externalSystem.task.TaskCallback
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil.runTask
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.wm.ToolWindowManager
import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
import javax.swing.SwingUtilities
class RunPreviewAction(
private val previewLocation: PreviewLocation
) : AnAction({ "Show non-interactive preview" }, PreviewIcons.RUN_PREVIEW) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
buildPreviewViaGradle(project, previewLocation)
}
}
internal const val PREVIEW_EDITOR_TOOLBAR_GROUP_ID = "Compose.Desktop.Preview.Editor.Toolbar"
class RefreshOrRunPreviewAction : AnAction(PreviewIcons.COMPOSE) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val previewLocation = ReadAction.compute<PreviewLocation?, Throwable> {
val editor = e.dataContext.getData(CommonDataKeys.EDITOR)
if (editor != null) {
e.presentation.isEnabled = false
parentPreviewAtCaretOrNull(editor)
} else null
}
if (previewLocation != null) {
buildPreviewViaGradle(project, previewLocation)
}
}
}
private fun buildPreviewViaGradle(project: Project, previewLocation: PreviewLocation) {
val previewToolWindow = ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")
previewToolWindow?.setAvailable(true)
val gradleVmOptions = GradleSettings.getInstance(project).gradleVmOptions
val settings = ExternalSystemTaskExecutionSettings()
settings.executionName = "Preview: ${previewLocation.fqName}"
settings.externalProjectPath = previewLocation.modulePath
settings.taskNames = listOf("configureDesktopPreview")
settings.vmOptions = gradleVmOptions
settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id
val previewService = project.service<PreviewStateService>()
val gradleCallbackPort = previewService.gradleCallbackPort
settings.scriptParameters =
listOf(
"-Pcompose.desktop.preview.target=${previewLocation.fqName}",
"-Pcompose.desktop.preview.ide.port=$gradleCallbackPort"
).joinToString(" ")
SwingUtilities.invokeLater {
ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate {
previewService.buildStarted()
}
}
runTask(
settings,
DefaultRunExecutor.EXECUTOR_ID,
project,
GradleConstants.SYSTEM_ID,
object : TaskCallback {
override fun onSuccess() {
previewService.buildFinished(success = true)
}
override fun onFailure() {
previewService.buildFinished(success = false)
}
},
ProgressExecutionMode.IN_BACKGROUND_ASYNC,
false,
UserDataHolderBase()
)
}

67
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt

@ -0,0 +1,67 @@
/*
* 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.ide.preview
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import com.intellij.openapi.editor.toolbar.floating.AbstractFloatingToolbarProvider
import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarComponent
import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarComponentImpl
import com.intellij.openapi.project.Project
import com.intellij.util.concurrency.AppExecutorUtil
class PreviewFloatingToolbarProvider : AbstractFloatingToolbarProvider(PREVIEW_EDITOR_TOOLBAR_GROUP_ID) {
override val autoHideable = false
override val priority: Int = 100
// todo: disable if not in Compose JVM module
override fun register(toolbar: FloatingToolbarComponent, parentDisposable: Disposable) {
try {
// todo: use provided data context once 2020.3 is no longer supported
val toolbarImpl = toolbar as? FloatingToolbarComponentImpl ?: return
val editor = toolbarImpl.getData(CommonDataKeys.EDITOR.name) as? Editor ?: return
registerComponent(toolbar, editor, parentDisposable)
} catch (e: Exception) {
LOG.error(e)
}
}
private fun registerComponent(
component: FloatingToolbarComponent,
editor: Editor,
parentDisposable: Disposable
) {
val project = editor.project ?: return
val listener = PreviewEditorToolbarVisibilityUpdater(component, project, editor)
editor.caretModel.addCaretListener(listener, parentDisposable)
}
}
internal class PreviewEditorToolbarVisibilityUpdater(
private val toolbar: FloatingToolbarComponent,
private val project: Project,
private val editor: Editor
) : CaretListener {
override fun caretPositionChanged(event: CaretEvent) {
ReadAction.nonBlocking { updateVisibility() }
.inSmartMode(project)
.submit(AppExecutorUtil.getAppExecutorService())
}
private fun updateVisibility() {
val parentPreviewFun = parentPreviewAtCaretOrNull(editor)
if (parentPreviewFun != null) {
toolbar.scheduleShow()
} else {
toolbar.scheduleHide()
}
}
}

27
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt

@ -0,0 +1,27 @@
/*
* 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.ide.preview
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.jetbrains.kotlin.idea.util.projectStructure.module
import org.jetbrains.kotlin.psi.KtNamedFunction
data class PreviewLocation(val fqName: String, val modulePath: String)
@RequiresReadLock
internal fun KtNamedFunction.asPreviewFunctionOrNull(): PreviewLocation? {
if (isValidComposablePreviewFunction()) {
val fqName = composePreviewFunctionFqn()
val module = module?.let { ExternalSystemApiUtil.getExternalProjectPath(it) }
if (module != null) {
return PreviewLocation(fqName = fqName, modulePath = module)
}
}
return null
}

11
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt

@ -0,0 +1,11 @@
/*
* 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.ide.preview
import com.intellij.openapi.diagnostic.Logger
val LOG = Logger.getInstance(PreviewRootLogger::class.java)
private class PreviewRootLogger

11
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt

@ -35,13 +35,10 @@ class PreviewRunLineMarkerContributor : RunLineMarkerContributor() {
if (element !is LeafPsiElement) return null if (element !is LeafPsiElement) return null
if (element.node.elementType != KtTokens.IDENTIFIER) return null if (element.node.elementType != KtTokens.IDENTIFIER) return null
val parent = element.parent return when (val parent = element.parent) {
return when { is KtNamedFunction -> {
parent is KtNamedFunction && parent.isValidComposePreview() -> { val previewFunction = parent.asPreviewFunctionOrNull() ?: return null
val fqName = parent.composePreviewFunctionFqn() val actions = arrayOf(RunPreviewAction(previewFunction))
val module = parent.module?.let { ExternalSystemApiUtil.getExternalProjectPath(it) }
?: error("Could not determine module for $parent")
val actions = arrayOf(RunPreviewAction(fqName, module))
Info(PreviewIcons.COMPOSE, actions) { PreviewMessages.runPreview(parent.name!!) } Info(PreviewIcons.COMPOSE, actions) { PreviewMessages.runPreview(parent.name!!) }
} }
else -> null else -> null

68
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt

@ -1,68 +0,0 @@
/*
* 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.ide.preview
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode
import com.intellij.openapi.externalSystem.task.TaskCallback
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil.runTask
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.wm.ToolWindowManager
import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
import javax.swing.SwingUtilities
class RunPreviewAction(
private val fqName: String,
private val modulePath: String
) : AnAction({ "Show non-interactive preview" }, PreviewIcons.RUN_PREVIEW) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project!!
val previewToolWindow = ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")
previewToolWindow?.setAvailable(true)
val gradleVmOptions = GradleSettings.getInstance(project).gradleVmOptions
val settings = ExternalSystemTaskExecutionSettings()
settings.executionName = "Preview: $fqName"
settings.externalProjectPath = modulePath
settings.taskNames = listOf("configureDesktopPreview")
settings.vmOptions = gradleVmOptions
settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id
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,
project,
GradleConstants.SYSTEM_ID,
object : TaskCallback {
override fun onSuccess() {
previewService.buildFinished(success = true)
}
override fun onFailure() {
previewService.buildFinished(success = false)
}
},
ProgressExecutionMode.IN_BACKGROUND_ASYNC,
false,
UserDataHolderBase()
)
}
}

42
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt

@ -0,0 +1,42 @@
/*
* 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.ide.preview
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.testFramework.LightVirtualFile
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.psi.KtNamedFunction
@RequiresReadLock
internal fun parentPreviewAtCaretOrNull(editor: Editor): PreviewLocation? {
val caretModel = editor.caretModel
val psiFile = kotlinPsiFile(editor)
if (psiFile != null) {
var node = psiFile.findElementAt(caretModel.offset)
while (node != null) {
val previewFunction = (node as? KtNamedFunction)?.asPreviewFunctionOrNull()
if (previewFunction != null) {
return previewFunction
}
node = node.parent
}
}
return null
}
private fun kotlinPsiFile(editor: Editor): PsiFile? {
val project = editor.project ?: return null
val documentManager = FileDocumentManager.getInstance()
val file = documentManager.getFile(editor.document)
return if (file != null && file.fileType is KotlinFileType) {
PsiManager.getInstance(project).findFile(file)
} else null
}

58
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt

@ -17,10 +17,10 @@
package org.jetbrains.compose.desktop.ide.preview package org.jetbrains.compose.desktop.ide.preview
import com.intellij.openapi.roots.ProjectRootModificationTracker import com.intellij.openapi.roots.ProjectRootModificationTracker
import com.intellij.psi.PsiElement
import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.parentOfType import com.intellij.psi.util.parentOfType
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.jetbrains.kotlin.asJava.findFacadeClass import org.jetbrains.kotlin.asJava.findFacadeClass
import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.ClassKind
@ -39,21 +39,13 @@ internal const val COMPOSABLE_FQ_NAME = "androidx.compose.runtime.Composable"
* tools/adt/idea/compose-designer/src/com/android/tools/idea/compose/preview/util/PreviewElement.kt * tools/adt/idea/compose-designer/src/com/android/tools/idea/compose/preview/util/PreviewElement.kt
*/ */
/**
* Whether this function is properly annotated with [PREVIEW_ANNOTATION_FQN] and is defined in a valid location.
*
* @see [isValidPreviewLocation]
*/
internal fun KtNamedFunction.isValidComposePreview() =
isValidPreviewLocation() && annotationEntries.any { it.fqNameMatches(DESKTOP_PREVIEW_ANNOTATION_FQN) }
/** /**
* Returns whether a `@Composable` [PREVIEW_ANNOTATION_FQN] is defined in a valid location, which can be either: * Returns whether a `@Composable` [PREVIEW_ANNOTATION_FQN] is defined in a valid location, which can be either:
* 1. Top-level functions * 1. Top-level functions
* 2. Non-nested functions defined in top-level classes that have a default (no parameter) constructor * 2. Non-nested functions defined in top-level classes that have a default (no parameter) constructor
* *
*/ */
internal fun KtNamedFunction.isValidPreviewLocation(): Boolean { private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
if (valueParameters.size > 0) return false if (valueParameters.size > 0) return false
if (receiverTypeReference != null) return false if (receiverTypeReference != null) return false
@ -117,19 +109,39 @@ private fun KtAnnotationEntry.getQualifiedName(): String? =
internal fun KtNamedFunction.composePreviewFunctionFqn() = "${getClassName()}.${name}" internal fun KtNamedFunction.composePreviewFunctionFqn() = "${getClassName()}.${name}"
// based on AndroidComposePsiUtils.kt from AOSP @RequiresReadLock
internal fun PsiElement.isComposableFunction(): Boolean { internal fun KtNamedFunction.isValidComposablePreviewFunction(): Boolean {
if (this !is KtNamedFunction) return false fun isValidComposablePreviewImpl(): Boolean {
if (!isValidPreviewLocation()) return false
var hasComposableAnnotation = false
var hasPreviewAnnotation = false
val annotationIt = annotationEntries.iterator()
while (annotationIt.hasNext() && !(hasComposableAnnotation && hasPreviewAnnotation)) {
val annotation = annotationIt.next()
hasComposableAnnotation = hasComposableAnnotation || annotation.fqNameMatches(COMPOSABLE_FQ_NAME)
hasPreviewAnnotation = hasPreviewAnnotation || annotation.fqNameMatches(DESKTOP_PREVIEW_ANNOTATION_FQN)
}
return hasComposableAnnotation && hasPreviewAnnotation
}
return CachedValuesManager.getCachedValue(this) { return CachedValuesManager.getCachedValue(this) {
val hasComposableAnnotation = annotationEntries.any { it.fqNameMatches(COMPOSABLE_FQ_NAME) } cachedResult(isValidComposablePreviewImpl())
val containingKtFile = this.containingKtFile
CachedValueProvider.Result.create(
// TODO: see if we can handle alias imports without ruining performance.
hasComposableAnnotation,
containingKtFile,
ProjectRootModificationTracker.getInstance(project)
)
} }
} }
// based on AndroidComposePsiUtils.kt from AOSP
internal fun KtNamedFunction.isComposableFunction(): Boolean {
return CachedValuesManager.getCachedValue(this) {
cachedResult(annotationEntries.any { it.fqNameMatches(COMPOSABLE_FQ_NAME) })
}
}
private fun <T> KtNamedFunction.cachedResult(value: T) =
CachedValueProvider.Result.create(
// TODO: see if we can handle alias imports without ruining performance.
value,
this.containingKtFile,
ProjectRootModificationTracker.getInstance(project)
)

3
idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt

@ -21,6 +21,7 @@ import com.intellij.psi.PsiElement
import org.jetbrains.compose.desktop.ide.preview.isComposableFunction import org.jetbrains.compose.desktop.ide.preview.isComposableFunction
import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtNamedFunction
/** /**
* Suppress inspection that require composable function names to start with a lower case letter. * Suppress inspection that require composable function names to start with a lower case letter.
@ -30,7 +31,7 @@ class ComposeSuppressor : InspectionSuppressor {
return toolId == "FunctionName" && return toolId == "FunctionName" &&
element.language == KotlinLanguage.INSTANCE && element.language == KotlinLanguage.INSTANCE &&
element.node.elementType == KtTokens.IDENTIFIER && element.node.elementType == KtTokens.IDENTIFIER &&
element.parent.isComposableFunction() element.parent.let { it is KtNamedFunction && it.isComposableFunction() }
} }
override fun getSuppressActions(element: PsiElement?, toolId: String): Array<SuppressQuickFix> { override fun getSuppressActions(element: PsiElement?, toolId: String): Array<SuppressQuickFix> {

8
idea-plugin/src/main/resources/META-INF/plugin.xml

@ -40,5 +40,13 @@
<lang.inspectionSuppressor language="kotlin" implementationClass="org.jetbrains.compose.inspections.ComposeSuppressor"/> <lang.inspectionSuppressor language="kotlin" implementationClass="org.jetbrains.compose.inspections.ComposeSuppressor"/>
<notificationGroup id="Compose MPP Notifications" displayType="BALLOON"/> <notificationGroup id="Compose MPP Notifications" displayType="BALLOON"/>
<editorFloatingToolbarProvider implementation="org.jetbrains.compose.desktop.ide.preview.PreviewFloatingToolbarProvider"/>
</extensions> </extensions>
<actions>
<group id="Compose.Desktop.Preview.Editor.Toolbar">
<action class="org.jetbrains.compose.desktop.ide.preview.RefreshOrRunPreviewAction"/>
</group>
</actions>
</idea-plugin> </idea-plugin>

Loading…
Cancel
Save