Skip to content

Commit

Permalink
Add dialog sample to ide-plugin (#342)
Browse files Browse the repository at this point in the history
The dialog shows how to implement a simple wizard, with pages that can
prevent the wizard from going forward or backwards depending on their
state.
  • Loading branch information
rock3r authored Apr 4, 2024
1 parent fd87c8a commit ca3aeb6
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 3 deletions.
1 change: 1 addition & 0 deletions .idea/runConfigurations/IDE_sample.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import javax.swing.JComponent

public fun JewelComposePanel(
content: @Composable () -> Unit,
): JComponent {
return ComposePanel().apply {
): JComponent =
ComposePanel().apply {
setContent {
SwingBridgeTheme {
CompositionLocalProvider(LocalComponent provides this@apply) {
Expand All @@ -22,7 +22,6 @@ public fun JewelComposePanel(
}
}
}
}

@ExperimentalJewelApi
public val LocalComponent: ProvidableCompositionLocal<JComponent> = staticCompositionLocalOf {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.jetbrains.jewel.samples.ideplugin.dialog

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.jetbrains.jewel.ui.component.CheckboxRow
import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.Typography
import kotlin.time.Duration.Companion.seconds

@Service(Service.Level.PROJECT)
private class ProjectScopeProviderService(val scope: CoroutineScope)

internal class JewelDemoAction : DumbAwareAction() {

override fun actionPerformed(event: AnActionEvent) {
val project = checkNotNull(event.project) { "Project not available" }
val scope = project.service<ProjectScopeProviderService>().scope

scope.launch(Dispatchers.EDT) {
WizardDialogWrapper(
project = project,
title = "Jewel Demo wizard",
pages = listOf(FirstPage(project), SecondPage()),
).showAndGet()
}
}
}

private class FirstPage(private val project: Project) : WizardPage {

override val canGoBackwards: StateFlow<Boolean> = MutableStateFlow(true)

private val checkboxChecked = MutableStateFlow(false)
override val canGoForward: StateFlow<Boolean> = checkboxChecked

@Composable
override fun PageContent() {
Column {
Text("This is the first page!", style = Typography.h1TextStyle())

Spacer(Modifier.height(16.dp))

Text("Project name: ${project.name}")

Spacer(Modifier.height(16.dp))

val checked by checkboxChecked.collectAsState()
CheckboxRow("Allow going to next step", checked, {
checkboxChecked.value = it
println("Checkbox value: ${checkboxChecked.value}")
})
}
}
}

private class SecondPage : WizardPage {

override val canGoBackwards: StateFlow<Boolean> = MutableStateFlow(true)
override val canGoForward: StateFlow<Boolean> = MutableStateFlow(true)

@Composable
override fun PageContent() {
Column {
Text("This is the second page!", style = Typography.h1TextStyle())

Spacer(Modifier.height(16.dp))

var count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
launch {
while (true) {
delay(1.seconds.inWholeMilliseconds)
count++
}
}
}

Text("You've been staring at this for $count second(s)")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.jetbrains.jewel.samples.ideplugin.dialog

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.util.ui.JBDimension
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.jetbrains.annotations.Nls
import org.jetbrains.jewel.bridge.JewelComposePanel
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.enableNewSwingCompositing
import java.awt.event.ActionEvent
import javax.swing.Action
import javax.swing.JComponent
import kotlin.coroutines.CoroutineContext

internal class WizardDialogWrapper(
project: Project,
@Nls title: String,
private val pages: List<WizardPage>,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
) : DialogWrapper(project), CoroutineScope {

private val logger = thisLogger()

override val coroutineContext: CoroutineContext =
SupervisorJob() + Dispatchers.EDT + CoroutineName("ComposeWizard")

private val currentPageIndex = mutableIntStateOf(0)

private val cancelAction = CancelAction()
private val backAction = WizardAction("Back") { onBackClick() }
private val nextAction = WizardAction("Next") { onNextClick() }
private val finishAction = WizardAction("Finish") { onFinishClick() }

private var pageScope: CoroutineScope? = null

init {
require(pages.isNotEmpty()) { "Wizard must have at least one page" }
init()

this.title = title

updateActions()
}

private fun updateActions() {
pageScope?.cancel("Page changing")
val newScope = CoroutineScope(coroutineContext)
pageScope = newScope

val pageIndex = currentPageIndex.value
val page = pages[pageIndex]

backAction.isEnabled = pageIndex > 0 && page.canGoBackwards.value
nextAction.isEnabled = pageIndex < pages.lastIndex && page.canGoForward.value
finishAction.isEnabled = pageIndex == pages.lastIndex && page.canGoForward.value

newScope.launch(defaultDispatcher) {
page.canGoBackwards.collect { canGoBackwards ->
logger.info("CanGoBackwards: $canGoBackwards")
backAction.isEnabled = pageIndex > 0 && canGoBackwards
}
}
newScope.launch(defaultDispatcher) {
page.canGoForward.collect { canGoForward ->
logger.info("CanGoForward: $canGoForward")
nextAction.isEnabled =
pageIndex < pages.lastIndex && canGoForward
finishAction.isEnabled = pageIndex == pages.lastIndex && canGoForward
}
}
}

@OptIn(ExperimentalJewelApi::class)
override fun createCenterPanel(): JComponent {
enableNewSwingCompositing()

return JewelComposePanel {
val index by currentPageIndex
pages[index].PageContent()
}.apply {
minimumSize = JBDimension(400, 400)
}
}

override fun createActions(): Array<Action> =
arrayOf(cancelAction, backAction, nextAction, finishAction)

private fun onBackClick() {
if (currentPageIndex.value <= 0) {
logger.warn("Trying to go back beyond the first page")
return
}
currentPageIndex.value -= 1
updateActions()
}

private fun onNextClick() {
if (currentPageIndex.value >= pages.lastIndex) {
logger.warn("Trying to go next on or beyond the last page")
return
}
currentPageIndex.value += 1
updateActions()
}

private fun onFinishClick() {
logger.info("Finish clicked")
close(OK_EXIT_CODE)
}

private inner class CancelAction : DialogWrapperAction("Cancel") {

override fun doAction(e: ActionEvent?) {
logger.debug("Cancel clicked")
doCancelAction()
}
}

private inner class WizardAction(
@Nls name: String,
private val onAction: () -> Unit,
) : DialogWrapperAction(name) {

override fun doAction(e: ActionEvent?) {
onAction()
}
}
}

interface WizardPage {

@Composable
fun PageContent()

val canGoForward: StateFlow<Boolean>
val canGoBackwards: StateFlow<Boolean>
}
6 changes: 6 additions & 0 deletions samples/ide-plugin/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ See the <a href="https://github.com/JetBrains/jewel">Jewel repository</a> for mo

<iconMapper mappingFile="JewelIntUiIconMappings.json"/>
</extensions>

<actions>
<action id="Jewel Dialog Demo" class="org.jetbrains.jewel.samples.ideplugin.dialog.JewelDemoAction"
text="Jewel Demo Dialog">
</action>
</actions>
</idea-plugin>

0 comments on commit ca3aeb6

Please sign in to comment.