[python] PY-79486: (WIP): Front-end improvements:

Remove hardcodes, add filter

GitOrigin-RevId: 09e9b7dad0ac6c9ccfa5554915ee337697b8e1cb
This commit is contained in:
Ilya.Kazakevich
2025-11-05 20:52:34 +00:00
committed by intellij-monorepo-bot
parent 0215915f09
commit 13b58c18a3
11 changed files with 149 additions and 96 deletions
@@ -39,6 +39,7 @@ jvm_library(
"//platform/projectModel-impl",
"//platform/workspace/jps",
"//platform/workspace/storage",
"@lib//:kotlinx-collections-immutable",
]
)
### auto-generated section `build intellij.python.sdkConfigurator.backend` end
@@ -51,5 +51,6 @@
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" />
<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" />
</component>
</module>
@@ -1,4 +1,4 @@
<idea-plugin>
<idea-plugin visibility="private">
<dependencies>
<module name="intellij.python.pyproject"/>
<module name="intellij.python.sdk"/>
@@ -24,6 +24,7 @@ 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 kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -68,7 +69,7 @@ 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())
ModuleDTO(moduleName, createInfo.toolId.id, version, children[moduleName]!!.toList().sorted().toPersistentList())
}
is ModuleCreateInfo.SameAs -> null
}
@@ -1,6 +1,7 @@
package com.intellij.python.sdkConfigurator.common.impl
import com.intellij.openapi.util.NlsSafe
import kotlinx.collections.immutable.ImmutableList
import kotlinx.serialization.Serializable
typealias ModuleName = @NlsSafe String
@@ -15,7 +16,7 @@ data class ModuleDTO(
val name: ModuleName,
val createdByTool: ToolIdDTO,
val existingPyVersion: @NlsSafe String?,
val childModules: List<ModuleName>,
val childModules: ImmutableList<ModuleName>,
)
@Serializable
@@ -1,4 +1,4 @@
<idea-plugin>
<idea-plugin visibility="private">
<dependencies>
<module name="intellij.platform.frontend"/>
<module name="intellij.python.sdkConfigurator.common"/>
@@ -1,7 +1,6 @@
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.project.structure=Project Structure:
python.sdk.configurator.frontend.choose.modules.environment=Environment
python.sdk.configurator.frontend.choose.modules.workspace.member=Same as {0}
python.sdk.configurator.frontend.choose.modules.workspace.existing=Use existing version {0}
python.sdk.configurator.frontend.choose.modules.new=Create new environment
@@ -3,79 +3,65 @@ package com.intellij.python.sdkConfigurator.frontend.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateSet
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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 kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.FlowPreview
import org.jetbrains.annotations.Nls
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
import org.jetbrains.jewel.foundation.theme.JewelTheme
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.theme.dividerStyle
/**
* List of modules from [viewModel]
*/
@OptIn(FlowPreview::class)
@Composable
internal fun ModuleList(
moduleItems: PersistentList<ModuleDTO>,
icons: ImmutableMap<ToolIdDTO, IconKey>,
checked: SnapshotStateSet<String>,
onCheck: (ModuleName) -> Unit,
topLabel: String,
projectStructureLabel: String,
environmentLabel: String,
viewModel: ModulesViewModel,
// UI labels
topLabel: @Nls String,
projectStructureLabel: @Nls String,
environmentLabel: @Nls String,
) {
LaunchedEffect(viewModel) {
viewModel.processFilterUpdates()
}
val space = 5.dp
VerticallyScrollableContainer {
val colPadding = 2.dp
val horizontalArrangement = Arrangement.spacedBy(colPadding)
Column(Modifier.width(IntrinsicSize.Max)
.padding(end = scrollbarContentSafePadding())
.border(Stroke.Alignment.Inside, JewelTheme.dividerStyle.metrics.thickness, JewelTheme.dividerStyle.color)
.padding(5.dp)
val checkboxArrangement = Arrangement.spacedBy(space)
Column(Modifier.width(IntrinsicSize.Max), verticalArrangement = checkboxArrangement) {
Text(text = topLabel, Modifier.padding(end = space, bottom = space))
) {
Text(text = topLabel, maxLines = 1, softWrap = false, modifier = Modifier
.padding(bottom = 10.dp)
)
Divider(Orientation.Horizontal, thickness = 5.dp)
Row {
val modifier = Modifier.weight(1f)
Text(projectStructureLabel, modifier = modifier)
Divider(Orientation.Vertical)
Text(environmentLabel, modifier = modifier)
}
Divider(Orientation.Horizontal)
val hide = Modifier.alpha(0f)
for (module in moduleItems) {
var subModuleOpened by remember { mutableStateOf(false) }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = horizontalArrangement) {
OpenArrow(subModuleOpened, { subModuleOpened = !subModuleOpened },
modifier = if (module.childModules.isEmpty()) hide else Modifier)
Module(module.name in checked, onCheck, colPadding, module, icons)
} // Module children
for (childModule in module.childModules) {
Row(Modifier.alpha(if (subModuleOpened) 1f else 0f)) {
// Invisible elements are for padding only: will replace to the real padding
OpenArrow(false, {}, hide)
Checkbox(false, onCheckedChange = {}, modifier = hide)
CheckboxRow(
childModule,
childModule in checked,
onCheckedChange = {},
softWrap = false,
maxLines = 1,
enabled = false,
)
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) {
ModuleRow(
left = { modifier ->
Row(modifier) {
Text(projectStructureLabel)
Spacer(Modifier.width(1.dp))
TextField(viewModel.moduleFilter, undecorated = true, modifier = Modifier.weight(1f))
}
},
right = { modifier ->
Text(environmentLabel, modifier = modifier)
}
)
}
for (module in viewModel.filteredModules) {
Row(verticalAlignment = Alignment.Top, horizontalArrangement = checkboxArrangement, modifier = Modifier.padding(horizontal = space)) {
Module(module.name in viewModel.checkedModules, viewModel::clicked, module, viewModel.icons, checkboxArrangement)
}
}
}
@@ -83,36 +69,78 @@ internal fun ModuleList(
}
}
/**
* Renders [module]
*/
@Composable
private fun RowScope.Module(
private fun Module(
checked: Boolean,
onCheck: (ModuleName) -> Unit,
columnPadding: Dp,
moduleInfo: ModuleDTO,
module: ModuleDTO,
icons: ImmutableMap<ToolIdDTO, IconKey>,
checkBoxArrangement: Arrangement.HorizontalOrVertical,
) {
val colModifier = Modifier.weight(1f)
var subModuleOpened by remember { mutableStateOf(false) }
val newText = remember { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.new") }
val moduleName = moduleInfo.name
CheckboxRow(
moduleName,
softWrap = false,
maxLines = 1,
checked = checked,
onCheckedChange = {
onCheck(moduleName)
},
modifier = colModifier
)
val moduleName = module.name
val checkBoxWidth = Modifier.padding(start = LocalCheckboxStyle.current.metrics.checkboxSize.width)
Row(colModifier.padding(start = columnPadding)) {
val text = moduleInfo.existingPyVersion?.let { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.workspace.existing", it) }
?: newText
val icon = icons[moduleInfo.createdByTool]
if (icon != null) {
Icon(icon, text)
}
Text(text, Modifier.clickable(onClick = { onCheck(moduleName) }), maxLines = 1, softWrap = false)
Row(verticalAlignment = Alignment.Top, horizontalArrangement = checkBoxArrangement) {
ModuleRow(
left = { modifier ->
Row(modifier) {
// TODO: Get real size instead of "invisible" checkbox
OpenArrow(subModuleOpened, { subModuleOpened = !subModuleOpened },
modifier = if (module.childModules.isEmpty()) {
Modifier.alpha(0f)
}
else Modifier)
Column(verticalArrangement = checkBoxArrangement) {
CheckboxRow(
moduleName,
softWrap = false,
maxLines = 1,
checked = checked,
onCheckedChange = {
onCheck(moduleName)
},
)
if (subModuleOpened) {
for (childModule in module.childModules) {
Row {
CheckboxRow(
childModule,
checked,
onCheckedChange = {},
softWrap = false,
maxLines = 1,
enabled = false,
modifier = checkBoxWidth
)
}
}
}
}
}
},
right = { modifier ->
Row(modifier) {
val text = module.existingPyVersion?.let { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.workspace.existing", it) }
?: newText
val icon = icons[module.createdByTool]
if (icon != null) {
Icon(icon, text)
}
Text(text, Modifier.clickable(onClick = { onCheck(moduleName) }), maxLines = 1, softWrap = false)
}
})
}
}
@Composable
private fun RowScope.ModuleRow(left: @Composable (modifier: Modifier) -> Unit, right: @Composable (modifier: Modifier) -> Unit) {
val modifier = Modifier.weight(1f)
left(modifier)
Divider(Orientation.Vertical, color = JewelTheme.globalColors.borders.normal, thickness = 1.dp, modifier = Modifier.fillMaxHeight())
right(modifier)
}
@@ -15,6 +15,7 @@ import org.jetbrains.jewel.ui.theme.comboBoxStyle
internal fun OpenArrow(opened: Boolean, onOpenChangeState: (Boolean) -> Unit, modifier: Modifier = Modifier) {
var opened by remember { mutableStateOf(opened) }
val rotate = if (opened) 360f else 270f
Icon(key = JewelTheme.comboBoxStyle.icons.chevronDown,
"",
Modifier.rotate(rotate).clickable(onClick = {
@@ -1,6 +1,7 @@
package com.intellij.python.sdkConfigurator.frontend
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateSet
import com.intellij.python.common.tools.ToolId
import com.intellij.python.common.tools.getIcon
@@ -9,27 +10,38 @@ import com.intellij.python.sdkConfigurator.common.impl.ModuleName
import com.intellij.python.sdkConfigurator.common.impl.ModulesDTO
import com.intellij.python.sdkConfigurator.common.impl.ToolIdDTO
import kotlinx.collections.immutable.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.withContext
import org.jetbrains.jewel.bridge.icon.fromPlatformIcon
import org.jetbrains.jewel.ui.icon.IconKey
import org.jetbrains.jewel.ui.icon.IntelliJIconKey
import kotlin.time.Duration.Companion.milliseconds
/**
* UI should display [checkBoxItems] (either enabled or disabled). On each click call [clicked].
* Result can be taken from [checked]
* UI should display [filteredModules] (either enabled or disabled). On each click call [clicked].
* Show filter from [moduleFilter]
* Result can be taken from [checkedModules].
* While composable is displayed. call [processFilterUpdates]
*/
internal class ModulesViewModel(modulesDTO: ModulesDTO) {
val icons: PersistentMap<ToolIdDTO, IconKey> = persistentMapOf(*modulesDTO.modules
val icons: ImmutableMap<ToolIdDTO, IconKey> = persistentMapOf(*modulesDTO.modules
.mapNotNull { module ->
val toolId = module.createdByTool
val icon = getIcon(ToolId(toolId))?.let { IntelliJIconKey.fromPlatformIcon(it.first, it.second) } ?: return@mapNotNull null
Pair(toolId, icon)
}.toTypedArray())
val checkBoxItems: PersistentList<ModuleDTO> = modulesDTO.modules.toPersistentList()
val checked: SnapshotStateSet<ModuleName> = mutableStateSetOf()
private val modules: List<ModuleDTO> = modulesDTO.modules.sortedBy { it.name }
var filteredModules: List<ModuleDTO> by mutableStateOf(modules)
val checkedModules: SnapshotStateSet<ModuleName> = mutableStateSetOf()
val moduleFilter = TextFieldState()
// parent -> children
private val children: ImmutableMap<ModuleName, ImmutableSet<ModuleName>> = checkBoxItems.associate {
private val children: ImmutableMap<ModuleName, ImmutableSet<ModuleName>> = filteredModules.associate {
Pair(it.name, it.childModules.toPersistentSet())
}.toPersistentMap()
@@ -41,13 +53,25 @@ internal class ModulesViewModel(modulesDTO: ModulesDTO) {
fun clicked(module: ModuleName) {
val what = parents[module] ?: module // Get parent if child module
val toChange = setOf(what) + children.getOrDefault(what, emptySet()) // Get children if parent
val checkBoxSet = what !in checked
val checkBoxSet = what !in checkedModules
if (checkBoxSet) {
checked.addAll(toChange)
checkedModules.addAll(toChange)
}
else {
checked.removeAll(toChange)
checkedModules.removeAll(toChange)
}
}
@OptIn(FlowPreview::class)
suspend fun processFilterUpdates() {
snapshotFlow { moduleFilter.text }
.debounce(500.milliseconds)
.collectLatest { filter ->
withContext(Dispatchers.Default) {
val filter = filter.trim()
filteredModules = if (filter.isEmpty()) modules else modules.filter { filter in it.name }
}
}
}
}
@@ -21,7 +21,7 @@ internal suspend fun askUser(project: Project, modules: ModulesDTO, onResult: (S
withContext(Dispatchers.EDT) {
val myDialog = MyDialog(project, viewModel)
if (myDialog.showAndGet()) {
onResult(viewModel.checked)
onResult(viewModel.checkedModules)
}
}
}
@@ -38,10 +38,7 @@ private class MyDialog(project: Project, private val viewModel: ModulesViewModel
override fun createCenterPanel(): JComponent {
enableNewSwingCompositing()
return compose(focusOnClickInside = true, content = {
ModuleList(viewModel.checkBoxItems,
viewModel.icons,
viewModel.checked,
viewModel::clicked,
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"))