From 2ed2f281a4bcde4ce84c6278f0e812d09c492e2d Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Mon, 1 Dec 2025 23:53:57 +0100 Subject: [PATCH] [python] PY-79486: (WIP): Fix "search module" field, add "select all" checkbox. UI arranged with the sketch GitOrigin-RevId: cf0167b06757767f271ae4d33572ef2a42e16f41 --- ...PySdkConfiguratorFrontendBundle.properties | 5 +- .../frontend/src/components/ModuleList.kt | 51 ++++++++++--------- .../frontend/src/frontendLib.kt | 47 ++++++++++++----- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties b/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties index 0897220d7a30..9f5e28e0fa7c 100644 --- a/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties +++ b/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties @@ -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 diff --git a/python/python-sdk-configurator/frontend/src/components/ModuleList.kt b/python/python-sdk-configurator/frontend/src/components/ModuleList.kt index f1065f535796..bdd77d1f8bcf 100644 --- a/python/python-sdk-configurator/frontend/src/components/ModuleList.kt +++ b/python/python-sdk-configurator/frontend/src/components/ModuleList.kt @@ -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()) { diff --git a/python/python-sdk-configurator/frontend/src/frontendLib.kt b/python/python-sdk-configurator/frontend/src/frontendLib.kt index bc5c982572be..0da4965b501a 100644 --- a/python/python-sdk-configurator/frontend/src/frontendLib.kt +++ b/python/python-sdk-configurator/frontend/src/frontendLib.kt @@ -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 = modulesDTO.modules.sortedBy { it.name } + private val parentModules: List = modulesDTO.modules.sortedBy { it.name } + private val parentModuleNames: Set = parentModules.map { it.name }.toSet() - var filteredModules: List by mutableStateOf(modules) + var selectAllState: ToggleableState by mutableStateOf(ToggleableState.Off) + var filteredParentModules: List by mutableStateOf(parentModules) val checkedModules: SnapshotStateSet = mutableStateSetOf() val moduleFilter = TextFieldState() // parent -> children - private val children: ImmutableMap> = filteredModules.associate { + private val children: ImmutableMap> = filteredParentModules.associate { Pair(it.name, it.childModules.toPersistentSet()) }.toPersistentMap() + private val parentOnlyCheckedModules: Set get() = parentModuleNames.intersect(checkedModules) + // child -> parent private val parents: ImmutableMap = 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 } } } }