("com.demonwav.minecraft-dev.platformTypeWizard")
+
+ fun create(parent: P) where P : NewProjectWizardStep, P : NewProjectWizardBaseData =
+ PlatformTypeStep(parent)
+ }
+
+ override val self = this
+ override val label = "Platform Type:"
+
+ interface Factory : NewProjectWizardMultiStepFactory
+}
Index: src/main/kotlin/creator/platformtype/PluginPlatformStep.kt
===================================================================
--- src/main/kotlin/creator/platformtype/PluginPlatformStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/platformtype/PluginPlatformStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,39 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.platformtype
+
+import com.intellij.ide.wizard.AbstractNewProjectWizardMultiStep
+import com.intellij.ide.wizard.NewProjectWizardMultiStepFactory
+import com.intellij.openapi.extensions.ExtensionPointName
+
+/**
+ * The step to select a mod platform.
+ *
+ * To add custom mod platforms, register a [Factory] to the `com.demonwav.minecraft-dev.pluginPlatformWizard` extension
+ * point.
+ */
+class PluginPlatformStep(
+ parent: PlatformTypeStep
+) : AbstractNewProjectWizardMultiStep(parent, EP_NAME) {
+ companion object {
+ val EP_NAME = ExtensionPointName("com.demonwav.minecraft-dev.pluginPlatformWizard")
+ }
+
+ override val self = this
+ override val label = "Platform:"
+
+ class TypeFactory : PlatformTypeStep.Factory {
+ override val name = "Plugin"
+ override fun createStep(parent: PlatformTypeStep) = PluginPlatformStep(parent)
+ }
+
+ interface Factory : NewProjectWizardMultiStepFactory
+}
Index: src/main/kotlin/creator/step/AbstractCollapsibleStep.kt
===================================================================
--- src/main/kotlin/creator/step/AbstractCollapsibleStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/step/AbstractCollapsibleStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,36 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.step
+
+import com.intellij.ide.wizard.AbstractNewProjectWizardStep
+import com.intellij.ide.wizard.NewProjectWizardStep
+import com.intellij.openapi.project.Project
+import com.intellij.ui.dsl.builder.Panel
+
+abstract class AbstractCollapsibleStep(parent: NewProjectWizardStep) : AbstractNewProjectWizardStep(parent) {
+ private val child by lazy { createStep() }
+
+ abstract val title: String
+
+ protected abstract fun createStep(): NewProjectWizardStep
+
+ override fun setupUI(builder: Panel) {
+ with(builder) {
+ collapsibleGroup(title) {
+ child.setupUI(this)
+ }
+ }
+ }
+
+ override fun setupProject(project: Project) {
+ child.setupProject(project)
+ }
+}
Index: src/main/kotlin/creator/step/AbstractLatentStep.kt
===================================================================
--- src/main/kotlin/creator/step/AbstractLatentStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/step/AbstractLatentStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,157 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.step
+
+import com.demonwav.mcdev.util.asyncIO
+import com.demonwav.mcdev.util.capitalize
+import com.demonwav.mcdev.util.invokeLater
+import com.demonwav.mcdev.util.onHidden
+import com.demonwav.mcdev.util.onShown
+import com.intellij.ide.wizard.AbstractNewProjectWizardStep
+import com.intellij.ide.wizard.NewProjectWizardStep
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.validation.AFTER_GRAPH_PROPAGATION
+import com.intellij.openapi.ui.validation.validationErrorFor
+import com.intellij.openapi.util.Disposer
+import com.intellij.ui.JBColor
+import com.intellij.ui.dsl.builder.Panel
+import com.intellij.ui.dsl.builder.Placeholder
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.util.ui.AsyncProcessIcon
+import javax.swing.JLabel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.swing.Swing
+
+/**
+ * Used for when a long-running task is required to fully construct the wizard steps, for example when downloading
+ * Minecraft versions.
+ */
+abstract class AbstractLatentStep(parent: NewProjectWizardStep) : AbstractNewProjectWizardStep(parent) {
+ companion object {
+ private val LOGGER = logger>()
+ }
+
+ private var hasComputedData = false
+ private var step: NewProjectWizardStep? = null
+
+ /**
+ * Description of this step displayed to the user.
+ *
+ * This should be in sentence case starting with a lower case letter, and starting with a verb in the present tense,
+ * like a Git commit message.
+ *
+ * For example, "download Minecraft versions" would be an appropriate description.
+ */
+ protected abstract val description: String
+
+ private fun doComputeData(placeholder: Placeholder, lifetime: Disposable) {
+ if (hasComputedData) {
+ return
+ }
+ hasComputedData = true
+
+ var disposed = false
+ Disposer.register(lifetime) {
+ hasComputedData = false
+ disposed = true
+ }
+
+ CoroutineScope(Dispatchers.Swing).launch {
+ if (disposed) {
+ return@launch
+ }
+
+ val result = asyncIO {
+ try {
+ computeData()
+ } catch (e: Throwable) {
+ LOGGER.error(e)
+ null
+ }
+ }.await()
+
+ if (disposed) {
+ return@launch
+ }
+
+ invokeLater {
+ if (disposed) {
+ return@invokeLater
+ }
+
+ if (result == null) {
+ placeholder.component = panel {
+ row {
+ val label = label("Unable to $description")
+ .validationRequestor(AFTER_GRAPH_PROPAGATION(propertyGraph))
+ .validation(validationErrorFor { "Unable to $description" })
+ label.component.foreground = JBColor.RED
+ }
+ }
+ } else {
+ val s = createStep(result)
+ step = s
+ val panel = panel {
+ s.setupUI(this)
+ }
+ placeholder.component = panel
+ }
+ }
+ }
+ }
+
+ protected abstract suspend fun computeData(): T?
+
+ protected abstract fun createStep(data: T): NewProjectWizardStep
+
+ override fun setupUI(builder: Panel) {
+ lateinit var placeholder: Placeholder
+ with(builder) {
+ row {
+ placeholder = placeholder()
+ }
+ }
+ placeholder.component = panel {
+ row(description.capitalize()) {
+ cell(
+ AsyncProcessIcon("$javaClass.computeData").also { component ->
+ var lifetime: Disposable? = null
+ component.onShown {
+ lifetime?.let(Disposer::dispose)
+ lifetime = Disposer.newDisposable().also { lifetime ->
+ Disposer.register(context.disposable, lifetime)
+ doComputeData(placeholder, lifetime)
+ }
+ }
+ component.onHidden {
+ lifetime?.let(Disposer::dispose)
+ lifetime = null
+ }
+ }
+ )
+ .validationRequestor(AFTER_GRAPH_PROPAGATION(propertyGraph))
+ .validation(
+ validationErrorFor {
+ "Haven't finished $description"
+ }
+ )
+ }
+ }
+ }
+
+ override fun setupProject(project: Project) {
+ step?.setupProject(project)
+ }
+}
Index: src/main/kotlin/creator/step/AbstractLongRunningAssetsStep.kt
===================================================================
--- src/main/kotlin/creator/step/AbstractLongRunningAssetsStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/step/AbstractLongRunningAssetsStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,29 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.step
+
+import com.intellij.ide.wizard.NewProjectWizardStep
+import com.intellij.openapi.project.Project
+
+abstract class AbstractLongRunningAssetsStep(parent: NewProjectWizardStep) : AbstractLongRunningStep(parent) {
+ protected val assets = object : FixedAssetsNewProjectWizardStep(parent) {
+ override fun setupAssets(project: Project) {
+ outputDirectory = context.projectFileDirectory
+ this@AbstractLongRunningAssetsStep.setupAssets(project)
+ }
+ }
+
+ abstract fun setupAssets(project: Project)
+
+ override fun perform(project: Project) {
+ assets.setupProject(project)
+ }
+}
Index: src/main/kotlin/creator/step/AbstractLongRunningStep.kt
===================================================================
--- src/main/kotlin/creator/step/AbstractLongRunningStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/step/AbstractLongRunningStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,83 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.step
+
+import com.intellij.ide.wizard.AbstractNewProjectWizardStep
+import com.intellij.ide.wizard.NewProjectWizardStep
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Key
+import com.intellij.openapi.util.UserDataHolderEx
+import java.util.concurrent.ConcurrentLinkedQueue
+
+private typealias TaskQueue = ConcurrentLinkedQueue
+
+/**
+ * Creator steps that either take a long time to complete, or need to be run after other steps that take a long time to
+ * complete.
+ *
+ * These steps show an indeterminate progress bar to the user while they are running.
+ */
+abstract class AbstractLongRunningStep(parent: NewProjectWizardStep) : AbstractNewProjectWizardStep(parent) {
+
+ /**
+ * The text to display on the progress bar
+ */
+ abstract val description: String
+
+ abstract fun perform(project: Project)
+
+ final override fun setupProject(project: Project) {
+ val newQueue = TaskQueue()
+ val queue = (data as UserDataHolderEx).putUserDataIfAbsent(TASK_QUEUE_KEY, newQueue)
+ queue += this
+ if (queue === newQueue) {
+ startTaskQueue(project, queue)
+ }
+ }
+
+ private fun startTaskQueue(project: Project, queue: TaskQueue) {
+ ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Your project is being created") {
+ override fun run(indicator: ProgressIndicator) {
+ if (project.isDisposed) {
+ return
+ }
+
+ indicator.text = "Your project is being created"
+ var currentQueue = queue
+ while (true) {
+ while (true) {
+ val task = currentQueue.poll() ?: break
+ indicator.text2 = task.description
+ if (project.isDisposed) {
+ return
+ }
+ task.perform(project)
+ if (project.isDisposed) {
+ return
+ }
+ }
+ if ((data as UserDataHolderEx).replace(TASK_QUEUE_KEY, currentQueue, null)) {
+ break
+ }
+ currentQueue = data.getUserData(TASK_QUEUE_KEY) ?: break
+ }
+ indicator.text2 = null
+ }
+ })
+ }
+
+ companion object {
+ private val TASK_QUEUE_KEY = Key.create("${AbstractLongRunningStep::class.java.name}.queue")
+ }
+}
Index: src/main/kotlin/creator/step/AbstractReformatFilesStep.kt
===================================================================
--- src/main/kotlin/creator/step/AbstractReformatFilesStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/step/AbstractReformatFilesStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,53 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.step
+
+import com.demonwav.mcdev.util.runWriteTask
+import com.intellij.codeInsight.actions.ReformatCodeProcessor
+import com.intellij.ide.wizard.NewProjectWizardStep
+import com.intellij.openapi.application.ReadAction
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VfsUtil
+import com.intellij.psi.PsiFile
+import com.intellij.psi.PsiManager
+import java.nio.file.Path
+
+abstract class AbstractReformatFilesStep(parent: NewProjectWizardStep) : AbstractLongRunningStep(parent) {
+ override val description = "Reformatting files"
+
+ private val filesToReformat = mutableListOf()
+
+ fun addFileToReformat(file: String) {
+ filesToReformat += file
+ }
+
+ abstract fun addFilesToReformat()
+
+ override fun perform(project: Project) {
+ addFilesToReformat()
+
+ val rootDir = VfsUtil.findFile(Path.of(context.projectFileDirectory), true) ?: return
+ val psiManager = PsiManager.getInstance(project)
+ val files = ReadAction.compute, Throwable> {
+ filesToReformat.mapNotNull { path ->
+ VfsUtil.findRelativeFile(rootDir, *path.split('/').toTypedArray())?.let(psiManager::findFile)
+ }.toTypedArray()
+ }
+ files.ifEmpty { return }
+
+ runWriteTask {
+ WriteCommandAction.writeCommandAction(project, *files).withGlobalUndo().run {
+ ReformatCodeProcessor(project, files, null, false).run()
+ }
+ }
+ }
+}
Index: src/main/kotlin/creator/step/AbstractSelectVersionStep.kt
===================================================================
--- src/main/kotlin/creator/step/AbstractSelectVersionStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/step/AbstractSelectVersionStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,62 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.step
+
+import com.intellij.ide.util.PropertiesComponent
+import com.intellij.ide.wizard.AbstractNewProjectWizardStep
+import com.intellij.ide.wizard.NewProjectWizardStep
+import com.intellij.openapi.observable.util.bindStorage
+import com.intellij.openapi.ui.ComboBox
+import com.intellij.ui.dsl.builder.Panel
+import com.intellij.ui.dsl.builder.Row
+import com.intellij.ui.dsl.builder.bindItem
+
+abstract class AbstractSelectVersionStep>(
+ parent: NewProjectWizardStep,
+ val versions: List
+) : AbstractNewProjectWizardStep(parent) {
+ protected abstract val label: String
+
+ val versionProperty = propertyGraph.property("")
+ .bindStorage("${javaClass.name}.selectedVersion")
+ var version by versionProperty
+
+ protected lateinit var versionBox: ComboBox
+
+ override fun setupUI(builder: Panel) {
+ with(builder) {
+ row(label) {
+ setupRow(this)
+ }
+ }
+ }
+
+ open fun setupRow(builder: Row) {
+ with(builder) {
+ val box = comboBox(versions.sortedDescending().map(Any::toString)).bindItem(versionProperty)
+ val selectedItem = box.component.selectedItem
+ if (selectedItem is String) {
+ version = selectedItem
+ }
+ versionBox = box.component
+
+ // fix the selection to the latest version if it was previously at the latest version
+ val props = PropertiesComponent.getInstance()
+ val latestVersionProp = "${javaClass.name}.latestVersion"
+ val prevLatestVersion = props.getValue(latestVersionProp)
+ val latestVersion = versions.maxOrNull()?.toString()
+ if (version == prevLatestVersion) {
+ version = latestVersion ?: ""
+ }
+ props.setValue(latestVersionProp, latestVersion)
+ }
+ }
+}
Index: src/main/kotlin/creator/step/AbstractVersionChainStep.kt
===================================================================
--- src/main/kotlin/creator/step/AbstractVersionChainStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
+++ src/main/kotlin/creator/step/AbstractVersionChainStep.kt (revision 83949ccec33f5900f9afa7453acfd67b84f16454)
@@ -0,0 +1,215 @@
+/*
+ * Minecraft Dev for IntelliJ
+ *
+ * https://minecraftdev.org
+ *
+ * Copyright (c) 2023 minecraft-dev
+ *
+ * MIT License
+ */
+
+package com.demonwav.mcdev.creator.step
+
+import com.intellij.ide.wizard.AbstractNewProjectWizardMultiStepBase
+import com.intellij.ide.wizard.AbstractNewProjectWizardStep
+import com.intellij.ide.wizard.NewProjectWizardStep
+import com.intellij.openapi.components.PersistentStateComponent
+import com.intellij.openapi.components.RoamingType
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.State
+import com.intellij.openapi.components.Storage
+import com.intellij.openapi.components.service
+import com.intellij.openapi.observable.properties.ObservableMutableProperty
+import com.intellij.openapi.ui.ComboBox
+import com.intellij.ui.CollectionComboBoxModel
+import com.intellij.ui.dsl.builder.Cell
+import com.intellij.ui.dsl.builder.Panel
+import com.intellij.ui.dsl.builder.Row
+import com.intellij.ui.dsl.builder.bindItem
+
+private class VersionProperties(
+ val step: AbstractVersionChainStep,
+ val versionProperties: Array>>,
+ val preferredVersions: Array>, Comparable<*>>>,
+) {
+ init {
+ loadPreferredVersions()
+
+ var propertyChangeCount = 0L
+
+ for ((i, prop) in versionProperties.withIndex()) {
+ prop.afterChange { value ->
+ val prevPropertyChangeCount = ++propertyChangeCount
+
+ val versionsAbove = versionProperties.take(i).map(ObservableMutableProperty>::get)
+ val newestVersion = step.getAvailableVersions(versionsAbove).sortedDescending().first()
+ if (value == newestVersion) {
+ preferredVersions[i].remove(versionsAbove)
+ } else {
+ preferredVersions[i][versionsAbove] = value
+ }
+
+ for (j in (i + 1) until versionProperties.size) {
+ val versionsAboveChild =
+ versionProperties.take(j).map(ObservableMutableProperty>::get)
+ val preferredVersion = preferredVersions[j][versionsAboveChild]
+ step.comboBoxes?.let { comboBoxes ->
+ step.setSelectableItems(j, step.getAvailableVersions(versionsAboveChild).sortedDescending())
+ if (preferredVersion != null) {
+ comboBoxes[j].selectedItem = preferredVersion
+ } else {
+ comboBoxes[j].selectedIndex = 0
+ }
+ } ?: run {
+ versionProperties[j].set(
+ preferredVersion ?: step.getAvailableVersions(versionsAboveChild).first()
+ )
+ }
+
+ // the above code could have triggered a recursive property change which would have dealt with the
+ // rest of what we're going to do here
+ if (propertyChangeCount != prevPropertyChangeCount) {
+ return@afterChange
+ }
+ }
+
+ savePreferredVersions()
+ }
+ }
+ }
+
+ private fun savePreferredVersions() {
+ val stateComponent = PreferredVersionStateComponent.getInstance()
+ val preferredVersions = this.preferredVersions.map { m ->
+ m.map { (key, value) -> key.map(Comparable<*>::toString) to value.toString() }.toMap()
+ }
+ stateComponent.set("${step.javaClass.name}.preferredVersions", preferredVersions)
+ }
+
+ private fun loadPreferredVersions() {
+ val stateComponent = PreferredVersionStateComponent.getInstance()
+ val preferredVersions = stateComponent.get("${step.javaClass.name}.preferredVersions") ?: return
+ for ((i, preferences) in preferredVersions.withIndex()) {
+ if (i >= this.preferredVersions.size) {
+ break
+ }
+
+ preferenceEntryLoop@
+ for ((versionsAbove, version) in preferences) {
+ if (versionsAbove.size != i) {
+ continue@preferenceEntryLoop
+ }
+
+ val parsedVersionsAbove = mutableListOf>()
+ for (versionAbove in versionsAbove) {
+ parsedVersionsAbove += step.getAvailableVersions(parsedVersionsAbove)
+ .firstOrNull { it.toString() == versionAbove }
+ ?: continue@preferenceEntryLoop
+ }
+ val parsedVersion = step.getAvailableVersions(parsedVersionsAbove)
+ .firstOrNull { it.toString() == version }
+ ?: continue@preferenceEntryLoop
+
+ this.preferredVersions[i][parsedVersionsAbove] = parsedVersion
+ }
+
+ val preferredVersion =
+ this.preferredVersions[i][versionProperties.take(i).map(ObservableMutableProperty>::get)]
+ if (preferredVersion != null) {
+ versionProperties[i].set(preferredVersion)
+ }
+ }
+ }
+}
+
+/**
+ * This class replaces chains of [AbstractNewProjectWizardMultiStepBase]s. The problem with the latter approach is that
+ * widgets become improperly aligned.
+ */
+abstract class AbstractVersionChainStep(
+ parent: NewProjectWizardStep,
+ private vararg val labels: String
+) : AbstractNewProjectWizardStep(parent) {
+ private val versionProperties by lazy {
+ val versionProperties = mutableListOf>>()
+ for (i in labels.indices) {
+ versionProperties += propertyGraph.property(
+ getAvailableVersions(versionProperties.map(ObservableMutableProperty>::get)).first()
+ )
+ }
+ val preferredVersions = labels.indices.map { mutableMapOf>, Comparable<*>>() }
+ VersionProperties(this, versionProperties.toTypedArray(), preferredVersions.toTypedArray())
+ }
+
+ internal var comboBoxes: Array>>? = null
+
+ abstract fun getAvailableVersions(versionsAbove: List>): List>
+
+ fun getVersionProperty(index: Int) = versionProperties.versionProperties[index]
+
+ fun getVersion(index: Int) = versionProperties.versionProperties[index].get()
+
+ fun getVersionBox(index: Int) = comboBoxes?.let { it[index] }
+
+ open fun setSelectableItems(index: Int, items: List>) {
+ getVersionBox(index)!!.model = CollectionComboBoxModel(items)
+ }
+
+ open fun createComboBox(row: Row, index: Int, items: List>): Cell>> {
+ return row.comboBox(items)
+ }
+
+ override fun setupUI(builder: Panel) {
+ val comboBoxes = mutableListOf>>()
+ with(builder) {
+ for ((i, label) in labels.withIndex()) {
+ row(label) {
+ val comboBox = createComboBox(
+ this, i,
+ getAvailableVersions(
+ versionProperties.versionProperties
+ .take(i).map(ObservableMutableProperty>::get)
+ ).sortedDescending()
+ ).bindItem(versionProperties.versionProperties[i])
+ comboBoxes += comboBox.component
+ }
+ }
+ }
+ this.comboBoxes = comboBoxes.toTypedArray()
+ }
+}
+
+private typealias PreferredVersionStateValue = List