IJPL-207056: Move WebStarterModuleBuilder to intellij.java.ui

GitOrigin-RevId: b66c6a62ffbb751e0e4af0592d8e2ea7b95da7bd
This commit is contained in:
Karol Lewandowski
2025-09-12 10:56:22 +00:00
committed by intellij-monorepo-bot
parent c98d45b58b
commit 64497ad7a5
7 changed files with 1592 additions and 0 deletions
+2
View File
@@ -62,6 +62,8 @@ jvm_library(
"@lib//:slf4j-api",
"//xml/xml-psi-api:psi",
"@lib//:asm",
"@lib//:jackson",
"@lib//:jackson-databind",
],
runtime_deps = [":ui_resources"]
)
+2
View File
@@ -58,5 +58,7 @@
<orderEntry type="library" name="slf4j-api" level="project" />
<orderEntry type="module" module-name="intellij.xml.psi" />
<orderEntry type="library" name="ASM" level="project" />
<orderEntry type="library" name="jackson" level="project" />
<orderEntry type="library" name="jackson-databind" level="project" />
</component>
</module>
@@ -0,0 +1,21 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starters.remote
import com.intellij.ide.starters.shared.CommonStarterContext
import com.intellij.ide.starters.shared.DEFAULT_PACKAGE_NAME
import com.intellij.ide.starters.shared.StarterAppPackaging
import com.intellij.ide.starters.shared.StarterLanguageLevel
class WebStarterContext : CommonStarterContext() {
lateinit var serverUrl: String
lateinit var serverOptions: WebStarterServerOptions
var frameworkVersion: WebStarterFrameworkVersion? = null
var packageName: String = DEFAULT_PACKAGE_NAME
var languageLevel: StarterLanguageLevel? = null
var packaging: StarterAppPackaging? = null
val dependencies: MutableSet<WebStarterDependency> = HashSet()
var result: DownloadResult? = null
}
@@ -0,0 +1,439 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starters.remote
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.intellij.codeInsight.actions.ReformatCodeProcessor
import com.intellij.ide.IdeBundle
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.projectWizard.ProjectSettingsStep
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.local.StarterModuleBuilder
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.importModule
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.openSampleFiles
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.preprocessModuleCreated
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.preprocessModuleOpened
import com.intellij.ide.starters.remote.wizard.WebStarterInitialStep
import com.intellij.ide.starters.remote.wizard.WebStarterLibrariesStep
import com.intellij.ide.starters.shared.*
import com.intellij.ide.util.projectWizard.ModuleBuilder
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.SettingsStep
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.GitRepositoryInitializer
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil
import com.intellij.openapi.module.JavaModuleType
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.runBackgroundableTask
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.JavaSdkType
import com.intellij.openapi.projectRoots.JavaSdkVersion
import com.intellij.openapi.projectRoots.SdkTypeId
import com.intellij.openapi.roots.LanguageLevelModuleExtension
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.ui.configuration.ModulesProvider
import com.intellij.openapi.roots.ui.configuration.setupNewModuleJdk
import com.intellij.openapi.startup.StartupManager
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.getPluginSuggestionNotificationGroup
import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.installAndEnable
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.util.Url
import com.intellij.util.concurrency.EdtExecutorService
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.io.HttpRequests
import com.intellij.util.io.HttpRequests.RequestProcessor
import java.io.File
import java.io.IOException
import java.net.URLConnection
import java.net.UnknownHostException
import java.util.concurrent.TimeUnit
import javax.swing.Icon
abstract class WebStarterModuleBuilder : ModuleBuilder() {
protected val starterContext: WebStarterContext = WebStarterContext()
protected val starterSettings: StarterWizardSettings by lazy { createSettings() }
protected val objectMapper: ObjectMapper by lazy { ObjectMapper() }
override fun getModuleType(): ModuleType<*> = JavaModuleType.getModuleType()
override fun getWeight(): Int = JVM_WEIGHT
open fun getHelpId(): String? = null
// Required settings
abstract override fun getBuilderId(): String
abstract override fun getNodeIcon(): Icon?
abstract override fun getPresentableName(): String
abstract override fun getDescription(): String
abstract fun getDefaultServerUrl(): String
protected abstract fun getLanguages(): List<StarterLanguage>
protected abstract fun getProjectTypes(): List<StarterProjectType>
// Optional settings
protected open fun getDefaultVersion(): String = DEFAULT_MODULE_VERSION
protected open fun isPackageNameEditable(): Boolean = false
protected open fun isExampleCodeProvided(): Boolean = false
protected open fun getTestFrameworks(): List<StarterTestRunner> = emptyList()
protected open fun getLanguageLevels(): List<StarterLanguageLevel> = emptyList()
protected open fun getDefaultLanguageLevel(): StarterLanguageLevel? = null
protected open fun getApplicationTypes(): List<StarterAppType> = emptyList()
protected open fun getPackagingTypes(): List<StarterAppPackaging> = emptyList()
protected open fun getFilePathsToOpen(): List<String> = emptyList()
protected open fun isShowProjectTypes(): Boolean = true
override fun isSuitableSdkType(sdkType: SdkTypeId): Boolean {
return sdkType is JavaSdkType && !sdkType.isDependent
}
private fun createSettings(): StarterWizardSettings {
return StarterWizardSettings(
getProjectTypes(),
getLanguages(),
isExampleCodeProvided(),
isPackageNameEditable(),
getLanguageLevels(),
getDefaultLanguageLevel(),
getPackagingTypes(),
getApplicationTypes(),
getTestFrameworks(),
getCustomizedMessages(),
isShowProjectTypes()
)
}
override fun getCustomOptionsStep(context: WizardContext, parentDisposable: Disposable): ModuleWizardStep? {
starterContext.serverUrl = getDefaultServerUrl()
starterContext.version = getDefaultVersion()
starterContext.language = starterSettings.languages.first()
starterContext.projectType = starterSettings.projectTypes.firstOrNull()
starterContext.isCreatingNewProject = context.isCreatingNewProject
starterContext.applicationType = starterSettings.applicationTypes.firstOrNull()
starterContext.languageLevel = starterSettings.defaultLanguageLevel ?: starterSettings.languageLevels.firstOrNull()
starterContext.packaging = starterSettings.packagingTypes.firstOrNull()
starterContext.testFramework = starterSettings.testFrameworks.firstOrNull()
return createOptionsStep(WebStarterContextProvider(this, context, starterContext, starterSettings, parentDisposable))
}
override fun createWizardSteps(context: WizardContext, modulesProvider: ModulesProvider): Array<ModuleWizardStep> {
return arrayOf(createLibrariesStep(WebStarterContextProvider(this, context, starterContext, starterSettings, context.disposable)))
}
override fun getIgnoredSteps(): List<Class<out ModuleWizardStep>> {
return listOf(ProjectSettingsStep::class.java)
}
override fun modifyProjectTypeStep(settingsStep: SettingsStep): ModuleWizardStep? {
// do not add standard SDK selector at the top
return null
}
protected open fun createOptionsStep(contextProvider: WebStarterContextProvider): WebStarterInitialStep {
return WebStarterInitialStep(contextProvider)
}
protected open fun createLibrariesStep(contextProvider: WebStarterContextProvider): WebStarterLibrariesStep {
return WebStarterLibrariesStep(contextProvider)
}
private fun getUserAgentInternal(): String? = getUserAgent()
protected open fun getUserAgent(): String? {
return ApplicationNamesInfo.getInstance().fullProductName + "/" + ApplicationInfo.getInstance().fullVersion
}
protected open fun getCustomizedMessages(): CustomizedMessages? = null
@RequiresBackgroundThread
fun getServerOptions(serverUrl: String): WebStarterServerOptions = loadServerOptions(serverUrl)
@RequiresBackgroundThread
protected abstract fun loadServerOptions(serverUrl: String): WebStarterServerOptions
internal fun getDependencyStateInternal(frameworkVersion: WebStarterFrameworkVersion, dependency: WebStarterDependency): DependencyState {
return getDependencyState(frameworkVersion, dependency)
}
internal fun isVersionAvailableInternal(frameworkVersion: WebStarterFrameworkVersion): Boolean {
return isVersionAvailable(frameworkVersion)
}
protected open fun getDependencyState(frameworkVersion: WebStarterFrameworkVersion, dependency: WebStarterDependency): DependencyState {
return DependencyAvailable
}
protected open fun isVersionAvailable(frameworkVersion: WebStarterFrameworkVersion): Boolean {
return true
}
private fun getGeneratorUrlInternal(serverUrl: String, starterContext: WebStarterContext): Url {
return composeGeneratorUrl(serverUrl, starterContext)
}
protected abstract fun composeGeneratorUrl(serverUrl: String, starterContext: WebStarterContext): Url
protected abstract fun extractGeneratorResult(tempZipFile: File, contentEntryDir: File)
protected open fun getPluginRecommendations(): List<PluginRecommendation> = emptyList()
protected open fun isReformatAfterCreation(project: Project): Boolean = true
override fun createProject(name: String, path: String): Project? {
val project = super.createProject(name, path)
project?.let { StarterModuleBuilder.setupProject(it) }
return project
}
@Throws(ConfigurationException::class)
override fun setupModule(module: Module) {
super.setupModule(module)
val isMavenProject = starterContext.projectType?.id?.contains("Maven", ignoreCase = true) ?: false
ExternalSystemUtil.configureNewModule(module, starterContext.isCreatingNewProject, isMavenProject)
val project = module.project
try {
extractTemplate() // should be quite fast, only extracts single small ZIP file
}
catch (e: Exception) {
thisLogger().info(e)
StartupManager.getInstance(project).runAfterOpened {
EdtExecutorService.getScheduledExecutorInstance().schedule(
{
var message = JavaStartersBundle.message("error.text.with.error.content", e.message)
message = StringUtil.shortenTextWithEllipsis(message, 1024, 0) // exactly 1024 because why not
Messages.showErrorDialog(message, presentableName)
},
3, TimeUnit.SECONDS)
}
return
}
preprocessModuleCreated(module, this, starterContext.frameworkVersion?.id)
StartupManager.getInstance(project).runAfterOpened {
ApplicationManager.getApplication().invokeLater({ runImport(module) },
ModalityState.nonModal(), module.disposed)
}
}
override fun setupRootModel(modifiableRootModel: ModifiableRootModel) {
val sdk = setupNewModuleJdk(modifiableRootModel, moduleJdk, starterContext.isCreatingNewProject)
val moduleExt = modifiableRootModel.getModuleExtension(LanguageLevelModuleExtension::class.java)
if (moduleExt != null && sdk != null) {
val languageLevel = starterContext.languageLevel
if (languageLevel != null) {
val selectedVersion = JavaSdkVersion.fromVersionString(languageLevel.id)
val sdkVersion = JavaSdk.getInstance().getVersion(sdk)
if (selectedVersion != null && sdkVersion != null && sdkVersion.isAtLeast(selectedVersion)) {
moduleExt.languageLevel = selectedVersion.maxLanguageLevel
}
}
}
doAddContentEntry(modifiableRootModel)
}
private fun runImport(module: Module) {
LocalFileSystem.getInstance().refresh(false) // to avoid IDEA-232806
preprocessModuleOpened(module, this, starterContext.frameworkVersion?.id)
if (isReformatAfterCreation(module.project)) {
ReformatCodeProcessor(module.project, module, false).run()
}
openSampleFiles(module, getFilePathsToOpen())
if (starterContext.gitIntegration && starterContext.isCreatingNewProject) {
val moduleContentRoot = LocalFileSystem.getInstance().refreshAndFindFileByPath(contentEntryPath!!.replace("\\", "/"))
?: throw IllegalStateException("Module root not found")
runBackgroundableTask(IdeBundle.message("progress.title.creating.git.repository"), module.project) {
GitRepositoryInitializer.getInstance()?.initRepository(module.project, moduleContentRoot, true)
}
}
importModule(module)
verifyIdePlugins(module.project)
}
private fun extractTemplate() {
val downloadResult = starterContext.result!!
val tempFile = downloadResult.tempFile
val path: String = contentEntryPath!!
val contentEntryDir = File(path)
if (downloadResult.isZip) {
extractGeneratorResult(tempFile, contentEntryDir)
fixExecutableFlag(contentEntryDir, "gradlew")
fixExecutableFlag(contentEntryDir, "mvnw")
}
else {
FileUtil.copy(tempFile, File(contentEntryDir, downloadResult.filename))
}
}
private fun verifyIdePlugins(project: Project) {
val selectedDependenciesIds = starterContext.dependencies.map { it.id }.toSet()
val requiredPluginIds: MutableSet<PluginId> = HashSet()
for (pluginRecommendation in getPluginRecommendations()) {
for (dependencyId in pluginRecommendation.dependencyIds) {
if (selectedDependenciesIds.contains(dependencyId)) {
requiredPluginIds.add(PluginId.getId(pluginRecommendation.pluginId))
break
}
}
}
val toInstallOrEnable: MutableSet<PluginId> = HashSet()
for (pluginId in requiredPluginIds) {
val ideaPluginDescriptor = PluginManagerCore.getPlugin(pluginId)
if (ideaPluginDescriptor == null || !ideaPluginDescriptor.isEnabled) {
toInstallOrEnable.add(pluginId)
}
}
if (toInstallOrEnable.isEmpty()) return
getPluginSuggestionNotificationGroup()
.createNotification(IdeBundle.message("plugins.advertiser.plugins.suggestions.title"),
IdeBundle.message("plugins.advertiser.plugins.suggestions.text"), NotificationType.INFORMATION)
.addAction(NotificationAction.create(IdeBundle.message("plugins.advertiser.action.enable.plugins")) { _, notification ->
installAndEnable(project, toInstallOrEnable) { notification.expire() }
})
.notify(project)
}
private fun fixExecutableFlag(containingDir: File, relativePath: String) {
val toFix = File(containingDir, relativePath)
if (toFix.exists()) {
toFix.setExecutable(true, false)
}
}
@RequiresBackgroundThread
protected fun loadJsonData(url: String, accept: String? = null): JsonNode {
return HttpRequests.request(url)
.userAgent(getUserAgent())
.accept(accept)
.connectTimeout(10000)
.connect(RequestProcessor { request ->
val reader = try {
request.reader
}
catch (e: IOException) {
thisLogger().info("IOException loading JSON response from " + request.url, e)
throw IOException(HttpRequests.createErrorMessage(e, request, false), e)
}
val jsonRootElement = try {
objectMapper.readTree(reader)
}
catch (e: Throwable) {
thisLogger().info("Unable to read JSON response from " + request.url, e)
throw IOException("Error parsing JSON response", e)
}
jsonRootElement ?: throw IOException("Error parsing JSON response: empty document")
})
}
@RequiresBackgroundThread
internal fun downloadResultInternal(progressIndicator: ProgressIndicator): DownloadResult {
val tempFile = FileUtil.createTempFile(builderId, ".tmp", true)
return downloadResult(progressIndicator, tempFile)
}
@RequiresBackgroundThread
protected open fun downloadResult(progressIndicator: ProgressIndicator, tempFile: File): DownloadResult {
val url = getGeneratorUrlInternal(starterContext.serverUrl, starterContext).toExternalForm()
thisLogger().info("Loading project from ${url}")
return HttpRequests
.request(url)
.userAgent(getUserAgentInternal())
.connectTimeout(10000)
.isReadResponseOnError(true)
.connect(RequestProcessor { request ->
handleDownloadResponse(request, tempFile, progressIndicator)
})
}
protected fun handleDownloadResponse(request: HttpRequests.Request,
tempFile: File,
progressIndicator: ProgressIndicator): DownloadResult {
val connection: URLConnection = try {
request.connection
}
catch (e: IOException) {
thisLogger().warn("Can't download project. Message (with headers info): "
+ HttpRequests.createErrorMessage(e, request, true))
throw IOException(HttpRequests.createErrorMessage(e, request, false), e)
}
catch (he: UnknownHostException) {
thisLogger().warn("Can't download project: " + he.message)
throw IOException(HttpRequests.createErrorMessage(he, request, false), he)
}
val contentType = connection.contentType
val contentDisposition = connection.getHeaderField("Content-Disposition")
val filename = getFilename(contentDisposition)
val isZip = StringUtil.isNotEmpty(contentType) && contentType.startsWith("application/zip")
|| filename.endsWith(".zip")
// Micronaut has broken content-type (it's "text") but zip-file as attachment
// (https://github.com/micronaut-projects/micronaut-starter/issues/268)
request.saveToFile(tempFile, progressIndicator)
return DownloadResult(isZip, tempFile, filename)
}
@NlsSafe
private fun getFilename(contentDisposition: String?): String {
val filenameField = "filename="
if (StringUtil.isEmpty(contentDisposition)) return "unknown"
val startIdx = contentDisposition!!.indexOf(filenameField)
val endIdx = contentDisposition.indexOf(';', startIdx)
var fileName = contentDisposition.substring(startIdx + filenameField.length, if (endIdx > 0) endIdx else contentDisposition.length)
if (StringUtil.startsWithChar(fileName, '\"') && StringUtil.endsWithChar(fileName, '\"')) {
fileName = fileName.substring(1, fileName.length - 1)
}
return fileName
}
fun JsonNode.getNullable(field: String): JsonNode? {
val element = this.get(field) ?: return null
if (element.isNull) {
return null
}
return element
}
}
@@ -0,0 +1,122 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("WebStarterSettings")
package com.intellij.ide.starters.remote
import com.intellij.ide.starters.shared.*
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.util.io.Decompressor
import java.io.File
import java.io.IOException
import java.nio.file.Paths
import java.util.zip.ZipFile
open class WebStarterServerOptions(
val frameworkVersions: List<WebStarterFrameworkVersion>,
val dependencyCategories: List<WebStarterDependencyCategory>,
) : UserDataHolderBase() {
fun <T> extractOption(key: Key<T>, handler: (T) -> Unit) {
val value = getUserData(key)
if (value != null) {
handler.invoke(value)
}
}
}
val SERVER_NAME_KEY: Key<String> = Key.create("name")
val SERVER_GROUP_KEY: Key<String> = Key.create("group")
val SERVER_ARTIFACT_KEY: Key<String> = Key.create("artifact")
val SERVER_VERSION_KEY: Key<String> = Key.create("version")
val SERVER_PACKAGE_NAME_KEY: Key<String> = Key.create("packageName")
val SERVER_LANGUAGES: Key<List<StarterLanguage>> = Key.create("languages")
val SERVER_LANGUAGE_LEVEL_KEY: Key<StarterLanguageLevel> = Key.create("languageLevel")
val SERVER_LANGUAGE_LEVELS_KEY: Key<List<StarterLanguageLevel>> = Key.create("languageLevels")
val SERVER_PROJECT_TYPES: Key<List<StarterProjectType>> = Key.create("projectTypes")
val SERVER_APPLICATION_TYPES: Key<List<StarterAppType>> = Key.create("appTypes")
val SERVER_PACKAGING_TYPES: Key<List<StarterAppPackaging>> = Key.create("packagingTypes")
open class WebStarterFrameworkVersion(
val id: String,
@NlsSafe val title: String,
val isDefault: Boolean
) {
override fun toString(): String {
return "WebStarterFrameworkVersion(version='$id', isDefault=$isDefault)"
}
}
open class WebStarterDependencyCategory(
@NlsSafe
val title: String,
val dependencies: List<WebStarterDependency>
) {
open fun isAvailable(starterContext: WebStarterContext): Boolean = true
override fun toString(): String {
return "WebStarterDependencyCategory(title='$title')"
}
}
open class WebStarterDependency(
val id: String,
@NlsSafe
override val title: String,
override val description: String? = null,
override val links: List<LibraryLink> = emptyList(),
override val isDefault: Boolean = false,
override val isRequired: Boolean = false
) : LibraryInfo {
override fun toString(): String {
return "WebStarterDependency(id='$id', title='$title')"
}
}
class WebStarterContextProvider(
val moduleBuilder: WebStarterModuleBuilder,
val wizardContext: WizardContext,
val starterContext: WebStarterContext,
val settings: StarterWizardSettings,
val parentDisposable: Disposable
)
@Throws(IOException::class)
fun unzipSubfolder(tempZipFile: File, contentEntryDir: File) {
var rootFolderName: String
ZipFile(tempZipFile).use { jar ->
val entries = jar.entries()
if (!entries.hasMoreElements()) {
throw UnexpectedArchiveStructureException("The archive is empty")
}
val rootFolders = HashSet<String>()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val path = Paths.get(entry.name)
if (path.nameCount > 0) {
rootFolders.add(path.normalize().getName(0).toString())
}
}
if (rootFolders.size != 1) {
throw UnexpectedArchiveStructureException(
"The archive should have 1 subdirectory, but has: " + rootFolders.joinToString(","))
}
rootFolderName = rootFolders.iterator().next()
}
Decompressor.Zip(tempZipFile)
.removePrefixPath(rootFolderName)
.extract(contentEntryDir)
}
class DownloadResult(
val isZip: Boolean,
val tempFile: File,
val filename: String
)
internal class UnexpectedArchiveStructureException(message: String) : IOException(message)
@@ -0,0 +1,518 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starters.remote.wizard
import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.local.StarterModuleBuilder
import com.intellij.ide.starters.remote.*
import com.intellij.ide.starters.shared.*
import com.intellij.ide.starters.shared.ValidationFunctions.*
import com.intellij.ide.util.PropertiesComponent
import com.intellij.ide.wizard.AbstractWizard
import com.intellij.ide.wizard.withVisualPadding
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.observable.properties.GraphProperty
import com.intellij.openapi.observable.util.trim
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.roots.ui.configuration.validateJdkIntentVersion
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.InputValidator
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.popup.IconButton
import com.intellij.openapi.util.Condition
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.io.FileUtil
import com.intellij.ui.InplaceButton
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.*
import com.intellij.util.concurrency.Semaphore
import com.intellij.util.ui.AsyncProcessIcon
import com.intellij.util.ui.UIUtil
import java.awt.event.ActionListener
import java.io.IOException
import java.net.MalformedURLException
import java.net.URL
import java.util.concurrent.Future
import javax.swing.DefaultComboBoxModel
import javax.swing.JComponent
import javax.swing.SwingUtilities
open class WebStarterInitialStep(contextProvider: WebStarterContextProvider) : CommonStarterInitialStep(
contextProvider.wizardContext,
contextProvider.starterContext,
contextProvider.moduleBuilder,
contextProvider.parentDisposable,
contextProvider.settings
) {
protected val moduleBuilder: WebStarterModuleBuilder = contextProvider.moduleBuilder
private val starterContext: WebStarterContext = contextProvider.starterContext
private val packageNameProperty: GraphProperty<String> = propertyGraph.lazyProperty { starterContext.packageName }
private val packagingProperty: GraphProperty<StarterAppPackaging> = propertyGraph.lazyProperty {
starterContext.packaging ?: StarterAppPackaging("unknown", "")
}
private val languageLevelProperty: GraphProperty<StarterLanguageLevel> = propertyGraph.lazyProperty {
starterContext.languageLevel ?: StarterLanguageLevel("unknown", "", "")
}
private var languageLevel: StarterLanguageLevel by languageLevelProperty
private var packageName: String by packageNameProperty.trim()
private val contentPanel: DialogPanel by lazy { createComponent() }
private val progressIcon: AsyncProcessIcon by lazy { AsyncProcessIcon(moduleBuilder.builderId + "ServerOptions") }
private val serverUrlLink: ActionLink by lazy { createServerUrlLink() }
private val retryButton: InplaceButton by lazy { createRetryButton() }
private val languageLevelsModel: DefaultComboBoxModel<StarterLanguageLevel> = DefaultComboBoxModel<StarterLanguageLevel>()
private val applicationTypesModel: DefaultComboBoxModel<StarterAppType> = DefaultComboBoxModel<StarterAppType>()
private lateinit var projectTypesSelector: SegmentedButton<StarterProjectType>
private lateinit var packagingTypesSelector: SegmentedButton<StarterAppPackaging>
private lateinit var languagesSelector: SegmentedButton<StarterLanguage>
private var languages: List<StarterLanguage> = starterSettings.languages
private var applicationTypes: List<StarterAppType> = starterSettings.applicationTypes
private var projectTypes: List<StarterProjectType> = starterSettings.projectTypes
private var packagingTypes: List<StarterAppPackaging> = starterSettings.packagingTypes
@Volatile
private var serverOptions: WebStarterServerOptions? = null
@Volatile
private var currentRequest: Future<*>? = null
@Volatile
private var isDisposed: Boolean = false
private val serverOptionsLoadingSemaphore: Semaphore = Semaphore()
private val serverSettingsButton: InplaceButton = InplaceButton(
IconButton(JavaStartersBundle.message("button.tooltip.configure"), AllIcons.General.Gear, AllIcons.General.GearHover),
ActionListener {
configureServer()
}
)
init {
Disposer.register(parentDisposable, Disposable {
isDisposed = true
currentRequest?.cancel(true)
})
}
override fun getComponent(): JComponent = contentPanel
override fun getHelpId(): String? = moduleBuilder.getHelpId()
override fun updateDataModel() {
starterContext.serverOptions = this.serverOptions!!
starterContext.projectType = projectTypeProperty.get()
starterContext.language = languageProperty.get()
starterContext.group = groupId
starterContext.artifact = artifactId
starterContext.name = entityName
starterContext.packageName = packageName
starterContext.packaging = packagingProperty.get()
starterContext.languageLevel = languageLevel
starterContext.testFramework = testFrameworkProperty.get()
starterContext.applicationType = applicationTypeProperty.get()
starterContext.includeExamples = exampleCodeProperty.get()
starterContext.gitIntegration = gitProperty.get()
wizardContext.projectName = entityName
wizardContext.setProjectFileDirectory(FileUtil.join(location, entityName))
val sdk = jdkIntentProperty.get().prepareJdk()
if (wizardContext.project == null) {
wizardContext.projectJdk = sdk
}
else {
moduleBuilder.moduleJdk = sdk
}
}
private fun suggestPackageName(): String {
return StarterModuleBuilder.suggestPackageName(groupId, artifactId)
}
private fun createComponent(): DialogPanel {
entityNameProperty.dependsOn(artifactIdProperty) { artifactId }
artifactIdProperty.dependsOn(entityNameProperty) { entityName }
packageNameProperty.dependsOn(artifactIdProperty, ::suggestPackageName)
packageNameProperty.dependsOn(groupIdProperty, ::suggestPackageName)
progressIcon.toolTipText = JavaStartersBundle.message("message.state.connecting.and.retrieving.options")
return panel {
row {
label(JavaStartersBundle.message("title.project.server.url.label"))
cell(serverUrlLink)
cell(serverSettingsButton)
cell(retryButton)
cell(progressIcon)
}.bottomGap(BottomGap.SMALL)
addProjectLocationUi()
addFieldsBefore(this)
if (starterSettings.languages.size > 1) {
row(JavaStartersBundle.message("title.project.language.label")) {
languagesSelector = segmentedButton(starterSettings.languages) { text = it.title }
.bind(languageProperty)
bottomGap(BottomGap.SMALL)
}
}
if (starterSettings.projectTypes.isNotEmpty() && starterSettings.showProjectTypes) {
val messages = starterSettings.customizedMessages
row(messages?.projectTypeLabel ?: JavaStartersBundle.message("title.project.type.label")) {
projectTypesSelector = segmentedButton(starterSettings.projectTypes) {
text = it.title
toolTipText = it.description
}.bind(projectTypeProperty)
bottomGap(BottomGap.SMALL)
}
}
if (starterSettings.testFrameworks.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.test.framework.label")) {
segmentedButton(starterSettings.testFrameworks) { text = it.title }
.bind(testFrameworkProperty)
}.bottomGap(BottomGap.SMALL)
}
addGroupArtifactUi()
if (starterSettings.isPackageNameEditable) {
row(JavaStartersBundle.message("title.project.package.label")) {
textField()
.accessibleName(JavaStartersBundle.message("title.project.package.label"))
.bindText(packageNameProperty)
.columns(COLUMNS_MEDIUM)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_NO_RESERVED_WORDS, CHECK_PACKAGE_NAME)
}.bottomGap(BottomGap.SMALL)
}
if (starterSettings.applicationTypes.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.app.type.label")) {
applicationTypesModel.addAll(starterSettings.applicationTypes)
comboBox(applicationTypesModel, SimpleListCellRenderer.create("") { it?.title ?: "" })
.bindItem(applicationTypeProperty)
.columns(COLUMNS_MEDIUM)
}.bottomGap(BottomGap.SMALL)
}
addSdkUi()
if (starterSettings.languageLevels.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.java.version.label")) {
languageLevelsModel.addAll(starterSettings.languageLevels)
comboBox(languageLevelsModel, SimpleListCellRenderer.create("") { it?.title ?: "" })
.bindItem(languageLevelProperty)
}.bottomGap(BottomGap.SMALL)
}
if (starterSettings.packagingTypes.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.packaging.label")) {
packagingTypesSelector = segmentedButton(starterSettings.packagingTypes) { text = it.title }
.bind(packagingProperty)
}.bottomGap(BottomGap.SMALL)
}
addSampleCodeUi()
addFieldsAfter(this)
}.withVisualPadding()
}
private fun createServerUrlLink(): ActionLink {
val result = ActionLink(urlPreview(starterContext.serverUrl)) {
BrowserUtil.browse(starterContext.serverUrl)
}
UIUtil.applyStyle(UIUtil.ComponentStyle.REGULAR, result)
return result
}
private fun createRetryButton(): InplaceButton {
return InplaceButton(IconButton(JavaStartersBundle.message("button.tooltip.retry"),
AllIcons.Nodes.ErrorIntroduction, AllIcons.Actions.ForceRefresh), ActionListener {
requestServerOptions()
}).apply {
isVisible = false
}
}
@NlsSafe
private fun urlPreview(serverUrl: String): String {
val url = serverUrl
.removePrefix("https://")
.removeSuffix("/")
if (url.length > 35) {
return url.take(30) + "..."
}
return url
}
override fun getPreferredFocusedComponent(): JComponent? {
return contentPanel.preferredFocusedComponent
}
override fun validate(): Boolean {
if (!validateFormFields(component, contentPanel, validatedTextComponents)) {
return false
}
val passTechnologyName = if (starterSettings.languageLevels.size > 1) null else moduleBuilder.presentableName
if (languageLevel.javaVersion.isNotBlank() &&
!validateJdkIntentVersion(jdkIntentProperty, languageLevel.javaVersion, passTechnologyName)) {
return false
}
return checkServerOptionsLoaded()
}
private fun checkServerOptionsLoaded(): Boolean {
val request = currentRequest
if (serverOptions != null && request == null) {
return true
}
if (request == null) {
// failure? retry server options loading
requestServerOptions()
}
val newOptionsRef: Ref<WebStarterServerOptions> = Ref.create()
ProgressManager.getInstance().runProcessWithProgressSynchronously(Runnable {
val progressIndicator = ProgressManager.getInstance().progressIndicator
progressIndicator.isIndeterminate = true
for (i in 0 until 30) {
progressIndicator.checkCanceled()
if (serverOptionsLoadingSemaphore.waitFor(500)) {
serverOptions?.let {
newOptionsRef.set(it)
}
return@Runnable
}
}
}, JavaStartersBundle.message("message.state.connecting.and.retrieving.options"), true, wizardContext.project)
if (!newOptionsRef.isNull) {
updatePropertiesWithServerOptions(newOptionsRef.get())
}
return serverOptions != null
}
override fun _init() {
super._init()
if (serverOptions == null && currentRequest == null) {
val serverUrlFromSettings = PropertiesComponent.getInstance().getValue(getServerUrlPropertyName())
if (serverUrlFromSettings != null) {
setServerUrl(serverUrlFromSettings)
}
// required on dialog opening to get correct modality state
SwingUtilities.invokeLater(this::requestServerOptions)
}
}
private fun setServerUrl(@NlsSafe url: String) {
starterContext.serverUrl = url
serverUrlLink.text = urlPreview(url)
serverUrlLink.toolTipText = url
}
private fun requestServerOptions() {
progressIcon.isVisible = true
retryButton.isVisible = false
progressIcon.resume()
serverOptionsLoadingSemaphore.down()
currentRequest = ApplicationManager.getApplication().executeOnPooledThread {
val readyServerOptions = try {
moduleBuilder.getServerOptions(starterContext.serverUrl)
}
catch (e: Exception) {
if (e is IOException || e is IllegalStateException) {
logger<WebStarterInitialStep>().info("Unable to get server options for " + moduleBuilder.builderId, e)
}
else {
logger<WebStarterInitialStep>().error("Unable to get server options for " + moduleBuilder.builderId, e)
}
ApplicationManager.getApplication().invokeLater(
{
if (component.isShowing) {
// only if the wizard is visible
Messages.showErrorDialog(
JavaStartersBundle.message("message.no.connection.with.error.content", starterContext.serverUrl, e.message),
JavaStartersBundle.message("message.title.error"))
}
}, getModalityState())
null
}
setServerOptions(readyServerOptions)
}
}
private fun setServerOptions(serverOptions: WebStarterServerOptions?) {
this.serverOptions = serverOptions
this.currentRequest = null
this.serverOptionsLoadingSemaphore.up()
ApplicationManager.getApplication().invokeLater(Runnable {
progressIcon.suspend()
progressIcon.isVisible = false
retryButton.isVisible = serverOptions == null
if (serverOptions != null) {
updatePropertiesWithServerOptions(serverOptions)
}
}, getModalityState(), getDisposed())
}
private fun getModalityState(): ModalityState {
return ModalityState.stateForComponent(wizardContext.getUserData(AbstractWizard.KEY)!!.contentComponent)
}
private fun getDisposed(): Condition<Any> = Condition<Any> { isDisposed }
private fun configureServer() {
val currentServerUrl = starterContext.serverUrl
val serverUrlTitle = starterSettings.customizedMessages?.serverUrlDialogTitle
?: JavaStartersBundle.message("title.server.url.dialog")
val newUrl = Messages.showInputDialog(component, null, serverUrlTitle, null, currentServerUrl, object : InputValidator {
override fun canClose(inputString: String?): Boolean = checkInput(inputString)
override fun checkInput(inputString: String?): Boolean {
try {
URL(inputString)
}
catch (e: MalformedURLException) {
return false
}
return true
}
})
// update
if (newUrl != null && starterContext.serverUrl != newUrl) {
setServerUrl(newUrl)
PropertiesComponent.getInstance().setValue(getServerUrlPropertyName(), newUrl)
requestServerOptions()
}
}
private fun getServerUrlPropertyName(): String {
return moduleBuilder.builderId + ".service.url.last"
}
protected open fun updatePropertiesWithServerOptions(serverOptions: WebStarterServerOptions) {
starterContext.frameworkVersion = serverOptions.frameworkVersions.find { it.isDefault }
?: serverOptions.frameworkVersions.firstOrNull()
val currentPackageName = packageName // remember package name before applying group and artifact
serverOptions.extractOption(SERVER_NAME_KEY) {
if (entityName == suggestName(DEFAULT_MODULE_NAME)) {
val newName = suggestName(it)
if (entityName != newName) {
entityNameProperty.set(newName)
}
}
}
serverOptions.extractOption(SERVER_GROUP_KEY) {
if (groupId == DEFAULT_MODULE_GROUP && groupId != it) {
groupIdProperty.set(it)
}
}
serverOptions.extractOption(SERVER_ARTIFACT_KEY) {
if (artifactId == DEFAULT_MODULE_ARTIFACT && artifactId != it) {
artifactIdProperty.set(it)
}
}
serverOptions.extractOption(SERVER_PACKAGE_NAME_KEY) {
if (currentPackageName == DEFAULT_PACKAGE_NAME && currentPackageName != it) {
packageNameProperty.set(it)
}
}
serverOptions.extractOption(SERVER_VERSION_KEY) {
starterContext.version = it
}
serverOptions.extractOption(SERVER_LANGUAGE_LEVELS_KEY) { levels ->
val selectedItem = languageLevelsModel.selectedItem
languageLevelsModel.removeAllElements()
languageLevelsModel.addAll(levels)
if (levels.contains(selectedItem)) {
languageLevelsModel.selectedItem = selectedItem
}
else {
languageLevel = levels.firstOrNull() ?: StarterLanguageLevel("unknown", "", "")
languageLevelsModel.selectedItem = languageLevel
}
}
serverOptions.extractOption(SERVER_LANGUAGE_LEVEL_KEY) { level ->
if (languageLevel == starterSettings.defaultLanguageLevel && languageLevel != level) {
languageLevelProperty.set(level)
}
}
serverOptions.extractOption(SERVER_PROJECT_TYPES) { types ->
if (types.isNotEmpty() && types != this.projectTypes && ::projectTypesSelector.isInitialized) {
val correspondingOption = types.find { it.id == projectTypeProperty.get().id }
projectTypeProperty.set(correspondingOption ?: types.first())
projectTypesSelector.items = types
this.projectTypes = types
}
}
serverOptions.extractOption(SERVER_APPLICATION_TYPES) { types ->
if (types.isNotEmpty() && types != applicationTypes) {
applicationTypesModel.removeAllElements()
applicationTypesModel.addAll(types)
applicationTypesModel.selectedItem = types.firstOrNull()
this.applicationTypes = types
}
}
serverOptions.extractOption(SERVER_PACKAGING_TYPES) { types ->
if (types.isNotEmpty() && types != this.packagingTypes && ::packagingTypesSelector.isInitialized) {
val correspondingOption = types.find { it.id == packagingProperty.get().id }
packagingProperty.set(correspondingOption ?: types.first())
packagingTypesSelector.items = types
this.packagingTypes = types
}
}
serverOptions.extractOption(SERVER_LANGUAGES) { languages ->
if (languages.isNotEmpty() && languages != this.languages && ::languagesSelector.isInitialized) {
val correspondingOption = languages.find { it.id == languageProperty.get().id }
languageProperty.set(correspondingOption ?: languages.first())
languagesSelector.items = languages
this.languages = languages
}
}
contentPanel.revalidate()
}
}
@@ -0,0 +1,488 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starters.remote.wizard
import com.intellij.ide.IdeBundle
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.remote.*
import com.intellij.ide.starters.shared.*
import com.intellij.ide.starters.shared.ui.LibrariesSearchTextField
import com.intellij.ide.starters.shared.ui.LibraryDescriptionPanel
import com.intellij.ide.starters.shared.ui.SelectedLibrariesPanel
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.ide.wizard.AbstractWizard
import com.intellij.ide.wizard.withVisualPadding
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.observable.properties.GraphProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.Condition
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.*
import com.intellij.ui.components.JBLabel
import com.intellij.ui.dsl.builder.*
import com.intellij.util.ModalityUiUtil
import com.intellij.util.concurrency.EdtExecutorService
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil.DEFAULT_HGAP
import com.intellij.util.ui.UIUtil.DEFAULT_VGAP
import com.intellij.util.ui.components.BorderLayoutPanel
import com.intellij.util.ui.tree.TreeUtil
import com.intellij.util.ui.update.MergingUpdateQueue
import com.intellij.util.ui.update.Update
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.GridBagLayout
import java.util.concurrent.TimeUnit
import javax.swing.DefaultComboBoxModel
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTree
import javax.swing.event.DocumentEvent
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
import javax.swing.event.TreeSelectionListener
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreeSelectionModel
open class WebStarterLibrariesStep(contextProvider: WebStarterContextProvider) : ModuleWizardStep() {
protected val moduleBuilder: WebStarterModuleBuilder = contextProvider.moduleBuilder
private val wizardContext: WizardContext = contextProvider.wizardContext
protected val starterContext: WebStarterContext = contextProvider.starterContext
private val starterSettings: StarterWizardSettings = contextProvider.settings
protected val parentDisposable: Disposable = contextProvider.parentDisposable
private val topLevelPanel: JPanel = BorderLayoutPanel()
private val contentPanel: DialogPanel by lazy { createComponent() }
private val librariesList: CheckboxTreeBase by lazy { createLibrariesList() }
private val librariesSearchField: LibrariesSearchTextField by lazy { createLibrariesFilter() }
private val libraryDescriptionPanel: LibraryDescriptionPanel by lazy { LibraryDescriptionPanel() }
private val selectedLibrariesPanel: SelectedLibrariesPanel by lazy { createSelectedLibrariesPanel() }
private val frameworkVersionsModel: DefaultComboBoxModel<WebStarterFrameworkVersion> = DefaultComboBoxModel()
protected val propertyGraph: PropertyGraph = PropertyGraph()
private val frameworkVersionProperty: GraphProperty<WebStarterFrameworkVersion?> = propertyGraph.property(null)
private val selectedDependencies: MutableSet<WebStarterDependency> = mutableSetOf()
private var currentSearchString: String = ""
private val searchMergingUpdateQueue: MergingUpdateQueue by lazy {
MergingUpdateQueue("SearchLibs_" + moduleBuilder.builderId, 250, true, topLevelPanel, parentDisposable)
}
override fun getComponent(): JComponent = topLevelPanel
override fun getHelpId(): String? = moduleBuilder.getHelpId()
override fun getPreferredFocusedComponent(): JComponent? = librariesSearchField
override fun updateDataModel() {
starterContext.frameworkVersion = frameworkVersionProperty.get()
starterContext.dependencies.clear()
starterContext.dependencies.addAll(selectedDependencies)
}
override fun onStepLeaving() {
super.onStepLeaving()
updateDataModel()
}
override fun _init() {
super._init()
if (topLevelPanel.componentCount == 0) {
topLevelPanel.add(contentPanel, BorderLayout.CENTER)
}
if (topLevelPanel.isDisplayable && topLevelPanel.isShowing) {
// called after unsuccessful validation of step
return
}
// libraries list may depend on options specified on the first step
loadLibrariesList()
loadFrameworkVersions()
updateAvailableDependencies()
getLibrariesRoot()?.let {
selectFirstDependency(it)
}
}
final override fun validate(): Boolean {
val unavailable = selectedDependencies.filter { getDependencyState(it) is DependencyUnavailable }
if (unavailable.isNotEmpty()) {
val dependencyInfo = unavailable.joinToString { it.title }
val version = frameworkVersionProperty.get()?.title ?: ""
Messages.showErrorDialog(
JavaStartersBundle.message("message.unavailable.dependencies", dependencyInfo, version),
JavaStartersBundle.message("message.title.error"))
return false
}
if (!validateFields()) {
return false
}
if (starterContext.result == null) {
// commit selected dependencies to starterContext
updateDataModel()
// try to validate and download result
requestWebService()
if (starterContext.result == null) {
return false
}
}
return true
}
protected open fun validateFields(): Boolean {
return true
}
@RequiresBackgroundThread
protected open fun validateWithServer(progressIndicator: ProgressIndicator): Boolean {
return true
}
private fun requestWebService() {
ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
val progressIndicator = ProgressManager.getInstance().progressIndicator
if (!validateWithServer(progressIndicator)) {
return@runProcessWithProgressSynchronously
}
progressIndicator.checkCanceled()
progressIndicator.text = JavaStartersBundle.message("message.state.downloading.template", moduleBuilder.presentableName)
val downloadResult: DownloadResult? = try {
moduleBuilder.downloadResultInternal(progressIndicator)
}
catch (e: Exception) {
logger<WebStarterLibrariesStep>().info(e)
EdtExecutorService.getScheduledExecutorInstance().schedule(
{
var message = JavaStartersBundle.message("error.text.with.error.content", e.message)
message = StringUtil.shortenTextWithEllipsis(message, 1024, 0) // exactly 1024 because why not
Messages.showErrorDialog(message, moduleBuilder.presentableName)
},
3, TimeUnit.SECONDS)
null
}
starterContext.result = downloadResult
}, JavaStartersBundle.message("message.state.preparing.template"), true, wizardContext.project)
}
private fun loadFrameworkVersions() {
val availableFrameworkVersions = getAvailableFrameworkVersions()
frameworkVersionsModel.removeAllElements()
frameworkVersionsModel.addAll(availableFrameworkVersions)
val defaultVersion = starterContext.frameworkVersion ?: availableFrameworkVersions.firstOrNull()
if (availableFrameworkVersions.contains(defaultVersion)) {
frameworkVersionProperty.set(defaultVersion)
}
else {
frameworkVersionProperty.set(availableFrameworkVersions.firstOrNull())
}
}
protected open fun Panel.addFieldsAfter() {}
private fun createComponent(): DialogPanel {
val messages = starterSettings.customizedMessages
selectedLibrariesPanel.emptyText.text = messages?.noDependenciesSelectedLabel
?: JavaStartersBundle.message("hint.dependencies.not.selected")
return panel {
val frameworkVersions = getAvailableFrameworkVersions()
if (frameworkVersions.isNotEmpty()) {
row(messages?.frameworkVersionLabel ?: JavaStartersBundle.message("title.project.version.label")) {
if (frameworkVersions.size == 1) {
label(frameworkVersions[0].title)
}
else {
frameworkVersionsModel.addListDataListener(object : ListDataListener {
override fun intervalAdded(e: ListDataEvent?) = updateAvailableDependencies()
override fun intervalRemoved(e: ListDataEvent?) = updateAvailableDependencies()
override fun contentsChanged(e: ListDataEvent?) = updateAvailableDependencies()
})
comboBox(frameworkVersionsModel, SimpleListCellRenderer.create("") { it?.title ?: "" })
.bindItem(frameworkVersionProperty)
}
}.bottomGap(BottomGap.SMALL)
}
addFieldsAfter()
row {
label(messages?.dependenciesLabel ?: JavaStartersBundle.message("title.project.dependencies.label"))
.applyToComponent { labelFor = librariesList }
}
row {
cell(JPanel(GridBagLayout()).apply {
add(BorderLayoutPanel().apply {
preferredSize = Dimension(0, 0)
addToTop(librariesSearchField)
addToCenter(ScrollPaneFactory.createScrollPane(librariesList))
}, gridConstraint(0, 0))
add(JPanel(GridBagLayout()).apply {
border = JBUI.Borders.emptyLeft(DEFAULT_HGAP * 2)
preferredSize = Dimension(0, 0)
add(libraryDescriptionPanel.apply {
preferredSize = Dimension(0, 0)
}, gridConstraint(0, 0))
add(BorderLayoutPanel().apply {
preferredSize = Dimension(0, 0)
addToTop(JBLabel(messages?.selectedDependenciesLabel
?: JavaStartersBundle.message("title.project.dependencies.selected.label")).apply {
border = JBUI.Borders.emptyBottom(DEFAULT_VGAP * 2)
})
addToCenter(selectedLibrariesPanel)
}, gridConstraint(0, 1))
}, gridConstraint(1, 0))
}).resizableColumn()
.align(Align.FILL)
}.resizableRow()
}.withVisualPadding()
}
private fun getAvailableFrameworkVersions(): List<WebStarterFrameworkVersion> {
return starterContext.serverOptions.frameworkVersions.filter {
moduleBuilder.isVersionAvailableInternal(it)
}
}
private fun createLibrariesList(): CheckboxTreeBase {
val list = CheckboxTreeBase(object : CheckboxTree.CheckboxTreeCellRenderer() {
override fun customizeRenderer(
tree: JTree,
value: Any,
selected: Boolean,
expanded: Boolean,
leaf: Boolean,
row: Int,
hasFocus: Boolean,
) {
if (value !is DefaultMutableTreeNode) return
this.border = JBUI.Borders.empty(2, 0)
when (val item = value.userObject) {
is WebStarterDependencyCategory -> textRenderer.append(item.title)
is WebStarterDependency -> {
val enabled = (value as CheckedTreeNode).isEnabled
val attributes = if (enabled) SimpleTextAttributes.REGULAR_ATTRIBUTES else SimpleTextAttributes.GRAYED_ATTRIBUTES
textRenderer.append(item.title, attributes)
}
}
}
}, null)
list.emptyText.text = IdeBundle.message("empty.text.nothing.found")
enableEnterKeyHandling(list)
list.rowHeight = 0
list.isRootVisible = false
list.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
list.addCheckboxTreeListener(object : CheckboxTreeListener {
override fun nodeStateChanged(node: CheckedTreeNode) {
val dependency = node.userObject as? WebStarterDependency ?: return
if (node.isChecked) {
selectedDependencies.add(dependency)
}
else {
selectedDependencies.remove(dependency)
}
librariesList.repaint()
selectedLibrariesPanel.update(selectedDependencies)
}
})
list.selectionModel.addTreeSelectionListener(TreeSelectionListener { e ->
val path = e.path
if (path != null && e.isAddedPath) {
when (val item = (path.lastPathComponent as? DefaultMutableTreeNode)?.userObject) {
is WebStarterDependency -> {
updateSelectedLibraryInfo(item)
}
is WebStarterDependencyCategory -> libraryDescriptionPanel.update(item.title, null)
}
}
else {
libraryDescriptionPanel.reset()
}
})
librariesSearchField.list = list
return list
}
private fun isDependencyMatched(item: WebStarterDependency, search: String): Boolean {
return item.title.contains(search, true)
|| (item.description ?: "").contains(search, true)
|| item.id.contains(search, true)
}
private fun createLibrariesFilter(): LibrariesSearchTextField {
val textField = LibrariesSearchTextField()
textField.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
searchMergingUpdateQueue.queue(Update.create("", Runnable {
ModalityUiUtil.invokeLaterIfNeeded(getModalityState(), Runnable {
currentSearchString = textField.text
loadLibrariesList()
librariesList.repaint()
})
}))
}
})
return textField
}
protected open fun getModalityState(): ModalityState {
return ModalityState.stateForComponent(wizardContext.getUserData(AbstractWizard.KEY)!!.contentComponent)
}
protected fun getDisposed(): Condition<Any> = Condition<Any> { Disposer.isDisposed(parentDisposable) }
protected open fun createSelectedLibrariesPanel(): SelectedLibrariesPanel {
val panel = SelectedLibrariesPanel()
val messages = starterSettings.customizedMessages
panel.emptyText.text = messages?.noDependenciesSelectedLabel ?: JavaStartersBundle.message("hint.dependencies.not.selected")
panel.libraryRemoveListener = { libraryInfo ->
selectedDependencies.remove(libraryInfo)
walkCheckedTree(getLibrariesRoot()) {
if (it.userObject == libraryInfo) {
librariesList.setNodeState(it, false)
}
}
selectedLibrariesPanel.update(selectedDependencies)
}
if (starterContext.frameworkVersion != null) {
panel.dependencyStateFunction = { libraryInfo ->
getDependencyState(libraryInfo)
}
}
return panel
}
private fun getLibrariesRoot(): CheckedTreeNode? {
return librariesList.model.root as? CheckedTreeNode
}
private fun getDependencyState(libraryInfo: LibraryInfo): DependencyState {
val frameworkVersion = frameworkVersionProperty.get() ?: return DependencyAvailable
return moduleBuilder.getDependencyStateInternal(frameworkVersion, libraryInfo as WebStarterDependency)
}
private fun loadLibrariesList() {
val librariesRoot = CheckedTreeNode()
val search = currentSearchString.trim()
val dependencyCategories = starterContext.serverOptions.dependencyCategories
for (category in dependencyCategories) {
if (!category.isAvailable(starterContext)) continue
val categoryNode = DefaultMutableTreeNode(category, true)
for (dependency in category.dependencies) {
if (search.isBlank() || isDependencyMatched(dependency, search)) {
val libraryNode = CheckedTreeNode(dependency)
if (dependency.isDefault) {
selectedDependencies.add(dependency)
}
libraryNode.isChecked = selectedDependencies.contains(dependency)
val state = getDependencyState(dependency)
libraryNode.isEnabled = !dependency.isDefault && state is DependencyAvailable
if (dependencyCategories.size > 1) {
categoryNode.add(libraryNode)
}
else {
librariesRoot.add(libraryNode)
}
}
}
if (dependencyCategories.size > 1) {
if (categoryNode.childCount > 0) {
librariesRoot.add(categoryNode)
}
}
}
librariesList.model = DefaultTreeModel(librariesRoot)
if (search.isNotBlank()) {
for (category in librariesRoot.children()) {
librariesList.expandPath(TreeUtil.getPath(librariesRoot, category))
}
selectFirstDependency(librariesRoot)
}
}
private fun selectFirstDependency(librariesRoot: CheckedTreeNode) {
if (librariesRoot.childCount > 0) {
val firstNode = librariesRoot.getChildAt(0)
if (firstNode is CheckedTreeNode) {
librariesList.selectionModel.addSelectionPath(TreeUtil.getPath(librariesRoot, firstNode))
}
else {
librariesList.expandPath(TreeUtil.getPath(librariesRoot, firstNode))
if (firstNode.childCount > 0) {
librariesList.selectionModel.addSelectionPath(TreeUtil.getPath(librariesRoot, firstNode.getChildAt(0)))
}
}
}
}
private fun updateSelectedLibraryInfo(item: WebStarterDependency) {
val dependencyState = getDependencyState(item)
val versionInfo = if (dependencyState is DependencyUnavailable) dependencyState.hint else null
libraryDescriptionPanel.update(item, versionInfo)
}
private fun updateAvailableDependencies() {
selectedLibrariesPanel.update(selectedDependencies)
val root = getLibrariesRoot() ?: return
walkCheckedTree(root) {
val dependency = it.userObject as? WebStarterDependency
if (dependency != null) {
val state = getDependencyState(dependency)
it.isEnabled = !dependency.isDefault && state is DependencyAvailable
}
}
librariesList.repaint()
val selectedDependency = (librariesList.selectionPath?.lastPathComponent as? CheckedTreeNode)?.userObject
if (selectedDependency is WebStarterDependency) {
updateSelectedLibraryInfo(selectedDependency)
}
}
}