diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f58ca5f03..b0908e67e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,6 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: true + - name: Fetch latest submodule updates + run: git submodule update --remote - uses: actions/setup-java@v3 with: distribution: 'zulu' diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..ed1c5f036 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "templates"] + path = templates + branch = main + url = https://github.com/minecraft-dev/templates diff --git a/build.gradle.kts b/build.gradle.kts index a47d000e1..48e4ab67b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -72,6 +72,26 @@ val gradleToolingExtensionJar = tasks.register(gradleToolingExtensionSource archiveClassifier.set("gradle-tooling-extension") } +val templatesSourceSet: SourceSet = sourceSets.create("templates") { + resources { + srcDir("templates") + compileClasspath += sourceSets.main.get().output + } +} + +val templateSourceSets: List = (file("templates").listFiles() ?: emptyArray()).mapNotNull { file -> + if (file.isDirectory() && (file.listFiles() ?: emptyArray()).any { it.name.endsWith(".mcdev.template.json") }) { + sourceSets.create("templates-${file.name}") { + resources { + srcDir(file) + compileClasspath += sourceSets.main.get().output + } + } + } else { + null + } +} + val externalAnnotationsJar = tasks.register("externalAnnotationsJar") { from("externalAnnotations") destinationDirectory.set(layout.buildDirectory.dir("externalAnnotations")) @@ -381,6 +401,9 @@ tasks.withType { from(externalAnnotationsJar) { into("Minecraft Development/lib/resources") } + from("templates") { + into("Minecraft Development/lib/resources/builtin-templates") + } } tasks.runIde { @@ -391,8 +414,8 @@ tasks.runIde { systemProperty("idea.debug.mode", "true") } // Set these properties to test different languages - // systemProperty("user.language", "en") - // systemProperty("user.country", "US") + systemProperty("user.language", "fr") + systemProperty("user.country", "FR") } tasks.buildSearchableOptions { diff --git a/src/main/kotlin/MinecraftConfigurable.kt b/src/main/kotlin/MinecraftConfigurable.kt index 815131f7f..60f5a32ab 100644 --- a/src/main/kotlin/MinecraftConfigurable.kt +++ b/src/main/kotlin/MinecraftConfigurable.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.creator.custom.templateRepoTable import com.demonwav.mcdev.update.ConfigurePluginUpdatesDialog import com.intellij.ide.projectView.ProjectView import com.intellij.openapi.options.Configurable @@ -31,6 +32,7 @@ import com.intellij.ui.EnumComboBoxModel import com.intellij.ui.components.Label import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.MutableProperty import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel @@ -91,6 +93,19 @@ class MinecraftConfigurable : Configurable { } } + group(MCDevBundle("minecraft.settings.creator")) { + row(MCDevBundle("minecraft.settings.creator.repos")) {} + + row { + templateRepoTable( + MutableProperty( + { settings.creatorTemplateRepos.toMutableList() }, + { settings.creatorTemplateRepos = it } + ) + ) + }.resizableRow() + } + onApply { for (project in ProjectManager.getInstance().openProjects) { ProjectView.getInstance(project).refresh() diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt index b4b596114..0a924aa64 100644 --- a/src/main/kotlin/MinecraftSettings.kt +++ b/src/main/kotlin/MinecraftSettings.kt @@ -20,11 +20,15 @@ package com.demonwav.mcdev +import com.demonwav.mcdev.asset.MCDevBundle import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.editor.markup.EffectType +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.Tag +import com.intellij.util.xmlb.annotations.Text @State(name = "MinecraftSettings", storages = [Storage("minecraft_dev.xml")]) class MinecraftSettings : PersistentStateComponent { @@ -37,8 +41,29 @@ class MinecraftSettings : PersistentStateComponent { var underlineType: UnderlineType = UnderlineType.DOTTED, var isShadowAnnotationsSameLine: Boolean = true, + + var creatorTemplateRepos: List = listOf(TemplateRepo.makeBuiltinRepo()), ) + @Tag("repo") + data class TemplateRepo( + @get:Attribute("name") + var name: String, + @get:Attribute("provider") + var provider: String, + @get:Text + var data: String + ) { + constructor() : this("", "", "") + + companion object { + + fun makeBuiltinRepo(): TemplateRepo { + return TemplateRepo(MCDevBundle("minecraft.settings.creator.repo.builtin_name"), "builtin", "true") + } + } + } + private var state = State() override fun getState(): State { @@ -47,6 +72,9 @@ class MinecraftSettings : PersistentStateComponent { override fun loadState(state: State) { this.state = state + if (state.creatorTemplateRepos.isEmpty()) { + state.creatorTemplateRepos = listOf() + } } // State mappings @@ -86,6 +114,12 @@ class MinecraftSettings : PersistentStateComponent { state.isShadowAnnotationsSameLine = shadowAnnotationsSameLine } + var creatorTemplateRepos: List + get() = state.creatorTemplateRepos.map { it.copy() } + set(creatorTemplateRepos) { + state.creatorTemplateRepos = creatorTemplateRepos.map { it.copy() } + } + enum class UnderlineType(private val regular: String, val effectType: EffectType) { NORMAL("Normal", EffectType.LINE_UNDERSCORE), diff --git a/src/main/kotlin/creator/MinecraftModuleBuilder.kt b/src/main/kotlin/creator/MinecraftModuleBuilder.kt index 7b3f2318c..a847ccf14 100644 --- a/src/main/kotlin/creator/MinecraftModuleBuilder.kt +++ b/src/main/kotlin/creator/MinecraftModuleBuilder.kt @@ -35,7 +35,7 @@ import com.intellij.openapi.roots.ModifiableRootModel class MinecraftModuleBuilder : AbstractNewProjectWizardBuilder() { - override fun getPresentableName() = "Minecraft" + override fun getPresentableName() = "Minecraft (Old Wizard)" override fun getNodeIcon() = PlatformAssets.MINECRAFT_ICON override fun getGroupName() = "Minecraft" override fun getBuilderId() = "MINECRAFT_MODULE" diff --git a/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt b/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt index 6aa8694bb..6c3b70902 100644 --- a/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt +++ b/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt @@ -126,7 +126,9 @@ class JdkProjectSetupFinalizer( private var preferredJdkLabel: Placeholder? = null private var preferredJdkReason = MCDevBundle("creator.validation.jdk_preferred_default_reason") - var preferredJdk: JavaSdkVersion = JavaSdkVersion.JDK_17 + val preferredJdkProperty = propertyGraph.property(JavaSdkVersion.JDK_17) + + var preferredJdk: JavaSdkVersion by preferredJdkProperty private set fun setPreferredJdk(value: JavaSdkVersion, reason: String) { diff --git a/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt b/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt index 51614b1fb..887682753 100644 --- a/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt +++ b/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt @@ -49,7 +49,7 @@ abstract class AbstractBuildSystemStep( override val self get() = this override val label - get() = MCDevBundle("creator.ui.build_system.label.generic") + get() = MCDevBundle("creator.ui.build_system.label") override fun initSteps(): LinkedHashMap { context.putUserData(PLATFORM_NAME_KEY, platformName) diff --git a/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt b/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt index bc6324f54..67cc8a3ef 100644 --- a/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt +++ b/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt @@ -52,7 +52,7 @@ class BuildSystemPropertiesStep(private val parent: ParentStep) : Ab val groupIdProperty = propertyGraph.property("org.example") .bindStorage("${javaClass.name}.groupId") val artifactIdProperty = propertyGraph.lazyProperty(::suggestArtifactId) - private val versionProperty = propertyGraph.property("1.0-SNAPSHOT") + val versionProperty = propertyGraph.property("1.0-SNAPSHOT") .bindStorage("${javaClass.name}.version") var groupId by groupIdProperty diff --git a/src/main/kotlin/creator/creator-utils.kt b/src/main/kotlin/creator/creator-utils.kt index 687793ddf..a1ab81512 100644 --- a/src/main/kotlin/creator/creator-utils.kt +++ b/src/main/kotlin/creator/creator-utils.kt @@ -26,11 +26,15 @@ import com.demonwav.mcdev.creator.step.LicenseStep import com.demonwav.mcdev.util.MinecraftTemplates import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.starters.local.GeneratorTemplateFile +import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.ide.wizard.AbstractNewProjectWizardStep +import com.intellij.ide.wizard.AbstractWizard import com.intellij.ide.wizard.GitNewProjectWizardData import com.intellij.ide.wizard.NewProjectWizardStep import com.intellij.notification.Notification import com.intellij.notification.NotificationType +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.project.Project @@ -160,3 +164,15 @@ fun notifyCreatedProjectNotOpened() { NotificationType.ERROR, ).notify(null) } + +val WizardContext.modalityState: ModalityState + get() { + val contentPanel = this.getUserData(AbstractWizard.KEY)?.contentPanel + + if (contentPanel == null) { + thisLogger().error("Wizard content panel is null, using default modality state") + return ModalityState.defaultModalityState() + } + + return ModalityState.stateForComponent(contentPanel) + } diff --git a/src/main/kotlin/creator/custom/BuiltinValidations.kt b/src/main/kotlin/creator/custom/BuiltinValidations.kt new file mode 100644 index 000000000..8fd1a8401 --- /dev/null +++ b/src/main/kotlin/creator/custom/BuiltinValidations.kt @@ -0,0 +1,78 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.validation.DialogValidation +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.util.text.StringUtil +import javax.swing.JComponent + +object BuiltinValidations { + val nonBlank = validationErrorIf(MCDevBundle("creator.validation.blank")) { it.isBlank() } + + val validVersion = validationErrorIf(MCDevBundle("creator.validation.semantic_version")) { + SemanticVersion.tryParse(it) == null + } + + val nonEmptyVersion = DialogValidation.WithParameter> { combobox -> + DialogValidation { + if (combobox.item?.parts.isNullOrEmpty()) { + ValidationInfo(MCDevBundle("creator.validation.semantic_version")) + } else { + null + } + } + } + + val nonEmptyYarnVersion = DialogValidation.WithParameter> { combobox -> + DialogValidation { + if (combobox.item == null) { + ValidationInfo(MCDevBundle("creator.validation.semantic_version")) + } else { + null + } + } + } + + val validClassFqn = validationErrorIf(MCDevBundle("creator.validation.class_fqn")) { + it.isBlank() || it.split('.').any { part -> !StringUtil.isJavaIdentifier(part) } + } + + fun byRegex(regex: Regex): DialogValidation.WithParameter<() -> String> = + validationErrorIf(MCDevBundle("creator.validation.regex", regex)) { !it.matches(regex) } + + fun isAnyOf( + selectionGetter: () -> T, + options: Collection, + component: JComponent? = null + ): DialogValidation = DialogValidation { + if (selectionGetter() !in options) { + return@DialogValidation ValidationInfo(MCDevBundle("creator.validation.invalid_option"), component) + } + + return@DialogValidation null + } +} diff --git a/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt b/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt new file mode 100644 index 000000000..2bad9bf12 --- /dev/null +++ b/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt @@ -0,0 +1,58 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.progress.TaskInfo +import com.intellij.openapi.progress.util.ProgressIndicatorBase + +class CreatorProgressIndicator( + val loadingProperty: GraphProperty? = null, + val textProperty: GraphProperty? = null, + val text2Property: GraphProperty? = null, +) : ProgressIndicatorBase(false, false) { + + init { + loadingProperty?.set(false) + textProperty?.set("") + text2Property?.set("") + } + + override fun start() { + super.start() + loadingProperty?.set(true) + } + + override fun finish(task: TaskInfo) { + super.finish(task) + loadingProperty?.set(false) + } + + override fun setText(text: String?) { + super.setText(text) + textProperty?.set(text ?: "") + } + + override fun setText2(text: String?) { + super.setText2(text) + text2Property?.set(text ?: "") + } +} diff --git a/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt b/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt new file mode 100644 index 000000000..65a95052e --- /dev/null +++ b/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt @@ -0,0 +1,58 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.creator.step.NewProjectWizardChainStep.Companion.nextStep +import com.intellij.ide.projectWizard.ProjectSettingsStep +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.ide.wizard.AbstractNewProjectWizardBuilder +import com.intellij.ide.wizard.GitNewProjectWizardStep +import com.intellij.ide.wizard.NewProjectWizardBaseStep +import com.intellij.ide.wizard.RootNewProjectWizardStep +import com.intellij.openapi.roots.ModifiableRootModel + +class CustomMinecraftModuleBuilder : AbstractNewProjectWizardBuilder() { + + override fun getPresentableName() = "Minecraft" + override fun getNodeIcon() = PlatformAssets.MINECRAFT_ICON + override fun getGroupName() = "Minecraft" + override fun getBuilderId() = "CUSTOM_MINECRAFT_MODULE" + override fun getDescription() = MCDevBundle("creator.ui.create_minecraft_project") + + override fun setupRootModel(modifiableRootModel: ModifiableRootModel) { + if (moduleJdk != null) { + modifiableRootModel.sdk = moduleJdk + } else { + modifiableRootModel.inheritSdk() + } + } + + override fun getParentGroup() = "Minecraft" + + override fun createStep(context: WizardContext) = RootNewProjectWizardStep(context) + .nextStep(::NewProjectWizardBaseStep) + .nextStep(::GitNewProjectWizardStep) + .nextStep(::CustomPlatformStep) + + override fun getIgnoredSteps() = listOf(ProjectSettingsStep::class.java) +} diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt new file mode 100644 index 000000000..8c54b0bf9 --- /dev/null +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -0,0 +1,567 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.finalizers.CreatorFinalizer +import com.demonwav.mcdev.creator.custom.providers.EmptyLoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.TemplateProvider +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.creator.custom.types.CreatorPropertyFactory +import com.demonwav.mcdev.creator.custom.types.ExternalCreatorProperty +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.toTypedArray +import com.demonwav.mcdev.util.virtualFileOrError +import com.intellij.codeInsight.actions.ReformatCodeProcessor +import com.intellij.ide.projectView.ProjectView +import com.intellij.ide.wizard.AbstractNewProjectWizardStep +import com.intellij.ide.wizard.GitNewProjectWizardData +import com.intellij.ide.wizard.NewProjectWizardBaseData +import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.observable.util.transform +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.roots.ModuleRootManager +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.refreshAndFindVirtualFile +import com.intellij.psi.PsiManager +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Placeholder +import com.intellij.ui.dsl.builder.SegmentedButton +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import java.nio.file.Path +import java.util.function.Consumer +import javax.swing.JLabel +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +/** + * The step to select a custom template repo. + */ +class CustomPlatformStep( + parent: NewProjectWizardStep, +) : AbstractNewProjectWizardStep(parent) { + + val templateRepos = MinecraftSettings.instance.creatorTemplateRepos + + val templateRepoProperty = propertyGraph.property( + templateRepos.firstOrNull() ?: MinecraftSettings.TemplateRepo.makeBuiltinRepo() + ) + var templateRepo by templateRepoProperty + + val availableGroupsProperty = propertyGraph.property>(emptyList()) + var availableGroups by availableGroupsProperty + val availableTemplatesProperty = propertyGraph.property>(emptyList()) + var availableTemplates by availableTemplatesProperty + lateinit var availableGroupsSegmentedButton: SegmentedButton + lateinit var availableTemplatesSegmentedButton: SegmentedButton + + val selectedGroupProperty = propertyGraph.property("") + var selectedGroup by selectedGroupProperty + val selectedTemplateProperty = propertyGraph.property(EmptyLoadedTemplate) + var selectedTemplate by selectedTemplateProperty + + val templateProvidersLoadingProperty = propertyGraph.property(true) + val templateProvidersTextProperty = propertyGraph.property("") + val templateProvidersText2Property = propertyGraph.property("") + lateinit var templateProvidersProcessIcon: Cell + + val templateLoadingProperty = propertyGraph.property(true) + val templateLoadingTextProperty = propertyGraph.property("") + val templateLoadingText2Property = propertyGraph.property("") + lateinit var templatePropertiesProcessIcon: Cell + lateinit var noTemplatesAvailable: Cell + var templateLoadingIndicator: ProgressIndicator? = null + + private var hasTemplateErrors: Boolean = true + + private var properties = mutableMapOf>() + + override fun setupUI(builder: Panel) { + lateinit var templatePropertyPlaceholder: Placeholder + + builder.row(MCDevBundle("creator.ui.custom.repos.label")) { + segmentedButton(templateRepos) { it.name } + .bind(templateRepoProperty) + }.visible(templateRepos.size > 1) + + builder.row { + templateProvidersProcessIcon = + cell(AsyncProcessIcon("TemplateProviders init")) + .visibleIf(templateProvidersLoadingProperty) + label(MCDevBundle("creator.step.generic.init_template_providers.message")) + .visibleIf(templateProvidersLoadingProperty) + label("") + .bindText(templateProvidersTextProperty) + .visibleIf(templateProvidersLoadingProperty) + label("") + .bindText(templateProvidersText2Property) + .visibleIf(templateProvidersLoadingProperty) + } + + templateRepoProperty.afterChange { templateRepo -> + templatePropertyPlaceholder.component = null + availableTemplates = emptyList() + loadTemplatesInBackground { + val provider = TemplateProvider.get(templateRepo.provider) + provider?.loadTemplates(context, templateRepo).orEmpty() + } + } + + builder.row(MCDevBundle("creator.ui.custom.groups.label")) { + availableGroupsSegmentedButton = + segmentedButton(emptyList(), String::toString) + .bind(selectedGroupProperty) + }.visibleIf( + availableGroupsProperty.transform { it.size > 1 } + ) + + builder.row(MCDevBundle("creator.ui.custom.templates.label")) { + availableTemplatesSegmentedButton = + segmentedButton(emptyList(), LoadedTemplate::label, LoadedTemplate::tooltip) + .bind(selectedTemplateProperty) + .validation { + addApplyRule("", condition = ::hasTemplateErrors) + } + }.visibleIf( + availableTemplatesProperty.transform { it.size > 1 } + ) + + availableTemplatesProperty.afterChange { newTemplates -> + val groups = newTemplates.mapTo(linkedSetOf()) { it.descriptor.translatedGroup } + availableGroupsSegmentedButton.items(groups) + // availableGroupsSegmentedButton.visible(groups.size > 1) + availableGroups = groups + selectedGroup = groups.firstOrNull() ?: "empty" + } + + selectedGroupProperty.afterChange { group -> + val templates = availableTemplates.filter { it.descriptor.translatedGroup == group } + availableTemplatesSegmentedButton.items(templates) + // Force visiblity because the component might become hidden and not show up again + // when the segmented button switches between dropdown and buttons + availableTemplatesSegmentedButton.visible(true) + templatePropertyPlaceholder.component = null + selectedTemplate = templates.firstOrNull() ?: EmptyLoadedTemplate + } + + selectedTemplateProperty.afterChange { template -> + createOptionsPanelInBackground(template, templatePropertyPlaceholder) + } + + builder.row { + templatePropertiesProcessIcon = + cell(AsyncProcessIcon("Templates loading")) + .visibleIf(templateLoadingProperty) + label(MCDevBundle("creator.step.generic.load_template.message")) + .visibleIf(templateLoadingProperty) + label("") + .bindText(templateLoadingTextProperty) + .visibleIf(templateLoadingProperty) + label("") + .bindText(templateLoadingText2Property) + .visibleIf(templateLoadingProperty) + noTemplatesAvailable = label(MCDevBundle("creator.step.generic.no_templates_available.message")) + .visible(false) + .apply { component.foreground = JBColor.RED } + templatePropertyPlaceholder = placeholder().align(AlignX.FILL) + }.topGap(TopGap.SMALL) + + initTemplates() + } + + private fun initTemplates() { + selectedTemplate = EmptyLoadedTemplate + + val task = object : Task.Backgroundable( + context.project, + MCDevBundle("creator.step.generic.init_template_providers.message"), + true, + ALWAYS_BACKGROUND, + ) { + + override fun run(indicator: ProgressIndicator) { + if (project?.isDisposed == true) { + return + } + + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateProvidersLoadingProperty.set(true) + VirtualFileManager.getInstance().syncRefresh() + }, context.modalityState) + + for ((providerKey, repos) in templateRepos.groupBy { it.provider }) { + ProgressManager.checkCanceled() + val provider = TemplateProvider.get(providerKey) + ?: continue + indicator.text = provider.label + runCatching { provider.init(indicator, repos) } + .getOrLogException(logger()) + } + + ProgressManager.checkCanceled() + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateProvidersLoadingProperty.set(false) + // Force refresh to trigger template loading + templateRepoProperty.set(templateRepo) + }, context.modalityState) + } + } + + val indicator = CreatorProgressIndicator( + templateProvidersLoadingProperty, + templateProvidersTextProperty, + templateProvidersText2Property + ) + ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) + } + + private fun loadTemplatesInBackground(provider: () -> Collection) { + selectedTemplate = EmptyLoadedTemplate + + val task = object : Task.Backgroundable( + context.project, + MCDevBundle("creator.step.generic.load_template.message"), + true, + ALWAYS_BACKGROUND, + ) { + + override fun run(indicator: ProgressIndicator) { + if (project?.isDisposed == true) { + return + } + + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateLoadingProperty.set(true) + VirtualFileManager.getInstance().syncRefresh() + }, context.modalityState) + + ProgressManager.checkCanceled() + val newTemplates = runCatching { provider() } + .getOrLogException(logger()) + ?: emptyList() + + ProgressManager.checkCanceled() + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateLoadingProperty.set(false) + noTemplatesAvailable.visible(newTemplates.isEmpty()) + availableTemplates = newTemplates + }, context.modalityState) + } + } + + templateLoadingIndicator?.cancel() + + val indicator = CreatorProgressIndicator( + templateLoadingProperty, + templateLoadingTextProperty, + templateLoadingText2Property + ) + templateLoadingIndicator = indicator + ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) + } + + private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { + properties = mutableMapOf() + + if (!template.isValid) { + return + } + + val baseData = data.getUserData(NewProjectWizardBaseData.KEY) + ?: return thisLogger().error("Could not find wizard base data") + + properties["PROJECT_NAME"] = ExternalCreatorProperty( + graph = propertyGraph, + properties = properties, + graphProperty = baseData.nameProperty, + valueType = String::class.java + ) + + placeholder.component = panel { + val reporter = TemplateValidationReporterImpl() + val uiFactories = setupTemplate(template, reporter) + if (uiFactories.isEmpty() && !reporter.hasErrors) { + row { + label(MCDevBundle("creator.ui.warn.no_properties")) + .component.foreground = JBColor.YELLOW + } + } else { + hasTemplateErrors = reporter.hasErrors + reporter.display(this) + + if (!reporter.hasErrors) { + for (uiFactory in uiFactories) { + uiFactory.accept(this) + } + } + } + } + } + + private fun setupTemplate( + template: LoadedTemplate, + reporter: TemplateValidationReporterImpl + ): List> { + return try { + val properties = template.descriptor.properties.orEmpty() + .mapNotNull { + reporter.subject = it.name + setupProperty(it, reporter) + } + .sortedBy { (_, order) -> order } + .map { it.first } + + val finalizers = template.descriptor.finalizers + if (finalizers != null) { + CreatorFinalizer.validateAll(reporter, finalizers) + } + + properties + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error( + "Unexpected error during template setup", + t, + template.label, + template.descriptor.toString() + ) + + emptyList() + } finally { + reporter.subject = null + } + } + + private fun setupProperty( + descriptor: TemplatePropertyDescriptor, + reporter: TemplateValidationReporter + ): Pair, Int>? { + if (!descriptor.groupProperties.isNullOrEmpty()) { + val childrenUiFactories = descriptor.groupProperties + .mapNotNull { setupProperty(it, reporter) } + .sortedBy { (_, order) -> order } + .map { it.first } + + val factory = Consumer { panel -> + val label = descriptor.translatedLabel + if (descriptor.collapsible == false) { + panel.group(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@group) + } + } + } else { + val group = panel.collapsibleGroup(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@collapsibleGroup) + } + } + + group.expanded = descriptor.default as? Boolean ?: false + } + } + + val order = descriptor.order ?: 0 + return factory to order + } + + if (descriptor.name in properties.keys) { + reporter.fatal("Duplicate property name ${descriptor.name}") + } + + val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, propertyGraph, properties) + if (prop == null) { + reporter.fatal("Unknown template property type ${descriptor.type}") + } + + prop.setupProperty(reporter) + + properties[descriptor.name] = prop + + if (descriptor.visible == false) { + return null + } + + val factory = Consumer { panel -> prop.buildUi(panel, context) } + val order = descriptor.order ?: 0 + return factory to order + } + + override fun setupProject(project: Project) { + val template = selectedTemplate + if (template is EmptyLoadedTemplate) { + return + } + + val projectPath = context.projectDirectory + val templateProperties = collectTemplateProperties() + thisLogger().debug("Template properties: $templateProperties") + + val generatedFiles = mutableListOf>() + for (file in template.descriptor.files.orEmpty()) { + if (file.condition != null && + !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } + ) { + continue + } + + val relativeTemplate = TemplateEvaluator.template(templateProperties, file.template).getOrNull() + ?: continue + val relativeDest = TemplateEvaluator.template(templateProperties, file.destination).getOrNull() + ?: continue + + try { + val templateContents = template.loadTemplateContents(relativeTemplate) + ?: continue + + val destPath = projectPath.resolve(relativeDest).toAbsolutePath() + if (!destPath.startsWith(projectPath)) { + // We want to make sure template files aren't 'escaping' the project directory + continue + } + + var fileTemplateProperties = templateProperties + if (file.properties != null) { + fileTemplateProperties = templateProperties.toMutableMap() + fileTemplateProperties.putAll(file.properties) + } + + val processedContent = TemplateEvaluator.template(fileTemplateProperties, templateContents) + .onFailure { t -> + val attachment = Attachment(relativeTemplate, templateContents) + thisLogger().error("Failed evaluate template '$relativeTemplate'", t, attachment) + } + .getOrNull() + ?: continue + + destPath.parent.createDirectories() + destPath.writeText(processedContent) + + val virtualFile = destPath.refreshAndFindVirtualFile() + if (virtualFile != null) { + generatedFiles.add(file to virtualFile) + } else { + thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") + } + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to process template file $file", t) + } + } + + application.executeOnPooledThread { + application.invokeLater({ + application.runWriteAction { + LocalFileSystem.getInstance().refresh(false) + // Apparently a module root is required for the reformat to work + setupTempRootModule(project, projectPath) + } + reformatFiles(project, generatedFiles) + openFilesInEditor(project, generatedFiles) + }, project.disposed) + + val finalizers = selectedTemplate.descriptor.finalizers + if (!finalizers.isNullOrEmpty()) { + CreatorFinalizer.executeAll(context, finalizers, templateProperties) + } + } + } + + private fun setupTempRootModule(project: Project, projectPath: Path) { + val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() + val module = modifiableModel.newNonPersistentModule("mcdev-temp-root", ModuleTypeId.JAVA_MODULE) + val rootsModel = ModuleRootManager.getInstance(module).modifiableModel + rootsModel.addContentEntry(projectPath.virtualFileOrError) + rootsModel.commit() + modifiableModel.commit() + } + + private fun collectTemplateProperties(): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + val gitData = data.getUserData(GitNewProjectWizardData.KEY) + into["USE_GIT"] = gitData?.git == true + + return properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } + + private fun reformatFiles( + project: Project, + files: MutableList> + ) { + val psiManager = PsiManager.getInstance(project) + val psiFiles = files.asSequence() + .filter { (desc, _) -> desc.reformat != false } + .mapNotNull { (_, file) -> psiManager.findFile(file) } + ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false).run() + } + + private fun openFilesInEditor( + project: Project, + files: MutableList> + ) { + val fileEditorManager = FileEditorManager.getInstance(project) + val projectView = ProjectView.getInstance(project) + for ((desc, file) in files) { + if (desc.openInEditor == true) { + fileEditorManager.openFile(file, true) + projectView.select(null, file, false) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt b/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt new file mode 100644 index 000000000..d2fc2c771 --- /dev/null +++ b/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt @@ -0,0 +1,81 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class EvaluateTemplateExpressionAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + + val dialog = EvaluateDialog() + dialog.isModal = false + dialog.show() + } + + private class EvaluateDialog : DialogWrapper(null, false, IdeModalityType.IDE) { + val document = EditorFactory.getInstance().createDocument("") + val editor = EditorFactory.getInstance().createEditor(document) as EditorEx + + lateinit var field: JBTextField + + init { + title = "Evaluate Template Expression" + isOKActionEnabled = true + setValidationDelay(0) + + Disposer.register(disposable) { + EditorFactory.getInstance().releaseEditor(editor) + } + + init() + } + + override fun createCenterPanel(): JComponent = panel { + row { + cell(editor.component).align(Align.FILL) + } + + row("Result:") { + field = textField().align(Align.FILL).component + field.isEditable = false + } + } + + override fun doOKAction() { + val props = mapOf( + "BUILD_SYSTEM" to "gradle", + "USE_PAPER_MANIFEST" to false, + "MAIN_CLASS" to ClassFqn("io.github.rednesto.test.Test") + ) + field.text = TemplateEvaluator.evaluate(props, document.text).toString() + } + } +} diff --git a/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt b/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt new file mode 100644 index 000000000..c90077f45 --- /dev/null +++ b/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt @@ -0,0 +1,47 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.intellij.openapi.util.text.StringUtil +import java.util.MissingResourceException +import java.util.ResourceBundle +import org.jetbrains.annotations.Nls +import org.jetbrains.annotations.NonNls + +abstract class ResourceBundleTranslator { + + abstract val bundle: ResourceBundle? + + fun translate(key: @NonNls String): @Nls String { + return translateOrNull(key) ?: StringUtil.escapeMnemonics(key) + } + + fun translateOrNull(key: @NonNls String): @Nls String? { + if (bundle != null) { + try { + return bundle!!.getString(key) + } catch (_: MissingResourceException) { + } + } + return MCDevBundle.messageOrNull(key) + } +} diff --git a/src/main/kotlin/creator/custom/TemplateDescriptor.kt b/src/main/kotlin/creator/custom/TemplateDescriptor.kt new file mode 100644 index 000000000..abed981b6 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateDescriptor.kt @@ -0,0 +1,99 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import java.util.ResourceBundle + +data class TemplateDescriptor( + val version: Int, + val label: String? = null, + val group: String? = null, + val inherit: String? = null, + val hidden: Boolean? = null, + val properties: List? = null, + val files: List? = null, + val finalizers: List>? = null, +) : ResourceBundleTranslator() { + @Transient + override var bundle: ResourceBundle? = null + + val translatedGroup: String + get() = translate("creator.ui.group.${(group ?: "default").lowercase()}.label") + + companion object { + + const val FORMAT_VERSION = 1 + } +} + +data class TemplatePropertyDescriptor( + val name: String, + val type: String, + val label: String? = null, + val order: Int? = null, + val options: Any? = null, + val limit: Int? = null, + val maxSegmentedButtonsCount: Int? = null, + val forceDropdown: Boolean? = null, + val groupProperties: List? = null, + val remember: Any? = null, + val visible: Any? = null, + val editable: Boolean? = null, + val collapsible: Boolean? = null, + val warning: String? = null, + val default: Any?, + val nullIfDefault: Boolean? = null, + val derives: PropertyDerivation? = null, + val inheritFrom: String? = null, + val parameters: Map? = null, + val validator: Any? = null +) : ResourceBundleTranslator() { + @Transient + override var bundle: ResourceBundle? = null + + val translatedLabel: String + get() = translate(label ?: "creator.ui.${name.lowercase()}.label") + val translatedWarning: String? + get() = translateOrNull(label ?: "creator.ui.${name.lowercase()}.warning") ?: warning +} + +data class PropertyDerivation( + val parents: List? = null, + val method: String? = null, + val select: List? = null, + val default: Any? = null, + val whenModified: Boolean? = null, + val parameters: Map? = null, +) + +data class PropertyDerivationSelect( + val condition: String, + val value: Any +) + +data class TemplateFile( + val template: String, + val destination: String, + val condition: String? = null, + val properties: Map? = null, + val reformat: Boolean? = null, + val openInEditor: Boolean? = null, +) diff --git a/src/main/kotlin/creator/custom/TemplateEvaluator.kt b/src/main/kotlin/creator/custom/TemplateEvaluator.kt new file mode 100644 index 000000000..b717fe712 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateEvaluator.kt @@ -0,0 +1,52 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion +import org.apache.velocity.VelocityContext +import org.apache.velocity.app.Velocity +import org.apache.velocity.util.StringBuilderWriter + +object TemplateEvaluator { + + val baseProperties = mapOf( + "semver" to SemanticVersion.Companion, + "mcver" to MinecraftVersions + ) + + fun evaluate(properties: Map, template: String): Result> { + val context = VelocityContext(baseProperties + properties) + val stringWriter = StringBuilderWriter() + return runCatching { + Velocity.evaluate(context, stringWriter, "McDevTplExpr", template) to stringWriter.toString() + } + } + + fun template(properties: Map, template: String): Result { + return evaluate(properties, template).map { it.second } + } + + fun condition(properties: Map, condition: String): Result { + val actualCondition = "#if ($condition) true #else false #end" + return evaluate(properties, actualCondition).map { it.second.trim().toBoolean() } + } +} diff --git a/src/main/kotlin/creator/custom/TemplateRepoTable.kt b/src/main/kotlin/creator/custom/TemplateRepoTable.kt new file mode 100644 index 000000000..d14f023f0 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateRepoTable.kt @@ -0,0 +1,133 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.providers.TemplateProvider +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.ComboBoxTableCellRenderer +import com.intellij.ui.ToolbarDecorator +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.MutableProperty +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.table.TableView +import com.intellij.util.ListWithSelection +import com.intellij.util.ui.ColumnInfo +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.ListTableModel +import com.intellij.util.ui.table.ComboBoxTableCellEditor +import java.awt.Dimension +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.table.TableCellEditor +import javax.swing.table.TableCellRenderer + +private object NameColumn : ColumnInfo( + MCDevBundle("minecraft.settings.creator.repos.column.name") +) { + override fun valueOf(item: MinecraftSettings.TemplateRepo?): String? { + return item?.name + } + + override fun setValue(item: MinecraftSettings.TemplateRepo?, value: String?) { + item?.name = value ?: MCDevBundle("minecraft.settings.creator.repo.default_name") + } + + override fun isCellEditable(item: MinecraftSettings.TemplateRepo?): Boolean = true +} + +private object ProviderColumn : ColumnInfo( + MCDevBundle("minecraft.settings.creator.repos.column.provider") +) { + override fun valueOf(item: MinecraftSettings.TemplateRepo?): ListWithSelection? { + val providers = TemplateProvider.getAllKeys() + val list = ListWithSelection(providers) + list.select(item?.provider?.takeIf(providers::contains)) + + return list + } + + override fun setValue(item: MinecraftSettings.TemplateRepo?, value: Any?) { + item?.provider = value as? String ?: "local" + } + + override fun isCellEditable(item: MinecraftSettings.TemplateRepo?): Boolean = true + + override fun getRenderer(item: MinecraftSettings.TemplateRepo?): TableCellRenderer? { + return ComboBoxTableCellRenderer.INSTANCE + } + + override fun getEditor(item: MinecraftSettings.TemplateRepo?): TableCellEditor? { + return ComboBoxTableCellEditor.INSTANCE + } +} + +fun Row.templateRepoTable( + prop: MutableProperty> +): Cell { + val model = object : ListTableModel(NameColumn, ProviderColumn) { + override fun addRow() { + val defaultName = MCDevBundle("minecraft.settings.creator.repo.default_name") + addRow(MinecraftSettings.TemplateRepo(defaultName, "local", "")) + } + } + + val table = TableView(model) + table.setShowGrid(true) + table.tableHeader.reorderingAllowed = false + + val decoratedTable = ToolbarDecorator.createDecorator(table) + .setPreferredSize(Dimension(JBUI.scale(300), JBUI.scale(200))) + .setEditActionUpdater { + val selectedRepo = table.selection.firstOrNull() + ?: return@setEditActionUpdater false + val provider = TemplateProvider.get(selectedRepo.provider) + ?: return@setEditActionUpdater false + return@setEditActionUpdater provider.hasConfig + } + .setEditAction { + val selectedRepo = table.selection.firstOrNull() + ?: return@setEditAction + val provider = TemplateProvider.get(selectedRepo.provider) + ?: return@setEditAction + val dataConsumer = { data: String -> selectedRepo.data = data } + val configPanel = provider.setupConfigUi(selectedRepo.data, dataConsumer) + ?: return@setEditAction + + val dialog = object : DialogWrapper(table, true) { + init { + init() + } + + override fun createCenterPanel(): JComponent = configPanel + } + dialog.title = MCDevBundle("minecraft.settings.creator.repo_config.title", selectedRepo.name) + dialog.show() + } + .createPanel() + return cell(decoratedTable) + .bind( + { _ -> model.items }, + { _, repos -> model.items = repos; }, + prop + ) +} diff --git a/src/main/kotlin/creator/custom/TemplateResourceBundle.kt b/src/main/kotlin/creator/custom/TemplateResourceBundle.kt new file mode 100644 index 000000000..ecb4d4bf0 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateResourceBundle.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import java.io.Reader +import java.util.PropertyResourceBundle +import java.util.ResourceBundle + +class TemplateResourceBundle(val reader: Reader, parent: ResourceBundle?) : PropertyResourceBundle(reader) { + + init { + this.parent = parent + } +} diff --git a/src/main/kotlin/creator/custom/TemplateValidationReporter.kt b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt new file mode 100644 index 000000000..b953eb16e --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt @@ -0,0 +1,106 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel + +interface TemplateValidationReporter { + + fun warn(message: String) + + fun error(message: String) + + fun fatal(message: String, cause: Throwable? = null): Nothing +} + +class TemplateValidationReporterImpl : TemplateValidationReporter { + + private val validationItems: MutableMap> = linkedMapOf() + var hasErrors = false + private set + var hasWarns = false + private set + + var subject: String? = null + + override fun warn(message: String) { + check(subject != null) { "No subject is being validated" } + hasWarns = true + validationItems.getOrPut(subject!!, ::mutableListOf).add(TemplateValidationItem.Warn(message)) + } + + override fun error(message: String) { + check(subject != null) { "No subject is being validated" } + hasErrors = true + validationItems.getOrPut(subject!!, ::mutableListOf).add(TemplateValidationItem.Error(message)) + } + + override fun fatal(message: String, cause: Throwable?): Nothing { + error("Fatal validation error: $message") + throw TemplateValidationException(message, cause) + } + + fun display(panel: Panel) { + if (!hasErrors && !hasWarns) { + return + } + + panel.row { + when { + hasWarns && hasErrors -> label(MCDevBundle("creator.ui.error.template_warns_and_errors")).apply { + component.foreground = JBColor.RED + } + + hasWarns -> label(MCDevBundle("creator.ui.error.template_warns")).apply { + component.foreground = JBColor.YELLOW + } + + hasErrors -> label(MCDevBundle("creator.ui.error.template_errors")).apply { + component.foreground = JBColor.RED + } + } + } + + for ((subjectName, items) in validationItems) { + panel.row { + label("$subjectName:") + } + + panel.indent { + for (item in items) { + row { + label(item.message).component.foreground = item.color + } + } + } + } + } +} + +class TemplateValidationException(message: String?, cause: Throwable? = null) : Exception(message, cause) + +private sealed class TemplateValidationItem(val message: String, val color: JBColor) { + + class Warn(message: String) : TemplateValidationItem(message, JBColor.YELLOW) + class Error(message: String) : TemplateValidationItem(message, JBColor.RED) +} diff --git a/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt new file mode 100644 index 000000000..186117050 --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt @@ -0,0 +1,66 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.SemanticVersion + +class ExtractVersionMajorMinorPropertyDerivation : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val from = parentValues[0] as SemanticVersion + if (from.parts.size < 2) { + return SemanticVersion(emptyList()) + } + + val (part1, part2) = from.parts + if (part1 is SemanticVersion.Companion.VersionPart.ReleasePart && + part2 is SemanticVersion.Companion.VersionPart.ReleasePart + ) { + return SemanticVersion(listOf(part1, part2)) + } + + return SemanticVersion(emptyList()) + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents.isNullOrEmpty()) { + reporter.error("Expected a parent") + return null + } + + if (!parents[0]!!.acceptsType(SemanticVersion::class.java)) { + reporter.error("First parent must produce a semantic version") + return null + } + + return ExtractVersionMajorMinorPropertyDerivation() + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt new file mode 100644 index 000000000..8d1aaf1dd --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty + +fun interface PreparedDerivation { + fun derive(parentValues: List): Any? +} + +interface PropertyDerivationFactory { + + fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? +} diff --git a/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt new file mode 100644 index 000000000..c762c09b5 --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt @@ -0,0 +1,74 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.HasMinecraftVersion +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion + +class RecommendJavaVersionForMcVersionPropertyDerivation(val default: Int) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val mcVersion: SemanticVersion = when (val version = parentValues[0]) { + is SemanticVersion -> version + is HasMinecraftVersion -> version.minecraftVersion + else -> return default + } + return MinecraftVersions.requiredJavaVersion(mcVersion).ordinal + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents.isNullOrEmpty()) { + reporter.error("Expected one parent") + return null + } + + if (parents.size > 1) { + reporter.warn("More than one parent defined") + } + + val parentValue = parents[0]!! + if (!parentValue.acceptsType(SemanticVersion::class.java) && + !parentValue.acceptsType(HasMinecraftVersion::class.java) + ) { + reporter.error("Parent must produce a semantic version or a value that has a Minecraft version") + return null + } + + val default = (derivation.default as? Number)?.toInt() + if (default == null) { + reporter.error("Default value is required and must be an integer") + return null + } + + return RecommendJavaVersionForMcVersionPropertyDerivation(default) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt new file mode 100644 index 000000000..c3688f9ad --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt @@ -0,0 +1,94 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty + +class ReplacePropertyDerivation( + val regex: Regex, + val replacement: String, + val maxLength: Int?, +) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val projectName = parentValues.first() as? String + ?: return null + + val sanitized = projectName.lowercase().replace(regex, replacement) + if (maxLength != null && sanitized.length > maxLength) { + return sanitized.substring(0, maxLength) + } + + return sanitized + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (derivation.parameters == null) { + reporter.error("Missing parameters") + return null + } + + if (parents.isNullOrEmpty()) { + reporter.error("Missing parent value") + return null + } + + if (parents.size > 2) { + reporter.warn("More than one parent defined") + } + + if (!parents[0]!!.acceptsType(String::class.java)) { + reporter.error("Parent property must produce a string value") + return null + } + + val regexString = derivation.parameters["regex"] as? String + if (regexString == null) { + reporter.error("Missing 'regex' string parameter") + return null + } + + val regex = try { + Regex(regexString) + } catch (t: Throwable) { + reporter.error("Invalid regex: '$regexString': ${t.message}") + return null + } + + val replacement = derivation.parameters["replacement"] as? String + if (replacement == null) { + reporter.error("Missing 'replacement' string parameter") + return null + } + + val maxLength = (derivation.parameters["maxLength"] as? Number)?.toInt() + return ReplacePropertyDerivation(regex, replacement, maxLength) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt new file mode 100644 index 000000000..88444bb5d --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt @@ -0,0 +1,67 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.PropertyDerivationSelect +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.thisLogger + +class SelectPropertyDerivation( + val parents: List?, + val options: List, + val default: Any?, +) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val properties = if (!parents.isNullOrEmpty()) { + parentValues.mapIndexed { i, value -> parents[i] to value }.toMap() + } else { + emptyMap() + } + for (option in options) { + if (TemplateEvaluator.condition(properties, option.condition).getOrLogException(thisLogger()) == true) { + return option.value + } + } + + return default + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (derivation.select == null) { + reporter.error("Missing select options") + return null + } + + return SelectPropertyDerivation(derivation.parents, derivation.select, derivation.default) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt new file mode 100644 index 000000000..8362a7b4a --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt @@ -0,0 +1,68 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.BuildSystemCoordinates +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.capitalize +import com.demonwav.mcdev.util.decapitalize + +class SuggestClassNamePropertyDerivation : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val coords = parentValues[0] as BuildSystemCoordinates + val name = parentValues[1] as String + return ClassFqn("${coords.groupId}.${name.decapitalize()}.${name.capitalize()}") + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents == null || parents.size < 2) { + reporter.error("Expected 2 parents") + return null + } + + if (parents.size > 2) { + reporter.warn("More than two parents defined") + } + + if (!parents[0]!!.acceptsType(BuildSystemCoordinates::class.java)) { + reporter.error("First parent must produce a build system coordinates") + return null + } + + if (!parents[1]!!.acceptsType(String::class.java)) { + reporter.error("Second parent must produce a string value") + return null + } + + return SuggestClassNamePropertyDerivation() + } + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt new file mode 100644 index 000000000..a65e225b7 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt @@ -0,0 +1,121 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.finalizers + +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.TemplateValidationReporterImpl +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute + +interface CreatorFinalizer { + + fun validate(reporter: TemplateValidationReporter, properties: Map) = Unit + + fun execute(context: WizardContext, properties: Map, templateProperties: Map) + + companion object { + private val EP_NAME = + ExtensionPointName.create("com.demonwav.minecraft-dev.creatorFinalizer") + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun validateAll( + reporter: TemplateValidationReporterImpl, + finalizers: List>, + ) { + for ((index, properties) in finalizers.withIndex()) { + reporter.subject = "Finalizer #$index" + + val type = properties["type"] as? String + if (type == null) { + reporter.error("Missing required 'type' value") + } + + val condition = properties["condition"] + if (condition != null && condition !is String) { + reporter.error("'condition' must be a string") + } + + if (type != null) { + val finalizer = COLLECTOR.findSingle(type) + if (finalizer == null) { + reporter.error("Unknown finalizer of type $type") + } else { + try { + finalizer.validate(reporter, properties) + } catch (t: Throwable) { + reporter.error("Unexpected error during finalizer validation: ${t.message}") + thisLogger().error("Unexpected error during finalizer validation", t) + } + } + } + } + } + + fun executeAll( + context: WizardContext, + finalizers: List>, + templateProperties: Map + ) { + for ((index, properties) in finalizers.withIndex()) { + val type = properties["type"] as String + val condition = properties["condition"] as? String + if (condition != null && + !TemplateEvaluator.condition(templateProperties, condition).getOrElse { false } + ) { + continue + } + + val finalizer = COLLECTOR.findSingle(type)!! + try { + finalizer.execute(context, properties, templateProperties) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + thisLogger().error("Unhandled exception in finalizer #$index ($type)", t) + } + } + } + } +} + +class CreatorFinalizerBean : BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("type") + @RequiredElement + lateinit var type: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getKey(): String? = type + + override fun getImplementationClassName(): String? = implementation +} diff --git a/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt new file mode 100644 index 000000000..ea099c9f8 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.finalizers + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.util.ExecUtil +import com.intellij.ide.util.projectWizard.WizardContext + +class GitAddAllFinalizer : CreatorFinalizer { + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + ExecUtil.execAndGetOutput(GeneralCommandLine("git", "add", ".").withWorkDirectory(context.projectFileDirectory)) + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt new file mode 100644 index 000000000..7385d945e --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt @@ -0,0 +1,40 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.finalizers + +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import org.jetbrains.plugins.gradle.service.project.open.canLinkAndRefreshGradleProject +import org.jetbrains.plugins.gradle.service.project.open.linkAndRefreshGradleProject + +class ImportGradleProjectFinalizer : CreatorFinalizer { + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + val project = context.project!! + val projectDir = context.projectFileDirectory + val canLink = canLinkAndRefreshGradleProject(projectDir, project, showValidationDialog = false) + thisLogger().info("canLink = $canLink projectDir = $projectDir") + if (canLink) { + linkAndRefreshGradleProject(projectDir, project) + thisLogger().info("Linking done") + } + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt new file mode 100644 index 000000000..fb0652c57 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt @@ -0,0 +1,57 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.finalizers + +import com.demonwav.mcdev.util.invokeAndWait +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.vfs.VfsUtil +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import org.jetbrains.idea.maven.project.importing.MavenImportingManager + +class ImportMavenProjectFinalizer : CreatorFinalizer { + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + val project = context.project!! + val projectDir = context.projectFileDirectory + + val pomFile = VfsUtil.findFile(Path.of(projectDir).resolve("pom.xml"), true) + ?: return + thisLogger().info("Invoking import on EDT pomFile = ${pomFile.path}") + val promise = invokeAndWait { + if (project.isDisposed || !project.isInitialized) { + return@invokeAndWait null + } + + MavenImportingManager.getInstance(project).linkAndImportFile(pomFile) + } + + if (promise == null) { + thisLogger().info("Could not start import") + return + } + + thisLogger().info("Waiting for import to finish") + promise.finishPromise.blockingGet(Int.MAX_VALUE, TimeUnit.SECONDS) + thisLogger().info("Import finished") + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt new file mode 100644 index 000000000..9d919d1f9 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt @@ -0,0 +1,54 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.finalizers + +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.util.runGradleTaskAndWait +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger + +class RunGradleTasksFinalizer : CreatorFinalizer { + + override fun validate( + reporter: TemplateValidationReporter, + properties: Map + ) { + @Suppress("UNCHECKED_CAST") + val tasks = properties["tasks"] as? List + if (tasks == null) { + reporter.warn("Missing list of 'tasks' to execute") + } + } + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + @Suppress("UNCHECKED_CAST") + val tasks = properties["tasks"] as List + val project = context.project!! + val projectDir = context.projectDirectory + + thisLogger().info("tasks = $tasks projectDir = $projectDir") + runGradleTaskAndWait(project, projectDir) { settings -> + settings.taskNames = tasks + } + + thisLogger().info("Done running tasks") + } +} diff --git a/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt b/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt new file mode 100644 index 000000000..d77d9e22e --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ArchitecturyVersionsModel( + val minecraft: SemanticVersion, + val forge: SemanticVersion?, + val neoforge: SemanticVersion?, + val loom: SemanticVersion, + val loader: SemanticVersion, + val yarn: FabricVersions.YarnVersion, + val useFabricApi: Boolean, + val fabricApi: SemanticVersion, + val useOfficialMappings: Boolean, + val useArchitecturyApi: Boolean, + val architecturyApi: SemanticVersion, +) : HasMinecraftVersion { + + override val minecraftVersion: SemanticVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + + val hasForge: Boolean by lazy { !forge?.parts.isNullOrEmpty() } + val forgeSpec: String? by lazy { forge?.parts?.getOrNull(0)?.versionString } + + val hasNeoforge: Boolean by lazy { !neoforge?.parts.isNullOrEmpty() } + val neoforgeSpec: String? by lazy { neoforge?.parts?.getOrNull(0)?.versionString } +} diff --git a/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt b/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt new file mode 100644 index 000000000..0eeab3c6e --- /dev/null +++ b/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt @@ -0,0 +1,27 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +@TemplateApi +data class BuildSystemCoordinates(val groupId: String, val artifactId: String, val version: String) { + + override fun toString(): String = "$groupId:$artifactId:$version" +} diff --git a/src/main/kotlin/creator/custom/model/ClassFqn.kt b/src/main/kotlin/creator/custom/model/ClassFqn.kt new file mode 100644 index 000000000..5383f3fac --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ClassFqn.kt @@ -0,0 +1,51 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +@TemplateApi +data class ClassFqn(val fqn: String) { + + /** + * The [Class.simpleName] of this class. + */ + val className by lazy { fqn.substringAfterLast('.') } + + /** + * The relative filesystem path to this class, without extension. + */ + val path by lazy { fqn.replace('.', '/') } + + /** + * The package name of this FQN as it would appear in source code. + */ + val packageName by lazy { fqn.substringBeforeLast('.') } + + /** + * The package path of this FQN reflected as a local filesystem path + */ + val packagePath by lazy { packageName.replace('.', '/') } + + fun withClassName(className: String) = copy("$packageName.$className") + + fun withSubPackage(name: String) = copy("$packageName.$name.$className") + + override fun toString(): String = fqn +} diff --git a/src/main/kotlin/creator/custom/model/CreatorJdk.kt b/src/main/kotlin/creator/custom/model/CreatorJdk.kt new file mode 100644 index 000000000..1e442b19b --- /dev/null +++ b/src/main/kotlin/creator/custom/model/CreatorJdk.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.intellij.openapi.projectRoots.JavaSdk +import com.intellij.openapi.projectRoots.Sdk + +@TemplateApi +data class CreatorJdk(val sdk: Sdk?) { + + val javaVersion: Int + get() = sdk?.let { JavaSdk.getInstance().getVersion(it)?.ordinal } ?: 17 +} diff --git a/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt b/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt new file mode 100644 index 000000000..c5111c7c5 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt @@ -0,0 +1,35 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class FabricVersionsModel( + override val minecraftVersion: SemanticVersion, + val loom: SemanticVersion, + val loader: SemanticVersion, + val yarn: FabricVersions.YarnVersion, + val useFabricApi: Boolean, + val fabricApi: SemanticVersion, + val useOfficialMappings: Boolean, +) : HasMinecraftVersion diff --git a/src/main/kotlin/creator/custom/model/ForgeVersions.kt b/src/main/kotlin/creator/custom/model/ForgeVersions.kt new file mode 100644 index 000000000..a308f4787 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ForgeVersions.kt @@ -0,0 +1,44 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ForgeVersions( + val minecraft: SemanticVersion, + val forge: SemanticVersion, +) : HasMinecraftVersion { + override val minecraftVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + val forgeSpec by lazy { forge.parts[0].versionString } +} diff --git a/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt b/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt new file mode 100644 index 000000000..c33cd9676 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt @@ -0,0 +1,29 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +interface HasMinecraftVersion { + + val minecraftVersion: SemanticVersion +} diff --git a/src/main/kotlin/creator/custom/model/LicenseData.kt b/src/main/kotlin/creator/custom/model/LicenseData.kt new file mode 100644 index 000000000..ddbf7932c --- /dev/null +++ b/src/main/kotlin/creator/custom/model/LicenseData.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import java.time.ZonedDateTime + +@TemplateApi +data class LicenseData( + val id: String, + val name: String, + val year: String = ZonedDateTime.now().year.toString(), +) diff --git a/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt b/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt new file mode 100644 index 000000000..c5a9bb2a2 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt @@ -0,0 +1,46 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class NeoForgeVersions( + val minecraft: SemanticVersion, + val neoforge: SemanticVersion, + val neogradle: SemanticVersion, + val moddev: SemanticVersion, +) : HasMinecraftVersion { + override val minecraftVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + val neoforgeSpec by lazy { neoforge.parts[0].versionString } +} diff --git a/src/main/kotlin/creator/custom/model/ParchmentVersions.kt b/src/main/kotlin/creator/custom/model/ParchmentVersions.kt new file mode 100644 index 000000000..0d11a3c74 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ParchmentVersions.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ParchmentVersions( + val use: Boolean, + val version: SemanticVersion, + override val minecraftVersion: SemanticVersion, + val includeOlderMcVersions: Boolean, + val includeSnapshots: Boolean, +) : HasMinecraftVersion diff --git a/src/main/kotlin/creator/custom/model/StringList.kt b/src/main/kotlin/creator/custom/model/StringList.kt new file mode 100644 index 000000000..d2b3bf09c --- /dev/null +++ b/src/main/kotlin/creator/custom/model/StringList.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +@TemplateApi +data class StringList(val values: List) : List by values { + + override fun toString(): String = values.joinToString() + + @JvmOverloads + fun toString(separator: String, prefix: String = "", postfix: String = ""): String = + values.joinToString(separator, prefix, postfix) +} diff --git a/src/main/kotlin/creator/custom/model/TemplateApi.kt b/src/main/kotlin/creator/custom/model/TemplateApi.kt new file mode 100644 index 000000000..fb053db2d --- /dev/null +++ b/src/main/kotlin/creator/custom/model/TemplateApi.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +/** + * Marker annotation indicating classes exposed to templates. + * + * Be careful of not breaking source or binary compatibility of those APIs without a good reason. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class TemplateApi diff --git a/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt new file mode 100644 index 000000000..1bc3bc9b6 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt @@ -0,0 +1,92 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.update.PluginUtil +import com.demonwav.mcdev.util.refreshSync +import com.demonwav.mcdev.util.virtualFile +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class BuiltinTemplateProvider : RemoteTemplateProvider() { + + private val builtinRepoUrl = "https://github.com/minecraft-dev/templates/archive/refs/heads/v\$version.zip" + private val builtinTemplatesPath = PluginUtil.plugin.pluginPath.resolve("lib/resources/builtin-templates") + private val builtinTemplatesInnerPath = "templates-${TemplateDescriptor.FORMAT_VERSION}" + private var repoUpdated: Boolean = false + + override val label: String = MCDevBundle("template.provider.builtin.label") + + override val hasConfig: Boolean = true + + override fun init(indicator: ProgressIndicator, repos: List) { + if (repoUpdated || repos.none { it.data.toBoolean() }) { + // Auto update is disabled + return + } + + if (doUpdateRepo(indicator, label, builtinRepoUrl)) { + repoUpdated = true + } + } + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val remoteTemplates = doLoadTemplates(context, repo, builtinTemplatesInnerPath) + if (remoteTemplates.isNotEmpty()) { + return remoteTemplates + } + + val repoRoot = builtinTemplatesPath.virtualFile + ?: return emptyList() + repoRoot.refreshSync(context.modalityState) + return TemplateProvider.findTemplates(context.modalityState, repoRoot) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("BuiltinTemplateProvider config") + val autoUpdateProperty = propertyGraph.property(data.toBooleanStrictOrNull() != false) + + return panel { + row { + checkBox(MCDevBundle("creator.ui.custom.remote.auto_update.label")) + .bindSelected(autoUpdateProperty) + } + + onApply { + dataSetter(autoUpdateProperty.get().toString()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt new file mode 100644 index 000000000..6f0cd2f45 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt @@ -0,0 +1,40 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor + +/** + * Placeholder template + */ +object EmptyLoadedTemplate : LoadedTemplate { + + override val label: String = "Empty template" + override val tooltip: String = "Empty template tooltip" + + override val descriptor: TemplateDescriptor + get() = throw UnsupportedOperationException("The empty template can't have a descriptor") + + override val isValid: Boolean = false + + override fun loadTemplateContents(path: String): String? = + throw UnsupportedOperationException("The empty template can't have contents") +} diff --git a/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt new file mode 100644 index 000000000..186d58f40 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt @@ -0,0 +1,33 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor + +interface LoadedTemplate { + + val label: String + val tooltip: String? + val descriptor: TemplateDescriptor + val isValid: Boolean + + fun loadTemplateContents(path: String): String? +} diff --git a/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt new file mode 100644 index 000000000..d08fb037c --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt @@ -0,0 +1,94 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.refreshSync +import com.demonwav.mcdev.util.virtualFile +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.absolute + +class LocalTemplateProvider : TemplateProvider { + + override val label: String = MCDevBundle("template.provider.local.label") + + override val hasConfig: Boolean = true + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val rootPath = Path.of(repo.data.trim()).absolute() + val repoRoot = rootPath.virtualFile + ?: return emptyList() + val modalityState = context.modalityState + repoRoot.refreshSync(modalityState) + return TemplateProvider.findTemplates(modalityState, repoRoot) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("LocalTemplateProvider config") + val pathProperty = propertyGraph.property(data) + return panel { + row(MCDevBundle("creator.ui.custom.path.label")) { + val pathChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor().apply { + description = MCDevBundle("creator.ui.custom.path.dialog.description") + } + textFieldWithBrowseButton( + MCDevBundle("creator.ui.custom.path.dialog.title"), + null, + pathChooserDescriptor + ).align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(pathProperty) + .textValidation( + validationErrorIf(MCDevBundle("creator.validation.custom.path_not_a_directory")) { value -> + val file = kotlin.runCatching { + VirtualFileManager.getInstance().findFileByNioPath(Path.of(value)) + }.getOrNull() + file == null || !file.isDirectory + } + ) + } + + onApply { + dataSetter(pathProperty.get()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt new file mode 100644 index 000000000..2cbc70f16 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt @@ -0,0 +1,228 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.creator.selectProxy +import com.demonwav.mcdev.update.PluginUtil +import com.demonwav.mcdev.util.refreshSync +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.result.getOrNull +import com.github.kittinunf.result.onError +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.trim +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import com.intellij.util.io.createDirectories +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.writeBytes + +open class RemoteTemplateProvider : TemplateProvider { + + private var updatedTemplates = mutableSetOf() + + override val label: String = MCDevBundle("template.provider.remote.label") + + override val hasConfig: Boolean = true + + override fun init(indicator: ProgressIndicator, repos: List) { + for (repo in repos) { + ProgressManager.checkCanceled() + val remote = RemoteTemplateRepo.deserialize(repo.data) + ?: continue + if (!remote.autoUpdate || remote.url in updatedTemplates) { + continue + } + + if (doUpdateRepo(indicator, repo.name, remote.url)) { + updatedTemplates.add(remote.url) + } + } + } + + protected fun doUpdateRepo( + indicator: ProgressIndicator, + repoName: String, + originalRepoUrl: String + ): Boolean { + indicator.text2 = "Updating remote repository $repoName" + + val repoUrl = replaceVariables(originalRepoUrl) + + val manager = FuelManager() + manager.proxy = selectProxy(repoUrl) + val (_, _, result) = manager.get(repoUrl) + .header("User-Agent", "github_org/minecraft-dev/${PluginUtil.pluginVersion}") + .header("Accepts", "application/json") + .timeout(10000) + .response() + + val data = result.onError { + thisLogger().warn("Could not fetch remote templates repository update at $repoUrl", it) + }.getOrNull() ?: return false + + try { + val zipPath = RemoteTemplateRepo.getDestinationZip(repoName) + zipPath.parent.createDirectories() + zipPath.writeBytes(data) + + thisLogger().info("Remote templates repository update applied successfully") + return true + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + thisLogger().error("Failed to apply remote templates repository update of $repoName", t) + } + return false + } + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val remoteRepo = RemoteTemplateRepo.deserialize(repo.data) + ?: return emptyList() + return doLoadTemplates(context, repo, remoteRepo.innerPath) + } + + protected fun doLoadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo, + rawInnerPath: String + ): List { + val remoteRootPath = RemoteTemplateRepo.getDestinationZip(repo.name) + if (!remoteRootPath.exists()) { + return emptyList() + } + + val archiveRoot = remoteRootPath.absolutePathString() + JarFileSystem.JAR_SEPARATOR + + val fs = JarFileSystem.getInstance() + val rootFile = fs.refreshAndFindFileByPath(archiveRoot) + ?: return emptyList() + val modalityState = context.modalityState + rootFile.refreshSync(modalityState) + + val innerPath = replaceVariables(rawInnerPath) + val repoRoot = if (innerPath.isNotBlank()) { + rootFile.findFileByRelativePath(innerPath) + } else { + rootFile + } + + if (repoRoot == null) { + return emptyList() + } + + return TemplateProvider.findTemplates(modalityState, repoRoot) + } + + private fun replaceVariables(originalRepoUrl: String): String = + originalRepoUrl.replace("\$version", TemplateDescriptor.FORMAT_VERSION.toString()) + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("RemoteTemplateProvider config") + val defaultRepo = RemoteTemplateRepo.deserialize(data) + val urlProperty = propertyGraph.property(defaultRepo?.url ?: "").trim() + val autoUpdateProperty = propertyGraph.property(defaultRepo?.autoUpdate != false) + val innerPathProperty = propertyGraph.property(defaultRepo?.innerPath ?: "").trim() + + return panel { + row(MCDevBundle("creator.ui.custom.remote.url.label")) { + textField() + .comment(MCDevBundle("creator.ui.custom.remote.url.comment")) + .align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(urlProperty) + .textValidation(BuiltinValidations.nonBlank) + } + + row(MCDevBundle("creator.ui.custom.remote.inner_path.label")) { + textField() + .comment(MCDevBundle("creator.ui.custom.remote.inner_path.comment")) + .align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(innerPathProperty) + } + + row { + checkBox(MCDevBundle("creator.ui.custom.remote.auto_update.label")) + .bindSelected(autoUpdateProperty) + } + + onApply { + val repo = RemoteTemplateRepo(urlProperty.get(), autoUpdateProperty.get(), innerPathProperty.get()) + dataSetter(repo.serialize()) + } + } + } + + data class RemoteTemplateRepo(val url: String, val autoUpdate: Boolean, val innerPath: String) { + + fun serialize(): String = "$url\n$autoUpdate\n$innerPath" + + companion object { + + val templatesBaseDir: Path + get() = PathManager.getSystemDir().resolve("mcdev-templates") + + fun getDestinationZip(repoName: String): Path { + return templatesBaseDir.resolve("$repoName.zip") + } + + fun deserialize(data: String): RemoteTemplateRepo? { + if (data.isBlank()) { + return null + } + + val lines = data.lines() + return RemoteTemplateRepo( + lines[0], + lines.getOrNull(1).toBoolean(), + lines.getOrNull(2) ?: "", + ) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/TemplateProvider.kt b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt new file mode 100644 index 000000000..30efddbb2 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt @@ -0,0 +1,226 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.custom.TemplateResourceBundle +import com.demonwav.mcdev.util.fromJson +import com.demonwav.mcdev.util.refreshSync +import com.google.gson.Gson +import com.intellij.DynamicBundle +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileVisitor +import com.intellij.openapi.vfs.isFile +import com.intellij.openapi.vfs.readText +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute +import java.util.ResourceBundle +import javax.swing.JComponent + +/** + * Extensions responsible for creating a [TemplateDescriptor] based on whatever data it is provided in its configuration + * [UI][setupConfigUi]. + */ +interface TemplateProvider { + + val label: String + + val hasConfig: Boolean + + fun init(indicator: ProgressIndicator, repos: List) = Unit + + fun loadTemplates(context: WizardContext, repo: MinecraftSettings.TemplateRepo): Collection + + fun setupConfigUi(data: String, dataSetter: (String) -> Unit): JComponent? + + companion object { + + private val EP_NAME = + ExtensionPointName("com.demonwav.minecraft-dev.creatorTemplateProvider") + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun get(key: String): TemplateProvider? = COLLECTOR.findSingle(key) + + fun getAllKeys() = EP_NAME.extensionList.mapNotNull { it.key } + + fun findTemplates( + modalityState: ModalityState, + repoRoot: VirtualFile, + templates: MutableList = mutableListOf(), + bundle: ResourceBundle? = loadMessagesBundle(modalityState, repoRoot) + ): List { + val visitor = object : VirtualFileVisitor() { + override fun visitFile(file: VirtualFile): Boolean { + if (!file.isFile || !file.name.endsWith(".mcdev.template.json")) { + return true + } + + try { + createVfsLoadedTemplate(modalityState, file.parent, file, bundle = bundle) + ?.let(templates::add) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + val attachment = runCatching { Attachment(file.name, file.readText()) }.getOrNull() + if (attachment != null) { + thisLogger().error("Failed to load template ${file.path}", t, attachment) + } else { + thisLogger().error("Failed to load template ${file.path}", t) + } + } + + return true + } + } + VfsUtilCore.visitChildrenRecursively(repoRoot, visitor) + return templates + } + + fun loadMessagesBundle(modalityState: ModalityState, repoRoot: VirtualFile): ResourceBundle? = try { + val locale = DynamicBundle.getLocale() + // Simplified bundle resolution, but covers all the most common cases + val baseBundle = doLoadMessageBundle( + repoRoot.findChild("messages.properties"), + modalityState, + null + ) + val languageBundle = doLoadMessageBundle( + repoRoot.findChild("messages_${locale.language}.properties"), + modalityState, + baseBundle + ) + doLoadMessageBundle( + repoRoot.findChild("messages_${locale.language}_${locale.country}.properties"), + modalityState, + languageBundle + ) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to load resource bundle of template repository ${repoRoot.path}", t) + null + } + + private fun doLoadMessageBundle( + file: VirtualFile?, + modalityState: ModalityState, + parent: ResourceBundle? + ): ResourceBundle? { + if (file == null) { + return parent + } + + try { + return file.refreshSync(modalityState) + ?.inputStream?.reader()?.use { TemplateResourceBundle(it, parent) } + } catch (t: Throwable) { + if (t is ControlFlowException) { + return parent + } + + thisLogger().error("Failed to load resource bundle ${file.path}", t) + } + + return parent + } + + fun createVfsLoadedTemplate( + modalityState: ModalityState, + templateRoot: VirtualFile, + descriptorFile: VirtualFile, + tooltip: String? = null, + bundle: ResourceBundle? = null + ): VfsLoadedTemplate? { + descriptorFile.refreshSync(modalityState) + var descriptor = Gson().fromJson(descriptorFile.readText()) + if (descriptor.version != TemplateDescriptor.FORMAT_VERSION) { + thisLogger().warn("Cannot handle template ${descriptorFile.path} of version ${descriptor.version}") + return null + } + + if (descriptor.hidden == true) { + return null + } + + descriptor.bundle = bundle + + val labelKey = descriptor.label + ?: descriptorFile.name.removeSuffix(".mcdev.template.json").takeIf(String::isNotBlank) + ?: templateRoot.presentableName + val label = + descriptor.translateOrNull("platform.${labelKey.lowercase()}.label") ?: descriptor.translate(labelKey) + + if (descriptor.inherit != null) { + val parent = templateRoot.findFileByRelativePath(descriptor.inherit!!) + if (parent != null) { + parent.refresh(false, false) + val parentDescriptor = Gson().fromJson(parent.readText()) + val mergedProperties = parentDescriptor.properties.orEmpty() + descriptor.properties.orEmpty() + val mergedFiles = parentDescriptor.files.orEmpty() + descriptor.files.orEmpty() + descriptor = descriptor.copy(properties = mergedProperties, files = mergedFiles) + } else { + thisLogger().error( + "Could not find inherited template descriptor ${descriptor.inherit} from ${descriptorFile.path}" + ) + } + } + + if (bundle != null) { + descriptor.properties?.forEach { property -> + property.bundle = bundle + } + } + + return VfsLoadedTemplate(templateRoot, label, tooltip, descriptor, true) + } + } +} + +class TemplateProviderBean : BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("key") + @RequiredElement + lateinit var name: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getKey(): String? = name + + override fun getImplementationClassName(): String? = implementation +} diff --git a/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt new file mode 100644 index 000000000..aa34b456c --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt @@ -0,0 +1,43 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.readText +import java.io.FileNotFoundException + +class VfsLoadedTemplate( + val templateRoot: VirtualFile, + override val label: String, + override val tooltip: String? = null, + override val descriptor: TemplateDescriptor, + override val isValid: Boolean, +) : LoadedTemplate { + + override fun loadTemplateContents(path: String): String? { + templateRoot.refresh(false, true) + val virtualFile = templateRoot.findFileByRelativePath(path) + ?: throw FileNotFoundException("Could not find file $path in template root ${templateRoot.path}") + virtualFile.refresh(false, false) + return virtualFile.readText() + } +} diff --git a/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt new file mode 100644 index 000000000..6cae4d3ce --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt @@ -0,0 +1,92 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.refreshSync +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.isRegularFile + +class ZipTemplateProvider : TemplateProvider { + + override val label: String = MCDevBundle("template.provider.zip.label") + + override val hasConfig: Boolean = true + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val archiveRoot = repo.data + JarFileSystem.JAR_SEPARATOR + val fs = JarFileSystem.getInstance() + val rootFile = fs.refreshAndFindFileByPath(archiveRoot) + ?: return emptyList() + val modalityState = context.modalityState + rootFile.refreshSync(modalityState) + return TemplateProvider.findTemplates(modalityState, rootFile) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent { + val propertyGraph = PropertyGraph("ZipTemplateProvider config") + val pathProperty = propertyGraph.property(data) + + return panel { + row(MCDevBundle("creator.ui.custom.path.label")) { + val pathChooserDescriptor = FileChooserDescriptorFactory.createSingleLocalFileDescriptor() + .withFileFilter { it.extension == "zip" } + .apply { description = MCDevBundle("creator.ui.custom.archive.dialog.description") } + textFieldWithBrowseButton( + MCDevBundle("creator.ui.custom.archive.dialog.title"), + null, + pathChooserDescriptor + ).align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(pathProperty) + .textValidation( + validationErrorIf(MCDevBundle("creator.validation.custom.path_not_a_file")) { value -> + runCatching { !Path.of(value).isRegularFile() }.getOrDefault(true) + } + ) + } + + onApply { + dataSetter(pathProperty.get()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt new file mode 100644 index 000000000..1c742e80e --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt @@ -0,0 +1,481 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.MCDevBundle.invoke +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.ArchitecturyVersionsModel +import com.demonwav.mcdev.platform.architectury.ArchitecturyVersion +import com.demonwav.mcdev.platform.fabric.util.FabricApiVersions +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.platform.forge.version.ForgeVersion +import com.demonwav.mcdev.platform.neoforge.version.NeoForgeVersion +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class ArchitecturyVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ArchitecturyVersionsModel::class.java) { + + private val emptyVersion = SemanticVersion.release() + private val emptyValue = ArchitecturyVersionsModel( + emptyVersion, + emptyVersion, + emptyVersion, + emptyVersion, + emptyVersion, + FabricVersions.YarnVersion("", -1), + true, + emptyVersion, + true, + true, + emptyVersion, + ) + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var model: ArchitecturyVersionsModel by graphProperty + + val mcVersionProperty = graphProperty.transform({ it.minecraft }, { model.copy(minecraft = it) }) + val mcVersionModel = DefaultComboBoxModel() + + val forgeVersionProperty = graphProperty.transform({ it.forge }, { model.copy(forge = it) }) + val forgeVersionsModel = DefaultComboBoxModel() + val isForgeAvailableProperty = forgeVersionProperty.transform { !it?.parts.isNullOrEmpty() } + + val nfVersionProperty = graphProperty.transform({ it.neoforge }, { model.copy(neoforge = it) }) + val nfVersionsModel = DefaultComboBoxModel() + val isNfAvailableProperty = nfVersionProperty.transform { !it?.parts.isNullOrEmpty() } + + val loomVersionProperty = graphProperty.transform({ it.loom }, { model.copy(loom = it) }) + val loomVersionModel = DefaultComboBoxModel() + + val loaderVersionProperty = graphProperty.transform({ it.loader }, { model.copy(loader = it) }) + val loaderVersionModel = DefaultComboBoxModel() + + val yarnVersionProperty = graphProperty.transform({ it.yarn }, { model.copy(yarn = it) }) + val yarnVersionModel = DefaultComboBoxModel() + val yarnHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val versions = fabricVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + versions.mappings.any { it.gameVersion == mcVersionString } + } + + val fabricApiVersionProperty = graphProperty.transform({ it.fabricApi }, { model.copy(fabricApi = it) }) + val fabricApiVersionModel = DefaultComboBoxModel() + val useFabricApiVersionProperty = graphProperty.transform({ it.useFabricApi }, { model.copy(useFabricApi = it) }) + val fabricApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = fabricApiVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + apiVersions.versions.any { mcVersionString in it.gameVersions } + } + + val useOfficialMappingsProperty = + graphProperty.transform({ it.useOfficialMappings }, { model.copy(useOfficialMappings = it) }) + + val architecturyApiVersionProperty = + graphProperty.transform({ it.architecturyApi }, { model.copy(architecturyApi = it) }) + val architecturyApiVersionModel = DefaultComboBoxModel() + val useArchitecturyApiVersionProperty = + graphProperty.transform({ it.useArchitecturyApi }, { model.copy(useArchitecturyApi = it) }) + val architecturyApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = architecturyVersions + ?: return@transform true + apiVersions.versions.containsKey(mcVersion) + } + + override fun createDefaultValue(raw: Any?): ArchitecturyVersionsModel = when (raw) { + is String -> deserialize(raw) + else -> emptyValue + } + + override fun serialize(value: ArchitecturyVersionsModel): String { + return "${value.minecraft} ${value.forge} ${value.neoforge} ${value.loom} ${value.loader} ${value.yarn}" + + " ${value.useFabricApi} ${value.fabricApi} ${value.useOfficialMappings} ${value.useArchitecturyApi}" + + " ${value.architecturyApi}" + } + + override fun deserialize(string: String): ArchitecturyVersionsModel { + val segments = string.split(' ') + val yarnSegments = segments.getOrNull(5)?.split(':') + val yarnVersion = if (yarnSegments != null && yarnSegments.size == 2) { + FabricVersions.YarnVersion(yarnSegments[0], yarnSegments[1].toInt()) + } else { + emptyValue.yarn + } + return ArchitecturyVersionsModel( + segments.getOrNull(0)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(3)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(4)?.let(SemanticVersion::tryParse) ?: emptyVersion, + yarnVersion, + segments.getOrNull(6)?.toBoolean() != false, + segments.getOrNull(7)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(8)?.toBoolean() != false, + segments.getOrNull(9)?.toBoolean() != false, + segments.getOrNull(10)?.let(SemanticVersion::tryParse) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("ArchitecturyVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row("Minecraft Version:") { + comboBox(mcVersionModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row("Forge Version:") { + comboBox(forgeVersionsModel) + .bindItem(forgeVersionProperty) + .enabledIf(isForgeAvailableProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + .component + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row("NeoForge Version:") { + comboBox(nfVersionsModel) + .bindItem(nfVersionProperty) + .enabledIf(isNfAvailableProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + .component + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + // + // panel.row("Loom Version:") { + // comboBox(loomVersionModel) + // .bindItem(loomVersionProperty) + // .validationOnInput(BuiltinValidations.nonEmptyVersion) + // .validationOnApply(BuiltinValidations.nonEmptyVersion) + // .also { ComboboxSpeedSearch.installOn(it.component) } + // }.enabled(descriptor.editable != false) + + panel.row("Loader Version:") { + comboBox(loaderVersionModel) + .bindItem(loaderVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + // Official mappings forced currently, yarn mappings are not handled yet + // panel.row("Yarn Version:") { + // comboBox(yarnVersionModel) + // .bindItem(yarnVersionProperty) + // .enabledIf(useOfficialMappingsProperty.not()) + // .validationOnInput(BuiltinValidations.nonEmptyYarnVersion) + // .validationOnApply(BuiltinValidations.nonEmptyYarnVersion) + // .also { ComboboxSpeedSearch.installOn(it.component) } + // + // checkBox("Use official mappings") + // .bindSelected(useOfficialMappingsProperty) + // + // label("Unable to match Yarn versions to Minecraft version") + // .visibleIf(yarnHasMatchingGameVersion.not()) + // .component.foreground = JBColor.YELLOW + // }.enabled(descriptor.editable != false) + + panel.row("Fabric API Version:") { + comboBox(fabricApiVersionModel) + .bindItem(fabricApiVersionProperty) + .enabledIf(useFabricApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox("Use Fabric API") + .bindSelected(useFabricApiVersionProperty) + label("Unable to match API versions to Minecraft version") + .visibleIf(fabricApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.visibleIf(!loadingVersionsProperty) + + panel.row("Architectury API Version:") { + comboBox(architecturyApiVersionModel) + .bindItem(architecturyApiVersionProperty) + .enabledIf(useArchitecturyApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox("Use Architectury API") + .bindSelected(useArchitecturyApiVersionProperty) + label("Unable to match API versions to Minecraft version") + .visibleIf(architecturyApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (previousMcVersion == mcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + + updateForgeVersions() + updateNeoForgeVersions() + updateYarnVersions() + updateFabricApiVersions() + updateArchitecturyApiVersions() + } + + downloadVersions { + val fabricVersions = fabricVersions + if (fabricVersions != null) { + loaderVersionModel.removeAllElements() + loaderVersionModel.addAll(fabricVersions.loader) + loaderVersionProperty.set(fabricVersions.loader.firstOrNull() ?: emptyVersion) + } + + val loomVersions = loomVersions + if (loomVersions != null) { + loomVersionModel.removeAllElements() + loomVersionModel.addAll(loomVersions) + val defaultValue = loomVersions.find { + it.parts.any { it is SemanticVersion.Companion.VersionPart.PreReleasePart } + } ?: loomVersions.firstOrNull() ?: emptyVersion + + loomVersionProperty.set(defaultValue) + } + + updateMcVersionsList() + + loadingVersionsProperty.set(false) + } + } + + private fun updateMcVersionsList() { + val architecturyVersions = architecturyVersions + ?: return + + val mcVersions = architecturyVersions.versions.keys.sortedDescending() + mcVersionModel.removeAllElements() + mcVersionModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + } + + private fun updateForgeVersions() { + val mcVersion = mcVersionProperty.get() + + val filterExpr = descriptor.parameters?.get("forgeMcVersionFilter") as? String + if (filterExpr != null) { + val conditionProps = mapOf("MC_VERSION" to mcVersion) + if (!TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true)) { + forgeVersionsModel.removeAllElements() + application.invokeLater { + // For some reason we have to set those properties later for the values to actually be set + // and the enabled state to be set appropriately + forgeVersionProperty.set(null) + } + return + } + } + + val availableForgeVersions = forgeVersions!!.getForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + forgeVersionsModel.removeAllElements() + forgeVersionsModel.addAll(availableForgeVersions) + application.invokeLater { + forgeVersionProperty.set(availableForgeVersions.firstOrNull()) + } + } + + private fun updateNeoForgeVersions() { + val mcVersion = mcVersionProperty.get() + + val filterExpr = descriptor.parameters?.get("neoForgeMcVersionFilter") as? String + if (filterExpr != null) { + val conditionProps = mapOf("MC_VERSION" to mcVersion) + if (!TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true)) { + nfVersionsModel.removeAllElements() + application.invokeLater { + nfVersionProperty.set(null) + } + return + } + } + + val availableNeoForgeVersions = neoForgeVersions!!.getNeoForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + nfVersionsModel.removeAllElements() + nfVersionsModel.addAll(availableNeoForgeVersions) + application.invokeLater { + nfVersionProperty.set(availableNeoForgeVersions.firstOrNull()) + } + } + + private fun updateYarnVersions() { + val fabricVersions = fabricVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val yarnVersions = if (yarnHasMatchingGameVersion.get()) { + fabricVersions.mappings.asSequence() + .filter { it.gameVersion == mcVersionString } + .map { it.version } + .toList() + } else { + fabricVersions.mappings.map { it.version } + } + yarnVersionModel.removeAllElements() + yarnVersionModel.addAll(yarnVersions) + yarnVersionProperty.set(yarnVersions.firstOrNull() ?: emptyValue.yarn) + } + + private fun updateFabricApiVersions() { + val fabricApiVersions = fabricApiVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val apiVersions = if (fabricApiHasMatchingGameVersion.get()) { + fabricApiVersions.versions.asSequence() + .filter { mcVersionString in it.gameVersions } + .map(FabricApiVersions.Version::version) + .toList() + } else { + fabricApiVersions.versions.map(FabricApiVersions.Version::version) + } + fabricApiVersionModel.removeAllElements() + fabricApiVersionModel.addAll(apiVersions) + fabricApiVersionProperty.set(apiVersions.firstOrNull() ?: emptyVersion) + } + + private fun updateArchitecturyApiVersions() { + val architecturyVersions = architecturyVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val availableArchitecturyApiVersions = architecturyVersions.getArchitecturyVersions(mcVersion) + architecturyApiVersionModel.removeAllElements() + architecturyApiVersionModel.addAll(availableArchitecturyApiVersions) + + architecturyApiVersionProperty.set(availableArchitecturyApiVersions.firstOrNull() ?: emptyVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var forgeVersions: ForgeVersion? = null + private var neoForgeVersions: NeoForgeVersion? = null + private var fabricVersions: FabricVersions? = null + private var loomVersions: List? = null + private var fabricApiVersions: FabricApiVersions? = null + private var architecturyVersions: ArchitecturyVersion? = null + + private fun downloadVersions(completeCallback: () -> Unit) { + if (hasDownloadedVersions) { + completeCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + awaitAll( + asyncIO { ForgeVersion.downloadData().also { forgeVersions = it } }, + asyncIO { NeoForgeVersion.downloadData().also { neoForgeVersions = it } }, + asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, + asyncIO { + collectMavenVersions( + "https://maven.architectury.dev/dev/architectury/architectury-loom/maven-metadata.xml" + ).also { + loomVersions = it + .mapNotNull(SemanticVersion::tryParse) + .sortedDescending() + } + }, + asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, + asyncIO { ArchitecturyVersion.downloadData().also { architecturyVersions = it } }, + ) + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + completeCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ArchitecturyVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt new file mode 100644 index 000000000..57072d519 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt @@ -0,0 +1,67 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.intellij.icons.AllIcons +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.content.AlertIcon +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindSelected + +class BooleanCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, Boolean::class.java) { + + override fun createDefaultValue(raw: Any?): Boolean = raw as? Boolean ?: false + + override fun serialize(value: Boolean): String = value.toString() + + override fun deserialize(string: String): Boolean = string.toBoolean() + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + val label = descriptor.translatedLabel + panel.row(label) { + val warning = descriptor.translatedWarning + if (warning != null) { + icon(AlertIcon(AllIcons.General.Warning)) + .gap(RightGap.SMALL) + .comment(descriptor.translate(warning)) + } + + this.checkBox(label.removeSuffix(":").trim()) + .bindSelected(graphProperty) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = BooleanCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt new file mode 100644 index 000000000..2d70ef5cc --- /dev/null +++ b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt @@ -0,0 +1,135 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.BuildSystemCoordinates +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.ui.validation.CHECK_ARTIFACT_ID +import com.intellij.openapi.ui.validation.CHECK_GROUP_ID +import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY +import com.intellij.openapi.ui.validation.WHEN_GRAPH_PROPAGATION_FINISHED +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +private val nonExampleValidation = validationErrorIf(MCDevBundle("creator.validation.group_id_non_example")) { + it == "org.example" +} + +class BuildSystemCoordinatesCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, BuildSystemCoordinates::class.java) { + + private val default = createDefaultValue(descriptor.default) + + override val graphProperty: GraphProperty = graph.property(default) + var coords: BuildSystemCoordinates by graphProperty + + private val groupIdProperty = graphProperty.transform({ it.groupId }, { coords.copy(groupId = it) }) + private val artifactIdProperty = graphProperty.transform({ it.artifactId }, { coords.copy(artifactId = it) }) + private val versionProperty = graphProperty.transform({ it.version }, { coords.copy(version = it) }) + + override fun createDefaultValue(raw: Any?): BuildSystemCoordinates { + val str = (raw as? String) ?: return createDefaultValue() + return deserialize(str) + } + + private fun createDefaultValue() = BuildSystemCoordinates("org.example", "", "1.0-SNAPSHOT") + + override fun serialize(value: BuildSystemCoordinates): String = + "${value.groupId}:${value.artifactId}:${value.version}" + + override fun deserialize(string: String): BuildSystemCoordinates { + val segments = string.split(':') + + val groupId = segments.getOrElse(0) { "" } + val artifactId = segments.getOrElse(1) { "" } + val version = segments.getOrElse(2) { "" } + return BuildSystemCoordinates(groupId, artifactId, version) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val projectNameProperty = properties["PROJECT_NAME"]?.graphProperty + if (projectNameProperty != null) { + val projectName = projectNameProperty.get() + if (projectName is String) { + coords = coords.copy(artifactId = projectName) + } + + graphProperty.dependsOn(projectNameProperty, false) { + val newProjectName = projectNameProperty.get() + if (newProjectName is String) { + coords.copy(artifactId = newProjectName) + } else { + coords + } + } + } + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.collapsibleGroup(MCDevBundle("creator.ui.group.title")) { + this.row(MCDevBundle("creator.ui.group.group_id")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.groupIdProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(CHECK_NON_EMPTY, CHECK_GROUP_ID, nonExampleValidation) + } + this.row(MCDevBundle("creator.ui.group.artifact_id")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.artifactIdProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(CHECK_NON_EMPTY, CHECK_ARTIFACT_ID) + } + this.row(MCDevBundle("creator.ui.group.version")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.versionProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(BuiltinValidations.validVersion) + } + }.expanded = true + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = BuildSystemCoordinatesCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt new file mode 100644 index 000000000..5ee2470ad --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt @@ -0,0 +1,78 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.SuggestClassNamePropertyDerivation +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +class ClassFqnCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, ClassFqn::class.java) { + + override fun createDefaultValue(raw: Any?): ClassFqn = ClassFqn(raw as? String ?: "") + + override fun serialize(value: ClassFqn): String = value.toString() + + override fun deserialize(string: String): ClassFqn = ClassFqn(string) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@ClassFqnCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .textValidation(BuiltinValidations.validClassFqn) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "suggestClassName" -> { + val parents = collectDerivationParents(reporter) + SuggestClassNamePropertyDerivation.create(reporter, parents, derives) + } + + else -> null + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ClassFqnCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/CreatorProperty.kt b/src/main/kotlin/creator/custom/types/CreatorProperty.kt new file mode 100644 index 000000000..3e9e1845c --- /dev/null +++ b/src/main/kotlin/creator/custom/types/CreatorProperty.kt @@ -0,0 +1,288 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.bindStorage +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row + +abstract class CreatorProperty( + val descriptor: TemplatePropertyDescriptor, + val graph: PropertyGraph, + protected val properties: Map>, + val valueType: Class +) { + private var derivation: PreparedDerivation? = null + private lateinit var visibleProperty: GraphProperty + + abstract val graphProperty: GraphProperty + + abstract fun createDefaultValue(raw: Any?): T + + abstract fun serialize(value: T): String + + abstract fun deserialize(string: String): T + + open fun toStringProperty(graphProperty: GraphProperty): ObservableMutableProperty = + graphProperty.transform(::serialize, ::deserialize) + + open fun get(): T? { + val value = graphProperty.get() + if (descriptor.nullIfDefault == true) { + val default = createDefaultValue(descriptor.default) + if (value == default) { + return null + } + } + + return value + } + + fun acceptsType(type: Class<*>): Boolean = type.isAssignableFrom(valueType) + + /** + * Produces a new value based on the provided [parentValues] and the template-defined [derivation] configuration. + * + * You must **NOT** [set][GraphProperty.set] the value of [graphProperty] in the process. You may however [get][GraphProperty.get] it at will. + * + * @param parentValues the values of the properties this [graphProperty] depends on + * @param derivation the configuration of the desired derivation + * + * @see GraphProperty.dependsOn + */ + open fun derive(parentValues: List?, derivation: PropertyDerivation): Any? { + if (this.derivation == null) { + throw IllegalStateException("This property has not been configured with a derivation") + } + + val result = this.derivation!!.derive(parentValues.orEmpty()) + if (this.derivation is SelectPropertyDerivation) { + return convertSelectDerivationResult(result) + } + + return result + } + + protected open fun convertSelectDerivationResult(original: Any?): Any? = original + + abstract fun buildUi(panel: Panel, context: WizardContext) + + /** + * Prepares everything this property needs, like calling [GraphProperty]'s [GraphProperty.afterChange] and + * [GraphProperty.dependsOn] on this property or other properties declared before this one. + * + * [properties] contains all the properties declared in the descriptor + * up to this one, forward references are not permitted. + * + * This is also where you should validate the [descriptor] values you want to use, and report all validation errors + * or warnings through the [reporter], use [TemplateValidationReporter.fatal] if the error is a show-stopper and + * the validation cannot even proceed further. + */ + open fun setupProperty(reporter: TemplateValidationReporter) { + if (descriptor.remember != false && descriptor.derives == null) { + val storageKey = when (val remember = descriptor.remember) { + null, true -> makeStorageKey() + is String -> makeCustomStorageKey(remember) + else -> { + reporter.error("Invalid 'remember' value. Must be a boolean or a string") + null + } + } + + if (storageKey != null) { + toStringProperty(graphProperty).bindStorage(storageKey) + } + } + + visibleProperty = setupVisibleProperty(reporter, descriptor.visible) + + if (descriptor.derives != null) { + val parents = descriptor.derives.parents + ?: return reporter.error("No parents specified in derivation") + for (parent in parents) { + if (!properties.containsKey(parent)) { + return reporter.error("Unknown parent property '$parent' in derivation") + } + } + + derivation = setupDerivation(reporter, descriptor.derives) + if (derivation == null) { + reporter.fatal("Unknown method derivation: ${descriptor.derives}") + } + + @Suppress("UNCHECKED_CAST") + graphProperty.set(derive(collectDerivationParentValues(reporter), descriptor.derives) as T) + for (parent in parents) { + val parentProperty = properties[parent]!! + graphProperty.dependsOn(parentProperty.graphProperty, descriptor.derives.whenModified != false) { + @Suppress("UNCHECKED_CAST") + derive(collectDerivationParentValues(), descriptor.derives) as T + } + } + } + + if (descriptor.inheritFrom != null) { + val parentProperty = properties[descriptor.inheritFrom] + ?: return reporter.error("Unknown parent property '${descriptor.inheritFrom}' in derivation") + + @Suppress("UNCHECKED_CAST") + graphProperty.set(parentProperty.graphProperty.get() as T) + graphProperty.dependsOn(parentProperty.graphProperty, true) { + @Suppress("UNCHECKED_CAST") + parentProperty.graphProperty.get() as T + } + } + } + + protected open fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = null + + protected fun makeStorageKey(discriminator: String? = null): String { + val base = "${javaClass.name}.property.${descriptor.name}.${descriptor.type}" + if (discriminator == null) { + return base + } + + return "$base.$discriminator" + } + + protected fun makeCustomStorageKey(key: String): String { + return "${javaClass.name}.property.$key" + } + + protected fun collectPropertiesValues(names: List? = null): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + return if (names == null) { + properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } else { + names.associateWithTo(mutableMapOf()) { properties[it]?.get() } + } + } + + protected fun collectDerivationParents(reporter: TemplateValidationReporter? = null): List?>? = + descriptor.derives?.parents?.map { parentName -> + val property = properties[parentName] + if (property == null) { + reporter?.error("Unknown parent property: $parentName") + } + return@map property + } + + protected fun collectDerivationParentValues(reporter: TemplateValidationReporter? = null): List? = + descriptor.derives?.parents?.map { parentName -> + val property = properties[parentName] + if (property == null) { + reporter?.error("Unknown parent property: $parentName") + } + return@map property?.get() + } + + protected fun Row.propertyVisibility(): Row = this.visibleIf(visibleProperty) + + private fun setupVisibleProperty( + reporter: TemplateValidationReporter, + visibility: Any? + ): GraphProperty { + val prop = graph.property(true) + if (visibility == null || visibility is Boolean) { + prop.set(visibility != false) + return prop + } + + if (visibility !is Map<*, *>) { + reporter.error("Visibility can only be a boolean or an object") + return prop + } + + var dependsOn = visibility["dependsOn"] + if (dependsOn !is String && (dependsOn !is List<*> || dependsOn.any { it !is String })) { + reporter.error( + "Expected 'visible' to have a 'dependsOn' value that is either a string or a list of strings" + ) + return prop + } + + val dependenciesNames = when (dependsOn) { + is String -> setOf(dependsOn) + is Collection<*> -> dependsOn.filterIsInstance().toSet() + else -> throw IllegalStateException("Should not be reached") + } + val dependencies = dependenciesNames.mapNotNull { + val dependency = this.properties[it] + if (dependency == null) { + reporter.error("Visibility dependency '$it' does not exist") + } + dependency + } + if (dependencies.size != dependenciesNames.size) { + // Errors have already been reported + return prop + } + + val condition = visibility["condition"] + if (condition !is String) { + reporter.error("Expected 'visible' to have a 'condition' string") + return prop + } + + var didInitialUpdate = false + val update: () -> Boolean = { + val conditionProperties = dependencies.associate { prop -> prop.descriptor.name to prop.get() } + val result = TemplateEvaluator.condition(conditionProperties, condition) + val exception = result.exceptionOrNull() + if (exception != null) { + if (!didInitialUpdate) { + didInitialUpdate = true + reporter.error("Failed to compute initial visibility: ${exception.message}") + thisLogger().info("Failed to compute initial visibility: ${exception.message}", exception) + } else { + thisLogger().error("Failed to compute initial visibility: ${exception.message}", exception) + } + } + + result.getOrDefault(true) + } + + prop.set(update()) + for (dependency in dependencies) { + prop.dependsOn(dependency.graphProperty, deleteWhenModified = false, update) + } + + return prop + } +} diff --git a/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt new file mode 100644 index 000000000..8d3689d50 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt @@ -0,0 +1,73 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute + +interface CreatorPropertyFactory { + + companion object { + + private val EP_NAME = ExtensionPointName>( + "com.demonwav.minecraft-dev.creatorPropertyType" + ) + + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun createFromType( + type: String, + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*>? { + return COLLECTOR.findSingle(type)?.create(descriptor, graph, properties) + } + } + + fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> +} + +class CreatorPropertyFactoryBean : + BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("type") + @RequiredElement + lateinit var type: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getImplementationClassName(): String = implementation + + override fun getKey(): String = type +} diff --git a/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt new file mode 100644 index 000000000..b51b0e58c --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt @@ -0,0 +1,50 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.Panel + +class ExternalCreatorProperty( + descriptor: TemplatePropertyDescriptor = TemplatePropertyDescriptor("", "", "", default = ""), + graph: PropertyGraph, + properties: Map>, + override val graphProperty: GraphProperty, + valueType: Class, +) : CreatorProperty(descriptor, graph, properties, valueType) { + + override fun setupProperty(reporter: TemplateValidationReporter) = Unit + + override fun createDefaultValue(raw: Any?): T = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun serialize(value: T): String = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun deserialize(string: String): T = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun buildUi(panel: Panel, context: WizardContext) = Unit +} diff --git a/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt new file mode 100644 index 000000000..870c470cb --- /dev/null +++ b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt @@ -0,0 +1,350 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.FabricVersionsModel +import com.demonwav.mcdev.platform.fabric.util.FabricApiVersions +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.bindBooleanStorage +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.ui.validation.WHEN_GRAPH_PROPAGATION_FINISHED +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class FabricVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, FabricVersionsModel::class.java) { + + private val emptyVersion = SemanticVersion.release() + private val emptyValue = FabricVersionsModel( + emptyVersion, + emptyVersion, + emptyVersion, + FabricVersions.YarnVersion("", -1), + true, + emptyVersion, + false, + ) + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var model: FabricVersionsModel by graphProperty + + val mcVersionProperty = graphProperty.transform({ it.minecraftVersion }, { model.copy(minecraftVersion = it) }) + val mcVersionModel = DefaultComboBoxModel() + val showMcSnapshotsProperty = graph.property(false) + .bindBooleanStorage(makeStorageKey("showMcSnapshots")) + + val loomVersionProperty = graphProperty.transform({ it.loom }, { model.copy(loom = it) }) + val loomVersionModel = DefaultComboBoxModel() + + val loaderVersionProperty = graphProperty.transform({ it.loader }, { model.copy(loader = it) }) + val loaderVersionModel = DefaultComboBoxModel() + + val yarnVersionProperty = graphProperty.transform({ it.yarn }, { model.copy(yarn = it) }) + val yarnVersionModel = DefaultComboBoxModel() + val yarnHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val versions = fabricVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + versions.mappings.any { it.gameVersion == mcVersionString } + } + + val fabricApiVersionProperty = graphProperty.transform({ it.fabricApi }, { model.copy(fabricApi = it) }) + val fabricApiVersionModel = DefaultComboBoxModel() + val useFabricApiVersionProperty = graphProperty.transform({ it.useFabricApi }, { model.copy(useFabricApi = it) }) + val fabricApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = fabricApiVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + apiVersions.versions.any { mcVersionString in it.gameVersions } + } + + val useOfficialMappingsProperty = + graphProperty.transform({ it.useOfficialMappings }, { model.copy(useOfficialMappings = it) }) + + override fun createDefaultValue(raw: Any?): FabricVersionsModel = when (raw) { + is String -> deserialize(raw) + else -> emptyValue + } + + override fun serialize(value: FabricVersionsModel): String { + return "${value.minecraftVersion} ${value.loom} ${value.loader} ${value.yarn}" + + " ${value.useFabricApi} ${value.fabricApi} ${value.useOfficialMappings}" + } + + override fun deserialize(string: String): FabricVersionsModel { + val segments = string.split(' ') + val yarnSegments = segments.getOrNull(3)?.split(':') + val yarnVersion = if (yarnSegments != null && yarnSegments.size == 2) { + FabricVersions.YarnVersion(yarnSegments[0], yarnSegments[1].toInt()) + } else { + emptyValue.yarn + } + return FabricVersionsModel( + segments.getOrNull(0)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + yarnVersion, + segments.getOrNull(4).toBoolean(), + segments.getOrNull(5)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(6).toBoolean(), + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("FabricVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionModel) + .bindItem(mcVersionProperty) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.show_snapshots.label")) + .bindSelected(showMcSnapshotsProperty) + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.loom_version.label")) { + comboBox(loomVersionModel) + .bindItem(loomVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.loader_version.label")) { + comboBox(loaderVersionModel) + .bindItem(loaderVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.yarn_version.label")) { + comboBox(yarnVersionModel) + .bindItem(yarnVersionProperty) + .enabledIf(useOfficialMappingsProperty.not()) + .validationOnInput(BuiltinValidations.nonEmptyYarnVersion) + .validationOnApply(BuiltinValidations.nonEmptyYarnVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.use_official_mappings.label")) + .bindSelected(useOfficialMappingsProperty) + + label(MCDevBundle("creator.ui.warn.no_yarn_to_mc_match")) + .visibleIf(yarnHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.fabricapi_version.label")) { + comboBox(fabricApiVersionModel) + .bindItem(fabricApiVersionProperty) + .enabledIf(useFabricApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.use_fabricapi.label")) + .bindSelected(useFabricApiVersionProperty) + label(MCDevBundle("creator.ui.warn.no_fabricapi_to_mc_match")) + .visibleIf(fabricApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + showMcSnapshotsProperty.afterChange { updateMcVersionsList() } + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (previousMcVersion == mcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + updateYarnVersions() + updateFabricApiVersions() + } + + downloadVersion { + val fabricVersions = fabricVersions + if (fabricVersions != null) { + loaderVersionModel.removeAllElements() + loaderVersionModel.addAll(fabricVersions.loader) + loaderVersionProperty.set(fabricVersions.loader.firstOrNull() ?: emptyVersion) + + updateMcVersionsList() + } + + val loomVersions = loomVersions + if (loomVersions != null) { + loomVersionModel.removeAllElements() + loomVersionModel.addAll(loomVersions) + val defaultValue = loomVersions.firstOrNull { it.toString().endsWith("-SNAPSHOT") } + ?: loomVersions.firstOrNull() + ?: emptyVersion + + loomVersionProperty.set(defaultValue) + } + + loadingVersionsProperty.set(false) + } + } + + private fun updateMcVersionsList() { + val versions = fabricVersions + ?: return + + val showSnapshots = showMcSnapshotsProperty.get() + val mcVersions = versions.game.asSequence() + .filter { showSnapshots || it.stable } + .mapNotNull { version -> SemanticVersion.tryParse(version.version) } + .toList() + + mcVersionModel.removeAllElements() + mcVersionModel.addAll(mcVersions) + mcVersionProperty.set(mcVersions.firstOrNull() ?: emptyVersion) + } + + private fun updateYarnVersions() { + val fabricVersions = fabricVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val yarnVersions = if (yarnHasMatchingGameVersion.get()) { + fabricVersions.mappings.asSequence() + .filter { it.gameVersion == mcVersionString } + .map { it.version } + .toList() + } else { + fabricVersions.mappings.map { it.version } + } + yarnVersionModel.removeAllElements() + yarnVersionModel.addAll(yarnVersions) + yarnVersionProperty.set(yarnVersions.firstOrNull() ?: emptyValue.yarn) + } + + private fun updateFabricApiVersions() { + val fabricApiVersions = fabricApiVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val apiVersions = if (fabricApiHasMatchingGameVersion.get()) { + fabricApiVersions.versions.asSequence() + .filter { mcVersionString in it.gameVersions } + .map(FabricApiVersions.Version::version) + .toList() + } else { + fabricApiVersions.versions.map(FabricApiVersions.Version::version) + } + fabricApiVersionModel.removeAllElements() + fabricApiVersionModel.addAll(apiVersions) + fabricApiVersionProperty.set(apiVersions.firstOrNull() ?: emptyVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var fabricVersions: FabricVersions? = null + private var loomVersions: List? = null + private var fabricApiVersions: FabricApiVersions? = null + + private fun downloadVersion(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + awaitAll( + asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, + asyncIO { + collectMavenVersions( + "https://maven.fabricmc.net/net/fabricmc/fabric-loom/maven-metadata.xml" + ).mapNotNull(SemanticVersion::tryParse) + .sortedDescending() + .also { loomVersions = it } + }, + asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, + ) + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = FabricVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt new file mode 100644 index 000000000..de3464fae --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt @@ -0,0 +1,219 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.ForgeVersions +import com.demonwav.mcdev.platform.forge.version.ForgeVersion +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlin.collections.Map +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class ForgeVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ForgeVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: ForgeVersions by graphProperty + + private var previousMcVersion: SemanticVersion? = null + + private val mcVersionProperty = graphProperty.transform({ it.minecraft }, { versions.copy(minecraft = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val forgeVersionProperty = graphProperty.transform({ it.forge }, { versions.copy(forge = it) }) + private val forgeVersionsModel = DefaultComboBoxModel() + + private var mcVersionFilterParents: List? = null + + override fun createDefaultValue(raw: Any?): ForgeVersions { + if (raw is String) { + return deserialize(raw) + } + + return ForgeVersions(emptyVersion, emptyVersion) + } + + override fun serialize(value: ForgeVersions): String { + return "${value.minecraft} ${value.forge}" + } + + override fun deserialize(string: String): ForgeVersions { + val versions = string.split(' ') + .take(2) + .map { SemanticVersion.tryParse(it) ?: emptyVersion } + + return ForgeVersions( + versions.getOrNull(0) ?: emptyVersion, + versions.getOrNull(1) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("ForgeVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + label(MCDevBundle("creator.ui.forge_version.label")).gap(RightGap.SMALL) + comboBox(forgeVersionsModel) + .bindItem(forgeVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + val availableForgeVersions = forgeVersion!!.getForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + forgeVersionsModel.removeAllElements() + forgeVersionsModel.addAll(availableForgeVersions) + forgeVersionProperty.set(availableForgeVersions.firstOrNull() ?: emptyVersion) + } + + descriptor.parameters?.get("mcVersionFilterParents")?.let { parents -> + if (parents !is List<*> || parents.any { it !is String }) { + reporter.error("mcVersionFilterParents must be a list of strings") + } else { + @Suppress("UNCHECKED_CAST") + this.mcVersionFilterParents = parents as List + for (parent in parents) { + val parentProp = properties[parent] + if (parentProp == null) { + reporter.error("Unknown mcVersionFilter parent $parent") + continue + } + + parentProp.graphProperty.afterChange { + reloadMinecraftVersions() + } + } + } + } + + downloadVersions { + reloadMinecraftVersions() + + loadingVersionsProperty.set(false) + } + } + + private fun reloadMinecraftVersions() { + val forgeVersions = forgeVersion + ?: return + + val filterExpr = descriptor.parameters?.get("mcVersionFilter") as? String + val mcVersions = if (filterExpr != null) { + val conditionProps = collectPropertiesValues(mcVersionFilterParents) + forgeVersions.sortedMcVersions.filter { version -> + conditionProps["MC_VERSION"] = version + TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true) + } + } else { + forgeVersions.sortedMcVersions + } + + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var forgeVersion: ForgeVersion? = null + + private fun downloadVersions(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + forgeVersion = ForgeVersion.downloadData() + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ForgeVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt new file mode 100644 index 000000000..67e931edf --- /dev/null +++ b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt @@ -0,0 +1,62 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.model.StringList +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns + +class InlineStringListCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, StringList::class.java) { + + override fun createDefaultValue(raw: Any?): StringList = deserialize(raw as? String ?: "") + + override fun serialize(value: StringList): String = value.values.joinToString(transform = String::trim) + + override fun deserialize(string: String): StringList = string.split(',') + .map(String::trim) + .filter(String::isNotBlank) + .run(::StringList) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@InlineStringListCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = InlineStringListCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt new file mode 100644 index 000000000..bcd6edc6b --- /dev/null +++ b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt @@ -0,0 +1,82 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.RecommendJavaVersionForMcVersionPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindIntText +import com.intellij.ui.dsl.builder.columns + +class IntegerCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, Int::class.java) { + + override fun createDefaultValue(raw: Any?): Int = (raw as? Number)?.toInt() ?: 0 + + override fun serialize(value: Int): String = value.toString() + + override fun deserialize(string: String): Int = string.toIntOrNull() ?: 0 + + override fun convertSelectDerivationResult(original: Any?): Any? = (original as? Number)?.toInt() + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.intTextField().bindIntText(graphProperty) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "recommendJavaVersionForMcVersion" -> { + val parents = collectDerivationParents(reporter) + RecommendJavaVersionForMcVersionPropertyDerivation.create(reporter, parents, derives) + } + + null -> { + // No need to collect parent values for this one because it is not used + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = IntegerCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt new file mode 100644 index 000000000..02608ed5f --- /dev/null +++ b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt @@ -0,0 +1,77 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.JdkComboBoxWithPreference +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.model.CreatorJdk +import com.demonwav.mcdev.creator.jdkComboBoxWithPreference +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.projectRoots.JavaSdkVersion +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.ui.dsl.builder.Panel + +class JdkCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, CreatorJdk::class.java) { + + private lateinit var jdkComboBox: JdkComboBoxWithPreference + + override fun createDefaultValue(raw: Any?): CreatorJdk = CreatorJdk(null) + + override fun serialize(value: CreatorJdk): String = value.sdk?.homePath ?: "" + + override fun deserialize(string: String): CreatorJdk = + CreatorJdk(ProjectJdkTable.getInstance().allJdks.find { it.homePath == string }) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val sdkProperty = graphProperty.transform(CreatorJdk::sdk, ::CreatorJdk) + jdkComboBox = this.jdkComboBoxWithPreference(context, sdkProperty, descriptor.name).component + + val minVersionPropName = descriptor.default as? String + if (minVersionPropName != null) { + val minVersionProperty = properties[minVersionPropName] + ?: throw RuntimeException( + "Could not find property $minVersionPropName referenced" + + " by default value of property ${descriptor.name}" + ) + + jdkComboBox.setPreferredJdk(JavaSdkVersion.entries[minVersionProperty.graphProperty.get() as Int]) + minVersionProperty.graphProperty.afterPropagation { + jdkComboBox.setPreferredJdk(JavaSdkVersion.entries[minVersionProperty.graphProperty.get() as Int]) + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = JdkCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt new file mode 100644 index 000000000..fc6ae05df --- /dev/null +++ b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt @@ -0,0 +1,74 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.model.LicenseData +import com.demonwav.mcdev.util.License +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import java.time.ZonedDateTime + +class LicenseCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, LicenseData::class.java) { + + override val graphProperty: GraphProperty = + graph.property(createDefaultValue(descriptor.default)) + + override fun createDefaultValue(raw: Any?): LicenseData = + deserialize(raw as? String ?: License.ALL_RIGHTS_RESERVED.id) + + override fun serialize(value: LicenseData): String = value.id + + override fun deserialize(string: String): LicenseData = + LicenseData(string, License.byId(string)?.toString() ?: string, ZonedDateTime.now().year.toString()) + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val model = EnumComboBoxModel(License::class.java) + val licenseEnumProperty = graphProperty.transform( + { License.byId(it.id) ?: License.entries.first() }, + { deserialize(it.id) } + ) + comboBox(model) + .bindItem(licenseEnumProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = LicenseCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt new file mode 100644 index 000000000..733af37ae --- /dev/null +++ b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt @@ -0,0 +1,177 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class MavenArtifactVersionCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SemanticVersionCreatorProperty(descriptor, graph, properties) { + + lateinit var sourceUrl: String + var rawVersionFilter: (String) -> Boolean = { true } + var versionFilter: (SemanticVersion) -> Boolean = { true } + + override val graphProperty: GraphProperty = graph.property(SemanticVersion(emptyList())) + private val versionsProperty = graph.property>(emptyList()) + private val loadingVersionsProperty = graph.property(true) + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val combobox = comboBox(versionsProperty.get()) + .bindItem(graphProperty) + .enabled(descriptor.editable != false) + .also { ComboboxSpeedSearch.installOn(it.component) } + + cell(AsyncProcessIcon(makeStorageKey("progress"))) + .visibleIf(loadingVersionsProperty) + + versionsProperty.afterChange { versions -> + combobox.component.removeAllItems() + for (version in versions) { + combobox.component.addItem(version) + } + } + }.propertyVisibility() + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val url = descriptor.parameters?.get("sourceUrl") as? String + if (url == null) { + reporter.error("Expected string parameter 'sourceUrl'") + return + } + + sourceUrl = url + + val rawVersionFilterCondition = descriptor.parameters?.get("rawVersionFilter") + if (rawVersionFilterCondition != null) { + if (rawVersionFilterCondition !is String) { + reporter.error("'rawVersionFilter' must be a string") + } else { + rawVersionFilter = { version -> + val props = mapOf("version" to version) + TemplateEvaluator.condition(props, rawVersionFilterCondition) + .getOrLogException(thisLogger()) == true + } + } + } + + val versionFilterCondition = descriptor.parameters?.get("versionFilter") + if (versionFilterCondition != null) { + if (versionFilterCondition !is String) { + reporter.error("'versionFilter' must be a string") + } else { + versionFilter = { version -> + val props = mapOf("version" to version) + TemplateEvaluator.condition(props, versionFilterCondition) + .getOrLogException(thisLogger()) == true + } + } + } + + downloadVersions( + // The key might be a bit too unique, but that'll do the job + descriptor.name + "@" + descriptor.hashCode(), + sourceUrl, + rawVersionFilter, + versionFilter, + descriptor.limit ?: 50 + ) { versions -> + versionsProperty.set(versions) + loadingVersionsProperty.set(false) + } + } + + companion object { + + private var versionsCache = ConcurrentHashMap>() + + private fun downloadVersions( + key: String, + url: String, + rawVersionFilter: (String) -> Boolean, + versionFilter: (SemanticVersion) -> Boolean, + limit: Int, + uiCallback: (List) -> Unit + ) { + // Let's not mix up cached versions if different properties + // point to the same URL, but have different filters or limits + val cacheKey = "$key-$url" + val cachedVersions = versionsCache[cacheKey] + if (cachedVersions != null) { + uiCallback(cachedVersions) + return + } + + application.executeOnPooledThread { + runBlocking { + val versions = collectMavenVersions(url) + .asSequence() + .filter(rawVersionFilter) + .mapNotNull(SemanticVersion::tryParse) + .filter(versionFilter) + .sortedDescending() + .take(limit) + .toList() + + versionsCache[cacheKey] = versions + + withContext(Dispatchers.Swing) { + uiCallback(versions) + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = MavenArtifactVersionCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt new file mode 100644 index 000000000..10925897d --- /dev/null +++ b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt @@ -0,0 +1,211 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.NeoForgeVersions +import com.demonwav.mcdev.platform.neoforge.version.NeoForgeVersion +import com.demonwav.mcdev.platform.neoforge.version.NeoGradleVersion +import com.demonwav.mcdev.platform.neoforge.version.platform.neoforge.version.NeoModDevVersion +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class NeoForgeVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, NeoForgeVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: NeoForgeVersions by graphProperty + + private var previousMcVersion: SemanticVersion? = null + + private val mcVersionProperty = graphProperty.transform({ it.minecraft }, { versions.copy(minecraft = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val nfVersionProperty = graphProperty.transform({ it.neoforge }, { versions.copy(neoforge = it) }) + private val nfVersionsModel = DefaultComboBoxModel() + private val ngVersionProperty = graphProperty.transform({ it.neogradle }, { versions.copy(neogradle = it) }) + private val mdVersionProperty = graphProperty.transform({ it.moddev }, { versions.copy(moddev = it) }) + + override fun createDefaultValue(raw: Any?): NeoForgeVersions { + if (raw is String) { + return deserialize(raw) + } + + return NeoForgeVersions(emptyVersion, emptyVersion, emptyVersion, emptyVersion) + } + + override fun serialize(value: NeoForgeVersions): String { + return "${value.minecraft} ${value.neoforge} ${value.neogradle} ${value.moddev}" + } + + override fun deserialize(string: String): NeoForgeVersions { + val versions = string.split(' ') + .take(4) + .map { SemanticVersion.tryParse(it) ?: emptyVersion } + + return NeoForgeVersions( + versions.getOrNull(0) ?: emptyVersion, + versions.getOrNull(1) ?: emptyVersion, + versions.getOrNull(2) ?: emptyVersion, + versions.getOrNull(3) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("NeoForgeVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + label(MCDevBundle("creator.ui.neoforge_version.label")).gap(RightGap.SMALL) + comboBox(nfVersionsModel) + .bindItem(nfVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + val availableNfVersions = nfVersion!!.getNeoForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + nfVersionsModel.removeAllElements() + nfVersionsModel.addAll(availableNfVersions) + nfVersionProperty.set(availableNfVersions.firstOrNull() ?: emptyVersion) + } + + val mcVersionFilter = descriptor.parameters?.get("mcVersionFilter") as? String + downloadVersion(mcVersionFilter) { + val mcVersions = mcVersions ?: return@downloadVersion + + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + + ngVersionProperty.set(ngVersion?.versions?.firstOrNull() ?: emptyVersion) + mdVersionProperty.set(mdVersion?.versions?.firstOrNull() ?: emptyVersion) + + loadingVersionsProperty.set(false) + } + } + + companion object { + + private var hasDownloadedVersions = false + + private var nfVersion: NeoForgeVersion? = null + private var ngVersion: NeoGradleVersion? = null + private var mdVersion: NeoModDevVersion? = null + private var mcVersions: List? = null + + private fun downloadVersion(mcVersionFilter: String?, uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + awaitAll( + asyncIO { NeoForgeVersion.downloadData().also { nfVersion = it } }, + asyncIO { NeoGradleVersion.downloadData().also { ngVersion = it } }, + asyncIO { NeoModDevVersion.downloadData().also { mdVersion = it } }, + ) + + mcVersions = nfVersion?.sortedMcVersions?.let { mcVersion -> + if (mcVersionFilter != null) { + mcVersion.filter { version -> + val conditionProps = mapOf("MC_VERSION" to version) + TemplateEvaluator.condition(conditionProps, mcVersionFilter).getOrDefault(true) + } + } else { + mcVersion + } + } + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = NeoForgeVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt new file mode 100644 index 000000000..360c3d2f9 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt @@ -0,0 +1,281 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.ParchmentVersion +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.HasMinecraftVersion +import com.demonwav.mcdev.creator.custom.model.ParchmentVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.util.application +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class ParchmentCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ParchmentVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: ParchmentVersions by graphProperty + + private var availableParchmentVersions: List = emptyList() + + private val useParchmentProperty = graphProperty.transform({ it.use }, { versions.copy(use = it) }) + private val versionProperty = graphProperty.transform({ it.version }, { versions.copy(version = it) }) + private val versionsModel = DefaultComboBoxModel() + private val mcVersionProperty = + graphProperty.transform({ it.minecraftVersion }, { versions.copy(minecraftVersion = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val includeOlderMcVersionsProperty = + graphProperty.transform({ it.includeOlderMcVersions }, { versions.copy(includeOlderMcVersions = it) }) + private val includeSnapshotsProperty = + graphProperty.transform({ it.includeSnapshots }, { versions.copy(includeSnapshots = it) }) + + override fun createDefaultValue(raw: Any?): ParchmentVersions { + if (raw is String) { + return deserialize(raw) + } + + return ParchmentVersions(true, emptyVersion, emptyVersion, false, false) + } + + override fun serialize(value: ParchmentVersions): String { + return "${value.use} ${value.version} ${value.minecraftVersion}" + + " ${value.includeOlderMcVersions} ${value.includeSnapshots}" + } + + override fun deserialize(string: String): ParchmentVersions { + val segments = string.split(' ') + return ParchmentVersions( + segments.getOrNull(0)?.toBoolean() ?: true, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(3).toBoolean(), + segments.getOrNull(4).toBoolean(), + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + checkBox("Use Parchment") + .bindSelected(useParchmentProperty) + + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .enabledIf(useParchmentProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + comboBox(versionsModel) + .bindItem(versionProperty) + .enabledIf(useParchmentProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + + panel.row("Include") { + checkBox("Older Minecraft versions") + .bindSelected(includeOlderMcVersionsProperty) + .enabledIf(useParchmentProperty) + + checkBox("Snapshots") + .bindSelected(includeSnapshotsProperty) + .enabledIf(useParchmentProperty) + }.enabled(descriptor.editable != false) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val platformMcVersionPropertyName = descriptor.parameters?.get("minecraftVersionProperty") as? String + val platformMcVersionProperty = properties[platformMcVersionPropertyName] + if (platformMcVersionProperty != null) { + graphProperty.dependsOn(platformMcVersionProperty.graphProperty, true) { + val minecraftVersion = getPlatformMinecraftVersion() + if (minecraftVersion != null) { + graphProperty.get().copy(minecraftVersion = minecraftVersion) + } else { + graphProperty.get() + } + } + } + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + refreshVersionsLists(updateMcVersions = false) + } + + var previousOlderMcVersions: Boolean? = null + includeOlderMcVersionsProperty.afterChange { newValue -> + if (previousOlderMcVersions == newValue) { + return@afterChange + } + + previousOlderMcVersions = newValue + refreshVersionsLists() + } + + var previousIncludeSnapshots: Boolean? = null + includeSnapshotsProperty.afterChange { newValue -> + if (previousIncludeSnapshots == newValue) { + return@afterChange + } + + previousIncludeSnapshots = newValue + refreshVersionsLists() + } + + downloadVersions { + refreshVersionsLists() + + val minecraftVersion = getPlatformMinecraftVersion() + if (minecraftVersion != null) { + mcVersionProperty.set(minecraftVersion) + } + } + } + + private fun refreshVersionsLists(updateMcVersions: Boolean = true) { + val includeOlderMcVersions = includeOlderMcVersionsProperty.get() + val includeSnapshots = includeSnapshotsProperty.get() + + if (updateMcVersions) { + val platformMcVersion = getPlatformMinecraftVersion() + availableParchmentVersions = allParchmentVersions + ?.filter { version -> + if (!includeOlderMcVersions && platformMcVersion != null && version.mcVersion < platformMcVersion) { + return@filter false + } + + if (!includeSnapshots && version.parchmentVersion.contains("-SNAPSHOT")) { + return@filter false + } + + return@filter true + } + ?: return + + val mcVersions = availableParchmentVersions.mapTo(mutableSetOf(), ParchmentVersion::mcVersion) + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraftVersion in mcVersions -> defaultValue.minecraftVersion + else -> getPlatformMinecraftVersion() ?: mcVersions.first() + } + + if (mcVersionProperty.get() != selectedMcVersion) { + mcVersionProperty.set(selectedMcVersion) + } + } + + val selectedMcVersion = mcVersionProperty.get() + val parchmentVersions = availableParchmentVersions.asSequence() + .filter { it.mcVersion == selectedMcVersion } + .mapNotNull { SemanticVersion.tryParse(it.parchmentVersion) } + .sortedDescending() + .toList() + versionsModel.removeAllElements() + versionsModel.addAll(parchmentVersions) + versionProperty.set(parchmentVersions.firstOrNull() ?: emptyVersion) + } + + private fun getPlatformMinecraftVersion(): SemanticVersion? { + val platformMcVersionPropertyName = descriptor.parameters?.get("minecraftVersionProperty") as? String + val platformMcVersionProperty = properties[platformMcVersionPropertyName] + + val version = when (val version = platformMcVersionProperty?.get()) { + is SemanticVersion -> version + is HasMinecraftVersion -> version.minecraftVersion + else -> return null + } + + // Ensures we get no trailing .0 for the first major version (1.21.0 -> 1.21) + // This is required because otherwise those versions won't be properly compared against Parchment's + val normalizedVersion = version.parts.dropLastWhile { part -> + part is SemanticVersion.Companion.VersionPart.ReleasePart && part.version == 0 + } + + return SemanticVersion(normalizedVersion) + } + + companion object { + + private var hasDownloadedVersions = false + + private var allParchmentVersions: List? = null + + private fun downloadVersions(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + allParchmentVersions = ParchmentVersion.downloadData() + .sortedByDescending(ParchmentVersion::parchmentVersion) + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ParchmentCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt new file mode 100644 index 000000000..f500d03d0 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt @@ -0,0 +1,86 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.ExtractVersionMajorMinorPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_SHORT +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns + +open class SemanticVersionCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, SemanticVersion::class.java) { + + override fun createDefaultValue(raw: Any?): SemanticVersion = + SemanticVersion.tryParse(raw as? String ?: "") ?: SemanticVersion(emptyList()) + + override fun serialize(value: SemanticVersion): String = value.toString() + + override fun deserialize(string: String): SemanticVersion = + SemanticVersion.tryParse(string) ?: SemanticVersion(emptyList()) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@SemanticVersionCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_SHORT) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "extractVersionMajorMinor" -> { + val parents = collectDerivationParents(reporter) + ExtractVersionMajorMinorPropertyDerivation.create(reporter, parents, derives) + } + + null -> { + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + override fun convertSelectDerivationResult(original: Any?): Any? { + return (original as? String)?.let(SemanticVersion::tryParse) + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = SemanticVersionCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt new file mode 100644 index 000000000..7a735d7fb --- /dev/null +++ b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt @@ -0,0 +1,134 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import java.awt.Component +import javax.swing.DefaultListCellRenderer +import javax.swing.JList + +abstract class SimpleCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map>, + valueType: Class +) : CreatorProperty(descriptor, graph, properties, valueType) { + + private val options: Map? = makeOptionsList() + + private fun makeOptionsList(): Map? { + val map = when (val options = descriptor.options) { + is Map<*, *> -> options.mapValues { descriptor.translate(it.value.toString()) } + is Iterable<*> -> options.associateWithTo(linkedMapOf()) { + val optionKey = it.toString() + descriptor.translateOrNull("creator.ui.${descriptor.name.lowercase()}.option.${optionKey.lowercase()}") + ?: optionKey + } + + else -> null + } + + return map?.mapKeys { + @Suppress("UNCHECKED_CAST") + when (val key = it.key) { + is String -> deserialize(key) + else -> key + } as T + } + } + + private val isDropdown = !options.isNullOrEmpty() + private val defaultValue by lazy { + val raw = if (isDropdown) { + if (descriptor.default is Number && descriptor.options is List<*>) { + descriptor.options[descriptor.default.toInt()] + } else { + descriptor.default ?: options?.keys?.firstOrNull() + } + } else { + descriptor.default + } + + createDefaultValue(raw) + } + + override val graphProperty: GraphProperty by lazy { graph.property(defaultValue) } + + override fun buildUi(panel: Panel, context: WizardContext) { + if (isDropdown) { + if (graphProperty.get() !in options!!.keys) { + graphProperty.set(defaultValue) + } + + panel.row(descriptor.translatedLabel) { + if (descriptor.forceDropdown == true) { + comboBox(options.keys, DropdownAutoRenderer()) + .bindItem(graphProperty) + .enabled(descriptor.editable != false) + .also { + val component = it.component + ComboboxSpeedSearch.installOn(component) + val validation = + BuiltinValidations.isAnyOf(component::getSelectedItem, options.keys, component) + it.validationOnInput(validation) + it.validationOnApply(validation) + } + } else { + segmentedButton(options.keys) { options[it] ?: it.toString() } + .bind(graphProperty) + .enabled(descriptor.editable != false) + .maxButtonsCount(4) + .validation { + val message = MCDevBundle("creator.validation.invalid_option") + addInputRule(message) { it.selectedItem !in options.keys } + addApplyRule(message) { it.selectedItem !in options.keys } + } + } + }.propertyVisibility() + } else { + buildSimpleUi(panel, context) + } + } + + abstract fun buildSimpleUi(panel: Panel, context: WizardContext) + + private inner class DropdownAutoRenderer : DefaultListCellRenderer() { + + override fun getListCellRendererComponent( + list: JList?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + val label = options!![value] ?: value.toString() + return super.getListCellRendererComponent(list, label, index, isSelected, cellHasFocus) + } + } +} diff --git a/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt new file mode 100644 index 000000000..31582bcc7 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt @@ -0,0 +1,103 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.types + +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.ReplacePropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +class StringCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, String::class.java) { + + private var validationRegex: Regex? = null + + override fun createDefaultValue(raw: Any?): String = raw as? String ?: "" + + override fun serialize(value: String): String = value + + override fun deserialize(string: String): String = string + + override fun toStringProperty(graphProperty: GraphProperty) = graphProperty + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val regexString = descriptor.validator as? String + if (regexString != null) { + try { + validationRegex = regexString.toRegex() + } catch (t: Throwable) { + reporter.error("Invalid validator regex: '$regexString': ${t.message}") + } + } + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "replace" -> { + val parents = collectDerivationParents(reporter) + ReplacePropertyDerivation.create(reporter, parents, derives) + } + + null -> { + // No need to collect parent values for this one because it is not used + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val textField = textField().bindText(this@StringCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + if (validationRegex != null) { + textField.textValidation(BuiltinValidations.byRegex(validationRegex!!)) + } + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = StringCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/step/UseMixinsStep.kt b/src/main/kotlin/creator/step/UseMixinsStep.kt index e5b9b1c1b..c328712b5 100644 --- a/src/main/kotlin/creator/step/UseMixinsStep.kt +++ b/src/main/kotlin/creator/step/UseMixinsStep.kt @@ -36,7 +36,7 @@ class UseMixinsStep(parent: NewProjectWizardStep) : AbstractNewProjectWizardStep override fun setupUI(builder: Panel) { with(builder) { - row(MCDevBundle("creator.ui.mixins.label")) { + row(MCDevBundle("creator.ui.use_mixins.label")) { checkBox("") .bindSelected(useMixinsProperty) } diff --git a/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt b/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt index dae07b3d7..e5df5ce13 100644 --- a/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt +++ b/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt @@ -29,21 +29,13 @@ import com.github.kittinunf.fuel.core.requests.suspendable import com.github.kittinunf.fuel.coroutines.awaitString import com.google.gson.Gson import com.google.gson.annotations.SerializedName -import java.io.IOException class ArchitecturyVersion private constructor( val versions: Map>, ) { fun getArchitecturyVersions(mcVersion: SemanticVersion): List { - return try { - val architecturyVersions = versions[mcVersion] - ?: throw IOException("Could not find any architectury versions for $mcVersion") - architecturyVersions.take(50) - } catch (e: IOException) { - e.printStackTrace() - emptyList() - } + return versions[mcVersion].orEmpty().take(50) } data class ModrinthVersionApi( diff --git a/src/main/kotlin/platform/fabric/util/FabricVersions.kt b/src/main/kotlin/platform/fabric/util/FabricVersions.kt index 9e1a03167..7b898ad78 100644 --- a/src/main/kotlin/platform/fabric/util/FabricVersions.kt +++ b/src/main/kotlin/platform/fabric/util/FabricVersions.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.fabric.util +import com.demonwav.mcdev.creator.custom.model.TemplateApi import com.demonwav.mcdev.creator.selectProxy import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.SemanticVersion @@ -36,6 +37,7 @@ class FabricVersions(val game: List, val mappings: List, val loa class Game(val version: String, val stable: Boolean) class Mappings(val gameVersion: String, val version: YarnVersion) + @TemplateApi class YarnVersion(val name: String, val build: Int) : Comparable { override fun toString() = name override fun compareTo(other: YarnVersion) = build.compareTo(other.build) diff --git a/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt b/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt new file mode 100644 index 000000000..c6d8ac4d0 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt @@ -0,0 +1,50 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.version.platform.neoforge.version + +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.openapi.diagnostic.logger +import java.io.IOException + +class NeoModDevVersion private constructor(val versions: List) { + + companion object { + private val LOGGER = logger() + + suspend fun downloadData(): NeoModDevVersion? { + try { + val url = "https://maven.neoforged.net/releases/net/neoforged/moddev" + + "/net.neoforged.moddev.gradle.plugin/maven-metadata.xml" + val versions = collectMavenVersions(url) + .asSequence() + .mapNotNull(SemanticVersion.Companion::tryParse) + .sortedDescending() + .take(50) + .toList() + return NeoModDevVersion(versions) + } catch (e: IOException) { + LOGGER.error("Failed to retrieve NeoForged ModDev version data", e) + } + return null + } + } +} diff --git a/src/main/kotlin/util/MinecraftVersions.kt b/src/main/kotlin/util/MinecraftVersions.kt index 9d19a575c..7333c5aa3 100644 --- a/src/main/kotlin/util/MinecraftVersions.kt +++ b/src/main/kotlin/util/MinecraftVersions.kt @@ -25,6 +25,7 @@ import com.intellij.openapi.projectRoots.JavaSdkVersion object MinecraftVersions { val MC1_12_2 = SemanticVersion.release(1, 12, 2) val MC1_14_4 = SemanticVersion.release(1, 14, 4) + val MC1_16 = SemanticVersion.release(1, 16) val MC1_16_1 = SemanticVersion.release(1, 16, 1) val MC1_16_5 = SemanticVersion.release(1, 16, 5) val MC1_17 = SemanticVersion.release(1, 17) @@ -35,6 +36,7 @@ object MinecraftVersions { val MC1_19_3 = SemanticVersion.release(1, 19, 3) val MC1_19_4 = SemanticVersion.release(1, 19, 4) val MC1_20 = SemanticVersion.release(1, 20) + val MC1_20_1 = SemanticVersion.release(1, 20, 1) val MC1_20_2 = SemanticVersion.release(1, 20, 2) val MC1_20_3 = SemanticVersion.release(1, 20, 3) val MC1_20_4 = SemanticVersion.release(1, 20, 4) diff --git a/src/main/kotlin/util/files.kt b/src/main/kotlin/util/files.kt index a1847d967..a00fd6c21 100644 --- a/src/main/kotlin/util/files.kt +++ b/src/main/kotlin/util/files.kt @@ -20,9 +20,11 @@ package com.demonwav.mcdev.util +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.RefreshQueue import java.io.File import java.io.IOException import java.nio.file.Path @@ -57,6 +59,7 @@ val VirtualFile.mcPath: String? operator fun Manifest.get(attribute: String): String? = mainAttributes.getValue(attribute) operator fun Manifest.get(attribute: Attributes.Name): String? = mainAttributes.getValue(attribute) -fun VirtualFile.refreshFs(): VirtualFile { - return this.parent.findOrCreateChildData(this, this.name) +fun VirtualFile.refreshSync(modalityState: ModalityState): VirtualFile? { + RefreshQueue.getInstance().refresh(false, this.isDirectory, null, modalityState, this) + return this.parent?.findOrCreateChildData(this, this.name) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 5be203414..8869a6aec 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -74,6 +74,16 @@ + + + + + + + + + + @@ -123,6 +133,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -182,6 +218,7 @@ + @@ -1232,5 +1269,8 @@ description="Copy the reference to clipboard in Access Widener format"> + diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 7ab481b10..2ee0ef24d 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -18,7 +18,7 @@ # along with this program. If not, see . # -creator.ui.build_system.label.generic=Build System: +creator.ui.build_system.label=Build System: creator.ui.build_system.label.gradle=Gradle creator.ui.build_system.label.maven=Maven @@ -31,12 +31,38 @@ creator.ui.platform.type.label=Platform Type: creator.ui.platform.label=Platform: creator.ui.platform.mod.name=Mod creator.ui.platform.plugin.name=Plugin +creator.ui.group.default.label=Default +creator.ui.group.mod.label=Mod +creator.ui.group.plugin.label=Plugin +creator.ui.group.proxy.label=Proxy + +creator.ui.custom.step.description=Creating project based on template... +creator.ui.custom.repos.label=Repositories: +creator.ui.custom.groups.label=Groups: +creator.ui.custom.templates.label=Templates: +creator.ui.custom.path.label=Templates Path: +creator.ui.custom.path.dialog.title=Template Root +creator.ui.custom.path.dialog.description=Select the root directory of the template repository +creator.ui.custom.archive.dialog.title=Template Archive +creator.ui.custom.archive.dialog.description=Select the ZIP file containing the template +creator.ui.custom.remote.url.label=Download URL: +creator.ui.custom.remote.url.comment='$version' will be replaced by the template descriptor version currently in use +creator.ui.custom.remote.inner_path.label=Inner Path: +creator.ui.custom.remote.inner_path.comment='$version' will be replaced by the template descriptor version currently in use +creator.ui.custom.remote.auto_update.label=Auto update + +creator.ui.warn.no_properties=This template has no properties +creator.ui.error.template_warns_and_errors=This template contains warnings and errors: +creator.ui.error.template_warns=This template contains warnings: +creator.ui.error.template_errors=This template contains errors: creator.ui.license.label=License: creator.ui.main_class.label=Main Class: -creator.ui.mc_version.label=Minecraft Version: -creator.ui.mod_name.label=Mod Name: -creator.ui.plugin_name.label=Plugin Name: +creator.ui.mc_version.label=Minecraft &Version: +creator.ui.mod_name.label=Mod &Name: +creator.ui.mod_id.label=Mod &ID: +creator.ui.plugin_name.label=Plugin &Name: +creator.ui.plugin_id.label=Plugin &ID: creator.ui.description.label=Description: creator.ui.authors.label=Authors: creator.ui.website.label=Website: @@ -44,13 +70,40 @@ creator.ui.repository.label=Repository: creator.ui.issue_tracker.label=Issue Tracker: creator.ui.update_url.label=Update URL: creator.ui.depend.label=Depend: +creator.ui.log_prefix.label=Log Prefix: +creator.ui.load_at.label=Load At: +creator.ui.load_at.option.startup=Startup: +creator.ui.load_at.option.postworld=Post World: creator.ui.soft_depend.label=Soft Depend: -creator.ui.mixins.label=Use Mixins: +creator.ui.use_mixins.label=Use &Mixins: +creator.ui.split_sources.label=Split Sources: +creator.ui.java_version.label=Java Version: +creator.ui.jdk.label=JDK: +creator.ui.optional_settings.label=Optional Settings creator.ui.parchment.label=Parchment: creator.ui.parchment.include.label=Include: creator.ui.parchment.include.old_mc.label=Older Minecraft versions creator.ui.parchment.include.snapshots.label=Snapshot versions creator.ui.parchment.no_version.message=No versions of Parchment matching your configuration +creator.ui.mod_environment.label=Environment: +creator.ui.mod_environment.option.*=Both +creator.ui.mod_environment.option.client=Client +creator.ui.mod_environment.option.server=Server +creator.ui.forge_version.label=Forge: +creator.ui.neoforge_version.label=NeoForge: +creator.ui.show_snapshots.label=Show snapshots: +creator.ui.loom_version.label=Loom Version: +creator.ui.loader_version.label=Loader Version: +creator.ui.yarn_version.label=Yarn Version: +creator.ui.use_official_mappings.label=Use official mappings +creator.ui.fabricapi_version.label=Fabric API Version: +creator.ui.use_fabricapi.label=Use Fabric API +creator.ui.spongeapi_version.label=Sponge Version: +creator.ui.velocity_version.label=Velocity Version: +creator.ui.versions_download.label=Downloading versions... + +creator.ui.warn.no_yarn_to_mc_match=Unable to match Yarn versions to Minecraft version +creator.ui.warn.no_fabricapi_to_mc_match=Unable to match API versions to Minecraft version creator.ui.outdated.message=Is the Minecraft project wizard outdated? \ Create an issue on the MinecraftDev issue tracker. @@ -61,6 +114,9 @@ creator.ui.generic_unfinished.message=Haven''t finished {0} creator.ui.create_minecraft_project=Create a new Minecraft project creator.step.generic.project_created.message=Your project is being created +creator.step.generic.init_template_providers.message=Initializing templates +creator.step.generic.load_template.message=Loading templates +creator.step.generic.no_templates_available.message=There are no templates available creator.step.gradle.patch_gradle.description=Patching Gradle files creator.step.gradle.import_gradle.description=Importing Gradle project @@ -72,8 +128,15 @@ creator.step.maven.import_maven.description=Importing Maven project creator.step.reformat.description=Reformatting files +creator.validation.custom.path_not_a_directory=Path is not a directory +creator.validation.custom.path_not_a_file=Path is not a file + +creator.validation.blank=Must not be blank creator.validation.group_id_non_example=Group ID must be changed from "org.example" creator.validation.semantic_version=Version must be a valid semantic version +creator.validation.class_fqn=Must be a valid class fully qualified name +creator.validation.regex=Must match regex {0} +creator.validation.invalid_option=Selection is not a valid option creator.validation.jdk_preferred=Java {0} is recommended for {1} creator.validation.jdk_preferred_default_reason=these settings @@ -207,6 +270,14 @@ minecraft.settings.show_chat_color_underlines=Show chat color underlines minecraft.settings.chat_color_underline_style=Chat color underline style: minecraft.settings.mixin=Mixin minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line +minecraft.settings.creator=Creator +minecraft.settings.creator.repos=Template Repositories: +minecraft.settings.creator.repos.column.name=Name +minecraft.settings.creator.repos.column.provider=Provider +minecraft.settings.creator.repo_config.title={0} Template Repo Configuration +minecraft.settings.creator.repo.default_name=My Repo +minecraft.settings.creator.repo.builtin_name=Built In + minecraft.settings.lang_template.display_name=Localization Template minecraft.settings.lang_template.scheme=Scheme: minecraft.settings.lang_template.project_must_be_selected=You must have selected a project for this! @@ -216,3 +287,8 @@ minecraft.settings.lang_template.comment=You may edit the template used fo minecraft.settings.translation=Translation minecraft.settings.translation.force_json_translation_file=Force JSON translation file (1.13+) minecraft.settings.translation.use_custom_convert_template=Use custom template for convert literal to translation + +template.provider.builtin.label=Built In +template.provider.remote.label=Remote +template.provider.local.label=Local +template.provider.zip.label=Archive diff --git a/src/main/resources/messages/MinecraftDevelopment_zh.properties b/src/main/resources/messages/MinecraftDevelopment_zh.properties index 5cac19f5d..c1f1e223e 100644 --- a/src/main/resources/messages/MinecraftDevelopment_zh.properties +++ b/src/main/resources/messages/MinecraftDevelopment_zh.properties @@ -18,7 +18,7 @@ # along with this program. If not, see . # -creator.ui.build_system.label.generic=构建系统: +creator.ui.build_system.label=构建系统: creator.ui.build_system.label.gradle=Gradle creator.ui.build_system.label.maven=Maven @@ -45,7 +45,7 @@ creator.ui.issue_tracker.label=Issue Tracker: creator.ui.update_url.label=更新 URL: creator.ui.depend.label=依赖: creator.ui.soft_depend.label=软依赖: -creator.ui.mixins.label=使用 Mixins: +creator.ui.use_mixins.label=使用 Mixins: creator.ui.parchment.label=Parchment: creator.ui.parchment.include.label=Include: creator.ui.parchment.include.old_mc.label=更旧的 Minecraft 版本 diff --git a/templates b/templates new file mode 160000 index 000000000..c8cf7b83d --- /dev/null +++ b/templates @@ -0,0 +1 @@ +Subproject commit c8cf7b83d9f15903c40e603725318de5bcba85f8