[python] PY-79486: (WIP): Improve UI towards the new design:

1. Show module paths
2. Show icons
3. Make window 65% wide

GitOrigin-RevId: 31c1e0e8e7f25bb9d387d9fc6165f7170626d3f0
This commit is contained in:
Ilya.Kazakevich
2025-11-06 05:26:16 +01:00
committed by intellij-monorepo-bot
parent 6326cb84cc
commit 249c8ce892
15 changed files with 177 additions and 57 deletions

View File

@@ -2,15 +2,24 @@
package com.intellij.python.pyproject.model.internal
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.jps.entities.ModuleEntityBuilder
import com.intellij.platform.workspace.jps.entities.ModuleId
import com.intellij.platform.workspace.storage.*
import com.intellij.platform.workspace.storage.EntitySource
import com.intellij.platform.workspace.storage.EntityType
import com.intellij.platform.workspace.storage.GeneratedCodeApiVersion
import com.intellij.platform.workspace.storage.MutableEntityStorage
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.WorkspaceEntityBuilder
import com.intellij.platform.workspace.storage.annotations.Parent
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
import com.intellij.python.common.tools.ToolId
@GeneratedCodeApiVersion(3)
internal interface PyProjectTomlWorkspaceEntityBuilder : WorkspaceEntityBuilder<PyProjectTomlWorkspaceEntity> {
interface PyProjectTomlWorkspaceEntityBuilder : WorkspaceEntityBuilder<PyProjectTomlWorkspaceEntity> {
override var entitySource: EntitySource
var participatedTools: Map<ToolId, ModuleId?>
var dirWithToml: VirtualFileUrl
var module: ModuleEntityBuilder
}
@@ -18,30 +27,33 @@ internal object PyProjectTomlWorkspaceEntityType : EntityType<PyProjectTomlWorks
override val entityClass: Class<PyProjectTomlWorkspaceEntity> get() = PyProjectTomlWorkspaceEntity::class.java
operator fun invoke(
participatedTools: Map<ToolId, ModuleId?>,
dirWithToml: VirtualFileUrl,
entitySource: EntitySource,
init: (PyProjectTomlWorkspaceEntityBuilder.() -> Unit)? = null,
): PyProjectTomlWorkspaceEntityBuilder {
val builder = builder()
builder.participatedTools = participatedTools
builder.dirWithToml = dirWithToml
builder.entitySource = entitySource
init?.invoke(builder)
return builder
}
}
internal fun MutableEntityStorage.modifyPyProjectTomlWorkspaceEntity(
fun MutableEntityStorage.modifyPyProjectTomlWorkspaceEntity(
entity: PyProjectTomlWorkspaceEntity,
modification: PyProjectTomlWorkspaceEntityBuilder.() -> Unit,
): PyProjectTomlWorkspaceEntity = modifyEntity(PyProjectTomlWorkspaceEntityBuilder::class.java, entity, modification)
internal var ModuleEntityBuilder.pyProjectTomlEntity: PyProjectTomlWorkspaceEntityBuilder?
var ModuleEntityBuilder.pyProjectTomlEntity: PyProjectTomlWorkspaceEntityBuilder?
by WorkspaceEntity.extensionBuilder(PyProjectTomlWorkspaceEntity::class.java)
@JvmOverloads
@JvmName("createPyProjectTomlWorkspaceEntity")
internal fun PyProjectTomlWorkspaceEntity(
fun PyProjectTomlWorkspaceEntity(
participatedTools: Map<ToolId, ModuleId?>,
dirWithToml: VirtualFileUrl,
entitySource: EntitySource,
init: (PyProjectTomlWorkspaceEntityBuilder.() -> Unit)? = null,
): PyProjectTomlWorkspaceEntityBuilder = PyProjectTomlWorkspaceEntityType(participatedTools, entitySource, init)
): PyProjectTomlWorkspaceEntityBuilder = PyProjectTomlWorkspaceEntityType(participatedTools, dirWithToml, entitySource, init)

View File

@@ -54,6 +54,11 @@ internal object MetadataStorageImpl : MetadataStorageBase() {
valueType = primitiveTypeStringNotNullable, withDefault = false)),
supertypes = listOf("com.intellij.platform.workspace.storage.SymbolicEntityId")))),
primitive = primitiveTypeMapNotNullable), withDefault = false),
OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "dirWithToml",
valueType = ValueTypeMetadata.SimpleType.CustomType(isNullable = false,
typeMetadata = FinalClassMetadata.KnownClass(
fqName = "com.intellij.platform.workspace.storage.url.VirtualFileUrl")),
withDefault = false),
OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "module",
valueType = ValueTypeMetadata.EntityReference(connectionType = ConnectionId.ConnectionType.ONE_TO_ONE,
entityFqName = "com.intellij.platform.workspace.jps.entities.ModuleEntity",
@@ -70,7 +75,7 @@ internal object MetadataStorageImpl : MetadataStorageBase() {
}
override fun initializeMetadataHash() {
addMetadataHash(typeFqn = "com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity", metadataHash = 1939139269)
addMetadataHash(typeFqn = "com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity", metadataHash = 1371919551)
addMetadataHash(typeFqn = "com.intellij.python.common.tools.ToolId", metadataHash = -1193602517)
addMetadataHash(typeFqn = "com.intellij.platform.workspace.jps.entities.ModuleId", metadataHash = -575206713)
addMetadataHash(typeFqn = "com.intellij.platform.workspace.storage.EntitySource", metadataHash = -1282078904)

View File

@@ -3,7 +3,15 @@ package com.intellij.python.pyproject.model.internal.impl
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.jps.entities.ModuleEntityBuilder
import com.intellij.platform.workspace.jps.entities.ModuleId
import com.intellij.platform.workspace.storage.*
import com.intellij.platform.workspace.storage.ConnectionId
import com.intellij.platform.workspace.storage.EntitySource
import com.intellij.platform.workspace.storage.GeneratedCodeApiVersion
import com.intellij.platform.workspace.storage.GeneratedCodeImplVersion
import com.intellij.platform.workspace.storage.MutableEntityStorage
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.WorkspaceEntityBuilder
import com.intellij.platform.workspace.storage.WorkspaceEntityInternalApi
import com.intellij.platform.workspace.storage.annotations.Parent
import com.intellij.platform.workspace.storage.impl.EntityLink
import com.intellij.platform.workspace.storage.impl.ModifiableWorkspaceEntityBase
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityBase
@@ -14,6 +22,7 @@ import com.intellij.platform.workspace.storage.instrumentation.EntityStorageInst
import com.intellij.platform.workspace.storage.instrumentation.EntityStorageInstrumentationApi
import com.intellij.platform.workspace.storage.instrumentation.MutableEntityStorageInstrumentation
import com.intellij.platform.workspace.storage.metadata.model.EntityMetadata
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
import com.intellij.python.common.tools.ToolId
import com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity
import com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntityBuilder
@@ -40,6 +49,12 @@ internal class PyProjectTomlWorkspaceEntityImpl(private val dataSource: PyProjec
readField("participatedTools")
return dataSource.participatedTools
}
override val dirWithToml: VirtualFileUrl
get() {
readField("dirWithToml")
return dataSource.dirWithToml
}
override val module: ModuleEntity
get() = snapshot.extractOneToOneParent(MODULE_CONNECTION_ID, this)!!
@@ -76,6 +91,7 @@ internal class PyProjectTomlWorkspaceEntityImpl(private val dataSource: PyProjec
// Builder may switch to snapshot at any moment and lock entity data to modification
this.currentEntityData = null
index(this, "dirWithToml", this.dirWithToml)
// Process linked entities that are connected without a builder
processLinkedEntities(builder)
checkInitialization() // TODO uncomment and check failed tests
@@ -89,6 +105,9 @@ internal class PyProjectTomlWorkspaceEntityImpl(private val dataSource: PyProjec
if (!getEntityData().isParticipatedToolsInitialized()) {
error("Field PyProjectTomlWorkspaceEntity#participatedTools should be initialized")
}
if (!getEntityData().isDirWithTomlInitialized()) {
error("Field PyProjectTomlWorkspaceEntity#dirWithToml should be initialized")
}
if (_diff != null) {
if (_diff.extractOneToOneParent<WorkspaceEntityBase>(MODULE_CONNECTION_ID, this) == null) {
error("Field PyProjectTomlWorkspaceEntity#module should be initialized")
@@ -110,6 +129,7 @@ internal class PyProjectTomlWorkspaceEntityImpl(private val dataSource: PyProjec
dataSource as PyProjectTomlWorkspaceEntity
if (this.entitySource != dataSource.entitySource) this.entitySource = dataSource.entitySource
if (this.participatedTools != dataSource.participatedTools) this.participatedTools = dataSource.participatedTools.toMutableMap()
if (this.dirWithToml != dataSource.dirWithToml) this.dirWithToml = dataSource.dirWithToml
updateChildToParentReferences(parents)
}
@@ -131,6 +151,16 @@ internal class PyProjectTomlWorkspaceEntityImpl(private val dataSource: PyProjec
changedProperty.add("participatedTools")
}
override var dirWithToml: VirtualFileUrl
get() = getEntityData().dirWithToml
set(value) {
checkModificationAllowed()
getEntityData(true).dirWithToml = value
changedProperty.add("dirWithToml")
val _diff = diff
if (_diff != null) index(this, "dirWithToml", value)
}
override var module: ModuleEntityBuilder
get() {
val _diff = diff
@@ -174,8 +204,10 @@ internal class PyProjectTomlWorkspaceEntityImpl(private val dataSource: PyProjec
@OptIn(WorkspaceEntityInternalApi::class)
internal class PyProjectTomlWorkspaceEntityData : WorkspaceEntityData<PyProjectTomlWorkspaceEntity>() {
lateinit var participatedTools: Map<ToolId, ModuleId?>
lateinit var dirWithToml: VirtualFileUrl
internal fun isParticipatedToolsInitialized(): Boolean = ::participatedTools.isInitialized
internal fun isDirWithTomlInitialized(): Boolean = ::dirWithToml.isInitialized
override fun wrapAsModifiable(diff: MutableEntityStorage): WorkspaceEntityBuilder<PyProjectTomlWorkspaceEntity> {
val modifiable = PyProjectTomlWorkspaceEntityImpl.Builder(null)
@@ -205,7 +237,7 @@ internal class PyProjectTomlWorkspaceEntityData : WorkspaceEntityData<PyProjectT
}
override fun createDetachedEntity(parents: List<WorkspaceEntityBuilder<*>>): WorkspaceEntityBuilder<*> {
return PyProjectTomlWorkspaceEntity(participatedTools, entitySource) {
return PyProjectTomlWorkspaceEntity(participatedTools, dirWithToml, entitySource) {
parents.filterIsInstance<ModuleEntityBuilder>().singleOrNull()?.let { this.module = it }
}
}
@@ -224,6 +256,7 @@ internal class PyProjectTomlWorkspaceEntityData : WorkspaceEntityData<PyProjectT
if (this.entitySource != other.entitySource) return false
if (this.participatedTools != other.participatedTools) return false
if (this.dirWithToml != other.dirWithToml) return false
return true
}
@@ -234,18 +267,21 @@ internal class PyProjectTomlWorkspaceEntityData : WorkspaceEntityData<PyProjectT
other as PyProjectTomlWorkspaceEntityData
if (this.participatedTools != other.participatedTools) return false
if (this.dirWithToml != other.dirWithToml) return false
return true
}
override fun hashCode(): Int {
var result = entitySource.hashCode()
result = 31 * result + participatedTools.hashCode()
result = 31 * result + dirWithToml.hashCode()
return result
}
override fun hashCodeIgnoringEntitySource(): Int {
var result = javaClass.hashCode()
result = 31 * result + participatedTools.hashCode()
result = 31 * result + dirWithToml.hashCode()
return result
}
}

View File

@@ -3,6 +3,7 @@ package com.intellij.python.pyproject.model.api
import com.intellij.openapi.module.Module
import com.intellij.python.common.tools.ToolId
import com.intellij.python.pyproject.model.internal.suggestSdkImpl
import com.jetbrains.python.venvReader.Directory
sealed interface SuggestedSdk {
@@ -12,9 +13,10 @@ sealed interface SuggestedSdk {
data class SameAs(val parentModule: Module, val accordingTo: ToolId) : SuggestedSdk
/**
* Standalone module. When possible, use one of [preferTools] to configure it
* Standalone module. When possible, use one of [preferTools] to configure it.
* Module's toml file is in [moduleDir]
*/
data class PyProjectIndependent(val preferTools: Set<ToolId>) : SuggestedSdk
data class PyProjectIndependent(val preferTools: Set<ToolId>, val moduleDir: Directory) : SuggestedSdk
}

View File

@@ -4,6 +4,7 @@ import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.jps.entities.ModuleId
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.annotations.Parent
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
import com.intellij.python.common.tools.ToolId
interface PyProjectTomlWorkspaceEntity : WorkspaceEntity {
@@ -11,6 +12,8 @@ interface PyProjectTomlWorkspaceEntity : WorkspaceEntity {
// [tool, probablyWorkspaceRoot?]. If root is null -> tool didn't implement workspace, just participated in this entry creation
val participatedTools: Map<ToolId, ModuleId?>
val dirWithToml: VirtualFileUrl
@Parent
val module: ModuleEntity
}

View File

@@ -1,6 +1,7 @@
package com.intellij.python.pyproject.model.internal
import com.intellij.openapi.module.Module
import com.intellij.platform.backend.workspace.virtualFile
import com.intellij.platform.backend.workspace.workspaceModel
import com.intellij.python.pyproject.model.api.SuggestedSdk
import com.intellij.workspaceModel.ide.legacyBridge.findModule
@@ -21,7 +22,11 @@ internal suspend fun suggestSdkImpl(module: Module): SuggestedSdk? = withContext
}
else {
val tools = entity.participatedTools.keys
SuggestedSdk.PyProjectIndependent(preferTools = tools)
val dirWithToml = entity.dirWithToml
val dirWithTomlPath = (dirWithToml.virtualFile
?: error("Can't find dir for $dirWithToml . Directory might already be deleted. Try to restart IDE")
).toNioPath()
SuggestedSdk.PyProjectIndependent(preferTools = tools, moduleDir = dirWithTomlPath)
}
}

View File

@@ -22,7 +22,10 @@ import com.intellij.platform.workspace.storage.url.VirtualFileUrlManager
import com.intellij.python.common.tools.ToolId
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
import com.intellij.python.pyproject.model.spi.*
import com.intellij.python.pyproject.model.spi.ProjectName
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
import com.intellij.python.pyproject.model.spi.Tool
import com.intellij.python.pyproject.model.spi.WorkspaceName
import com.intellij.util.messages.Topic
import com.jetbrains.python.venvReader.Directory
import kotlinx.coroutines.Dispatchers
@@ -144,7 +147,8 @@ private suspend fun generatePyProjectTomlEntries(files: Map<Path, PyProjectToml>
val namesByDir = entries.associate { Pair(it.root, it.name) }
val allNames = entriesByName.keys
for (tool in Tool.EP.extensionList) {
val (dependencies, workspaceMembers) = tool.getProjectStructure(entriesByName, namesByDir) ?: getProjectStructureDefault(entriesByName, namesByDir)
val (dependencies, workspaceMembers) = tool.getProjectStructure(entriesByName, namesByDir)
?: getProjectStructureDefault(entriesByName, namesByDir)
for ((name, deps) in dependencies) {
val orphanNames = deps - allNames
assert(orphanNames.isEmpty()) { "Tool $tool retuned wrong project names ${orphanNames.joinToString(", ")}" }
@@ -206,7 +210,7 @@ private suspend fun createEntityStorage(
}
}
pyProjectTomlEntity = PyProjectTomlWorkspaceEntity(participatedTools = participatedTools, entitySource)
pyProjectTomlEntity = PyProjectTomlWorkspaceEntity(participatedTools = participatedTools, pyProject.tomlFile.parent.toVirtualFileUrl(fileUrlManager), entitySource)
exModuleOptions = ExternalSystemModuleOptionsEntity(entitySource) {
externalSystem = PYTHON_SOURCE_ROOT_TYPE.name
}

View File

@@ -40,6 +40,7 @@ jvm_library(
"//platform/workspace/jps",
"//platform/workspace/storage",
"@lib//:kotlinx-collections-immutable",
"//platform/eel-provider",
]
)
### auto-generated section `build intellij.python.sdkConfigurator.backend` end

View File

@@ -52,5 +52,6 @@
<orderEntry type="module" module-name="intellij.platform.workspace.jps" />
<orderEntry type="module" module-name="intellij.platform.workspace.storage" />
<orderEntry type="library" name="kotlinx-collections-immutable" level="project" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
</component>
</module>

View File

@@ -4,6 +4,7 @@ import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessModuleDir
import com.intellij.openapi.project.modules
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ModuleRootModificationUtil
@@ -18,12 +19,14 @@ import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.C
import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.popModulesSDKConfigurator
import com.intellij.python.sdkConfigurator.common.impl.ModuleDTO
import com.intellij.python.sdkConfigurator.common.impl.ModuleName
import com.jetbrains.python.PathShorter
import com.jetbrains.python.Result
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.setAssociationToPath
import com.jetbrains.python.venvReader.Directory
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -44,6 +47,7 @@ import kotlinx.coroutines.withContext
internal class ModulesSdkConfigurator private constructor(
private val project: Project,
private val modules: Map<ModuleName, ModuleCreateInfo>,
private val pathShorter: PathShorter,
) {
val modulesDTO: List<ModuleDTO>
@@ -69,22 +73,24 @@ internal class ModulesSdkConfigurator private constructor(
is CreateSdkInfo.ExistingEnv -> r.pythonInfo.languageLevel.toPythonVersion()
is CreateSdkInfo.WillCreateEnv -> null
}
ModuleDTO(moduleName, createInfo.toolId.id, version, children[moduleName]!!.toList().sorted().toPersistentList())
ModuleDTO(moduleName,
path = createInfo.moduleDir?.let { pathShorter.toString(it) },
createdByTool = createInfo.toolId.id,
existingPyVersion = version,
childModules = children[moduleName]!!.toList().sorted().toPersistentList())
}
is ModuleCreateInfo.SameAs -> null
}
}
.sortedBy { it.name }
.toList()
}
companion object {
/**
* Create instance and save in [project]
*/
suspend fun create(project: Project): ModulesSdkConfigurator = ModulesSdkConfigurator(project, getModulesWithoutSDKCreateInfo(project)).also {
suspend fun create(project: Project): ModulesSdkConfigurator = ModulesSdkConfigurator(project, getModulesWithoutSDKCreateInfo(project), PathShorter.create(project)).also {
project.putUserData(key, it)
}
@@ -120,7 +126,7 @@ internal class ModulesSdkConfigurator private constructor(
private val logger = fileLogger()
private sealed interface ModuleCreateInfo {
data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo, val toolId: ToolId) : ModuleCreateInfo
data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo, val toolId: ToolId, val moduleDir: Directory?) : ModuleCreateInfo
data class SameAs(val parentModuleName: ModuleName) : ModuleCreateInfo
}
@@ -132,7 +138,7 @@ internal class ModulesSdkConfigurator private constructor(
tools.firstNotNullOfOrNull { tool ->
val createInfo = (tool.asPyProjectTomlSdkConfigurationExtension()?.createSdkWithoutPyProjectTomlChecks(module)
?: tool.checkEnvironmentAndPrepareSdkCreator(module)) ?: return@firstNotNullOfOrNull null
CreateSdkInfoWithTool(createInfo, tool.toolId).asDTO()
CreateSdkInfoWithTool(createInfo, tool.toolId).asDTO(r.moduleDir)
}
}
is SuggestedSdk.SameAs -> {
@@ -140,10 +146,10 @@ internal class ModulesSdkConfigurator private constructor(
}
null -> null
} // No tools or not pyproject.toml at all? Use EP as a fallback
?: PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull()?.let { CreateSdkInfoWithTool(it.createSdkInfo, it.toolId).asDTO() }
?: PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull()?.let { CreateSdkInfoWithTool(it.createSdkInfo, it.toolId).asDTO(module.guessModuleDir()?.toNioPath()) }
private fun CreateSdkInfoWithTool.asDTO(): ModuleCreateInfo = ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId)
private fun CreateSdkInfoWithTool.asDTO(moduleDir: Directory?): ModuleCreateInfo = ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId, moduleDir)
/**

View File

@@ -6,14 +6,16 @@ import kotlinx.serialization.Serializable
typealias ModuleName = @NlsSafe String
typealias ToolIdDTO = @NlsSafe String // value classes aren't serializable by default
typealias ModulePath = @NlsSafe String? // Path can't be sent to the other OS
/**
* Module and how do we create it
* Module and how do we create it.
* [ModulePath] is a string to be shown to user (might be null if module is not pyproject.toml based)
*/
@Serializable
data class ModuleDTO(
val name: ModuleName,
val path: ModulePath,
val createdByTool: ToolIdDTO,
val existingPyVersion: @NlsSafe String?,
val childModules: ImmutableList<ModuleName>,

View File

@@ -1,6 +1,7 @@
python.sdk.configurator.frontend.choose.modules.title=Configure Modules Environment
python.sdk.configurator.frontend.choose.modules.text=Pick the ones you want to use to run code and set up environment for them
python.sdk.configurator.frontend.choose.modules.project.structure=Project Structure:
python.sdk.configurator.frontend.choose.modules.environment=Environment
python.sdk.configurator.frontend.choose.modules.project.structure=Modules:
python.sdk.configurator.frontend.choose.modules.environment=Environments
python.sdk.configurator.frontend.choose.modules.workspace.existing=Use existing version {0}
python.sdk.configurator.frontend.choose.modules.new=Create new environment
python.sdk.configurator.frontend.choose.modules.configure=Configure

View File

@@ -6,15 +6,17 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.intellij.icons.AllIcons
import com.intellij.python.sdkConfigurator.common.impl.ModuleDTO
import com.intellij.python.sdkConfigurator.common.impl.ModuleName
import com.intellij.python.sdkConfigurator.common.impl.ToolIdDTO
import com.intellij.python.sdkConfigurator.frontend.ModulesViewModel
import com.intellij.python.sdkConfigurator.frontend.PySdkConfiguratorFrontendBundle
import com.intellij.python.sdkConfigurator.frontend.PySdkConfiguratorFrontendBundle.message
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.coroutines.FlowPreview
import org.jetbrains.annotations.Nls
import org.jetbrains.jewel.bridge.icon.fromPlatformIcon
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
import org.jetbrains.jewel.foundation.theme.JewelTheme
@@ -22,30 +24,37 @@ import org.jetbrains.jewel.ui.Orientation
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.component.styling.LocalCheckboxStyle
import org.jetbrains.jewel.ui.icon.IconKey
import org.jetbrains.jewel.ui.icon.IntelliJIconKey
/**
* List of modules from [viewModel]
* List of modules from [viewModel]. Screen sizes are in physical pixels
*/
@OptIn(FlowPreview::class)
@Composable
internal fun ModuleList(
screenWidthPx: Int,
screenHeightPx: Int,
viewModel: ModulesViewModel,
// UI labels
topLabel: @Nls String,
projectStructureLabel: @Nls String,
environmentLabel: @Nls String,
) {
LaunchedEffect(viewModel) {
viewModel.processFilterUpdates()
}
val space = 5.dp
VerticallyScrollableContainer {
val checkboxArrangement = Arrangement.spacedBy(space)
Column(Modifier.width(IntrinsicSize.Max), verticalArrangement = checkboxArrangement) {
Text(text = topLabel, Modifier.padding(end = space, bottom = space))
val topLabel = remember { message("python.sdk.configurator.frontend.choose.modules.text") }
val projectStructureLabel = remember { message("python.sdk.configurator.frontend.choose.modules.project.structure") }
val environmentLabel = remember { message("python.sdk.configurator.frontend.choose.modules.environment") }
Column(Modifier.padding(space).border(Stroke.Alignment.Outside, 1.dp, JewelTheme.globalColors.borders.normal).padding(space).fillMaxWidth(), verticalArrangement = checkboxArrangement) {
Row(Modifier.height(IntrinsicSize.Min).padding(start = space, bottom = space, end = space), horizontalArrangement = checkboxArrangement) {
val (width, height) = with(LocalDensity.current) {
// width: 50% of screen, height: 65% of the screen (according to Lena)
Pair(screenWidthPx.toDp() * 0.5f, screenHeightPx.toDp() * 0.65f)
}
val border = Modifier.border(Stroke.Alignment.Outside, 1.dp, JewelTheme.globalColors.borders.normal)
val space = 5.dp
VerticallyScrollableContainer(Modifier.padding(space).then(border).size(width = width, height = height)) {
val checkboxArrangement = Arrangement.spacedBy(space)
Column(Modifier.fillMaxSize(), verticalArrangement = checkboxArrangement) {
Text(text = topLabel, Modifier.padding(space))
Column(Modifier.fillMaxSize(), verticalArrangement = checkboxArrangement) {
Row(Modifier.fillMaxSize().then(border).padding(space), horizontalArrangement = checkboxArrangement) {
ModuleRow(
left = { modifier ->
Row(modifier) {
@@ -56,7 +65,7 @@ internal fun ModuleList(
},
right = { modifier ->
Text(environmentLabel, modifier = modifier)
}
},
)
}
for (module in viewModel.filteredModules) {
@@ -81,9 +90,10 @@ private fun Module(
checkBoxArrangement: Arrangement.HorizontalOrVertical,
) {
var subModuleOpened by remember { mutableStateOf(false) }
val newText = remember { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.new") }
val newText = remember { message("python.sdk.configurator.frontend.choose.modules.new") }
val moduleName = module.name
val checkBoxWidth = Modifier.padding(start = LocalCheckboxStyle.current.metrics.checkboxSize.width)
val moduleIcon = remember { IntelliJIconKey.fromPlatformIcon(AllIcons.Nodes.Module) }
Row(verticalAlignment = Alignment.Top, horizontalArrangement = checkBoxArrangement) {
ModuleRow(
@@ -97,13 +107,19 @@ private fun Module(
else Modifier)
Column(verticalArrangement = checkBoxArrangement) {
CheckboxRow(
moduleName,
softWrap = false,
maxLines = 1,
checked = checked,
onCheckedChange = {
onCheck(moduleName)
},
content = {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(moduleIcon, module.path)
Text(moduleName, softWrap = false, maxLines = 1)
module.path?.let { path ->
InfoText(path, softWrap = false, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
)
if (subModuleOpened) {
for (childModule in module.childModules) {
@@ -124,8 +140,8 @@ private fun Module(
}
},
right = { modifier ->
Row(modifier) {
val text = module.existingPyVersion?.let { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.workspace.existing", it) }
Row(modifier, horizontalArrangement = checkBoxArrangement) {
val text = module.existingPyVersion?.let { message("python.sdk.configurator.frontend.choose.modules.workspace.existing", it) }
?: newText
val icon = icons[module.createdByTool]
if (icon != null) {
@@ -143,4 +159,4 @@ private fun RowScope.ModuleRow(left: @Composable (modifier: Modifier) -> Unit, r
left(modifier)
Divider(Orientation.Vertical, color = JewelTheme.globalColors.borders.normal, thickness = 1.dp, modifier = Modifier.fillMaxHeight())
right(modifier)
}
}

View File

@@ -27,6 +27,13 @@ import kotlin.time.Duration.Companion.milliseconds
* While composable is displayed. call [processFilterUpdates]
*/
internal class ModulesViewModel(modulesDTO: ModulesDTO) {
// To be called when "ok" button should be enabled
@Volatile
var okButtonEnabledListener: ((enabled: Boolean) -> Unit)? = null
set(value) {
field = value
callEnabledButtonListener() // Set value as soon as set
}
val icons: ImmutableMap<ToolIdDTO, IconKey> = persistentMapOf(*modulesDTO.modules
.mapNotNull { module ->
val toolId = module.createdByTool
@@ -60,6 +67,13 @@ internal class ModulesViewModel(modulesDTO: ModulesDTO) {
else {
checkedModules.removeAll(toChange)
}
callEnabledButtonListener()
}
private fun callEnabledButtonListener() {
this.okButtonEnabledListener?.let { listener ->
listener(checkedModules.isNotEmpty())
}
}
@OptIn(FlowPreview::class)

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
import org.jetbrains.jewel.bridge.compose
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.enableNewSwingCompositing
import java.awt.GraphicsEnvironment
import javax.swing.JComponent
internal suspend fun askUser(project: Project, modules: ModulesDTO, onResult: (Set<ModuleName>) -> Unit) {
@@ -31,17 +32,28 @@ private class MyDialog(project: Project, private val viewModel: ModulesViewModel
init {
title = message("python.sdk.configurator.frontend.choose.modules.title")
isResizable = false
setOKButtonText(message("python.sdk.configurator.frontend.choose.modules.configure"))
init()
}
override fun dispose() {
super.dispose()
viewModel.okButtonEnabledListener = null
}
@OptIn(ExperimentalJewelApi::class)
override fun createCenterPanel(): JComponent {
enableNewSwingCompositing()
return compose(focusOnClickInside = true, content = {
ModuleList(viewModel,
topLabel = message("python.sdk.configurator.frontend.choose.modules.text"),
projectStructureLabel = message("python.sdk.configurator.frontend.choose.modules.project.structure"),
environmentLabel = message("python.sdk.configurator.frontend.choose.modules.environment"))
})
viewModel.okButtonEnabledListener = { enabled ->
isOKActionEnabled = enabled // To enable/disable "OK" button
}
val screenSize = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.displayMode
return compose(focusOnClickInside = true) {
ModuleList(
screenWidthPx = screenSize.width,
screenHeightPx = screenSize.height,
viewModel = viewModel
)
}
}
}