[python] PY-79486: (WIP): Fix "search module" field, add "select all" checkbox.

UI arranged with the sketch

GitOrigin-RevId: cf0167b06757767f271ae4d33572ef2a42e16f41
This commit is contained in:
Ilya.Kazakevich
2025-12-02 03:45:33 +00:00
committed by intellij-monorepo-bot
parent 01ccd0b87f
commit 2ed2f281a4
3 changed files with 66 additions and 37 deletions
@@ -1,7 +1,8 @@
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=Modules:
python.sdk.configurator.frontend.choose.modules.environment=Environments
python.sdk.configurator.frontend.choose.modules.project.search=Search
python.sdk.configurator.frontend.choose.modules.select.all=Select All
python.sdk.configurator.frontend.choose.modules.environment=Environment
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
@@ -24,6 +24,7 @@ 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
import org.jetbrains.jewel.ui.icons.AllIconsKeys
/**
* List of modules from [viewModel]. Screen sizes are in physical pixels
@@ -36,36 +37,40 @@ internal fun ModuleList(
viewModel.processFilterUpdates()
}
val topLabel = remember { message("python.sdk.configurator.frontend.choose.modules.text") }
val projectStructureLabel = remember { message("python.sdk.configurator.frontend.choose.modules.project.structure") }
val searchTitle = remember { message("python.sdk.configurator.frontend.choose.modules.project.search") }
val environmentLabel = remember { message("python.sdk.configurator.frontend.choose.modules.environment") }
val selectAllLabel = remember { message("python.sdk.configurator.frontend.choose.modules.select.all") }
val border = Modifier.border(Stroke.Alignment.Outside, 1.dp, JewelTheme.globalColors.borders.normal)
val space = 5.dp
VerticallyScrollableContainer(Modifier.padding(space).then(border).fillMaxSize()) {
val checkboxArrangement = Arrangement.spacedBy(space)
Column(Modifier.fillMaxSize(), verticalArrangement = checkboxArrangement) {
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) {
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) {
key(module.name) {
Row(verticalAlignment = Alignment.Top, horizontalArrangement = checkboxArrangement, modifier = Modifier.padding(horizontal = space)) {
Module(module.name in viewModel.checkedModules, viewModel::clicked, module, viewModel.icons, checkboxArrangement)
Row(Modifier.fillMaxSize().then(border).padding(space), horizontalArrangement = checkBoxArrangement, verticalAlignment = Alignment.CenterVertically) {
ModuleRow(
left = { modifier ->
Row(modifier) {
TextField(viewModel.moduleFilter, undecorated = false, modifier = Modifier.weight(1f), leadingIcon = { Icon(AllIconsKeys.Actions.Find, searchTitle) })
}
},
right = { modifier ->
Text(environmentLabel, modifier = modifier)
},
)
}
Column(Modifier.fillMaxSize(), verticalArrangement = checkBoxArrangement) {
Row(verticalAlignment = Alignment.Top) {
// TODO: Get real size instead of "invisible" checkbox
OpenArrow(false, {}, Modifier.alpha(0.0f))
TriStateCheckboxRow(selectAllLabel, viewModel.selectAllState, viewModel::selectAllClicked)
Spacer(Modifier.weight(1f))
}
for (module in viewModel.filteredParentModules) {
key(module.name) {
Module(module.name in viewModel.checkedModules, viewModel::moduleClicked, module, viewModel.icons, checkBoxArrangement)
}
}
}
@@ -93,7 +98,7 @@ private fun Module(
Row(verticalAlignment = Alignment.Top, horizontalArrangement = checkBoxArrangement) {
ModuleRow(
left = { modifier ->
Row(modifier) {
Row(modifier, verticalAlignment = if (subModuleOpened) Alignment.Top else Alignment.CenterVertically) {
// TODO: Get real size instead of "invisible" checkbox
OpenArrow(subModuleOpened, { subModuleOpened = !subModuleOpened },
modifier = if (module.childModules.isEmpty()) {
@@ -3,6 +3,7 @@ package com.intellij.python.sdkConfigurator.frontend
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateSet
import androidx.compose.ui.state.ToggleableState
import com.intellij.python.common.tools.ToolId
import com.intellij.python.common.tools.getIcon
import com.intellij.python.sdkConfigurator.common.impl.ModuleDTO
@@ -21,7 +22,7 @@ import org.jetbrains.jewel.ui.icon.IntelliJIconKey
import kotlin.time.Duration.Companion.milliseconds
/**
* UI should display [filteredModules] (either enabled or disabled). On each click call [clicked].
* UI should display [filteredParentModules] (either enabled or disabled). On each click call [moduleClicked].
* Show filter from [moduleFilter]
* Result can be taken from [checkedModules].
* While composable is displayed. call [processFilterUpdates]
@@ -41,33 +42,55 @@ internal class ModulesViewModel(modulesDTO: ModulesDTO) {
Pair(toolId, icon)
}.toTypedArray())
private val modules: List<ModuleDTO> = modulesDTO.modules.sortedBy { it.name }
private val parentModules: List<ModuleDTO> = modulesDTO.modules.sortedBy { it.name }
private val parentModuleNames: Set<ModuleName> = parentModules.map { it.name }.toSet()
var filteredModules: List<ModuleDTO> by mutableStateOf(modules)
var selectAllState: ToggleableState by mutableStateOf(ToggleableState.Off)
var filteredParentModules: List<ModuleDTO> by mutableStateOf(parentModules)
val checkedModules: SnapshotStateSet<ModuleName> = mutableStateSetOf()
val moduleFilter = TextFieldState()
// parent -> children
private val children: ImmutableMap<ModuleName, ImmutableSet<ModuleName>> = filteredModules.associate {
private val children: ImmutableMap<ModuleName, ImmutableSet<ModuleName>> = filteredParentModules.associate {
Pair(it.name, it.childModules.toPersistentSet())
}.toPersistentMap()
private val parentOnlyCheckedModules: Set<ModuleName> get() = parentModuleNames.intersect(checkedModules)
// child -> parent
private val parents: ImmutableMap<ModuleName, ModuleName> = children.flatMap { (parent, children) ->
children.map { Pair(it, parent) }
}.toMap().toImmutableMap()
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 checkedModules
if (checkBoxSet) {
checkedModules.addAll(toChange)
fun selectAllClicked() {
val checked = when (selectAllState) {
ToggleableState.On -> false
ToggleableState.Off, ToggleableState.Indeterminate -> true
}
setParentModules(checked = checked, parentModulesToSet = parentModuleNames.toTypedArray())
}
fun moduleClicked(module: ModuleName) {
val parentModule = parents[module] ?: module // Get parent if child module
val alreadyChecked = parentModule in checkedModules
setParentModules(checked = !alreadyChecked, parentModule)
}
private fun setParentModules(checked: Boolean, vararg parentModulesToSet: ModuleName) {
val parentModulesToSet = parentModulesToSet.toSet()
val parentsAndChildrenModules = parentModulesToSet + parentModulesToSet.flatMap { children.getOrDefault(it, emptySet()) } // Get children if parent
if (checked) {
checkedModules.addAll(parentsAndChildrenModules)
}
else {
checkedModules.removeAll(toChange)
checkedModules.removeAll(parentsAndChildrenModules)
}
callEnabledButtonListener()
selectAllState = when (parentOnlyCheckedModules.size) {
0 -> ToggleableState.Off
parentModules.size -> ToggleableState.On // All parent modules are checked
else -> ToggleableState.Indeterminate
}
}
private fun callEnabledButtonListener() {
@@ -83,7 +106,7 @@ internal class ModulesViewModel(modulesDTO: ModulesDTO) {
.collectLatest { filter ->
withContext(Dispatchers.Default) {
val filter = filter.trim()
filteredModules = if (filter.isEmpty()) modules else modules.filter { filter in it.name }
filteredParentModules = if (filter.isEmpty()) parentModules else parentModules.filter { filter in it.name }
}
}
}