diff --git a/python/python-sdk-configurator/backend/BUILD.bazel b/python/python-sdk-configurator/backend/BUILD.bazel
index 6ab1b7c7ed20..791ade747eec 100644
--- a/python/python-sdk-configurator/backend/BUILD.bazel
+++ b/python/python-sdk-configurator/backend/BUILD.bazel
@@ -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
\ No newline at end of file
diff --git a/python/python-sdk-configurator/backend/intellij.python.sdkConfigurator.backend.iml b/python/python-sdk-configurator/backend/intellij.python.sdkConfigurator.backend.iml
index c64a74de6337..b5c4b1008a39 100644
--- a/python/python-sdk-configurator/backend/intellij.python.sdkConfigurator.backend.iml
+++ b/python/python-sdk-configurator/backend/intellij.python.sdkConfigurator.backend.iml
@@ -51,5 +51,6 @@
+
\ No newline at end of file
diff --git a/python/python-sdk-configurator/backend/resources/intellij.python.sdkConfigurator.backend.xml b/python/python-sdk-configurator/backend/resources/intellij.python.sdkConfigurator.backend.xml
index a40a3629c62d..c334c2adea32 100644
--- a/python/python-sdk-configurator/backend/resources/intellij.python.sdkConfigurator.backend.xml
+++ b/python/python-sdk-configurator/backend/resources/intellij.python.sdkConfigurator.backend.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt
index 454c61936cd8..00e20a417cef 100644
--- a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt
+++ b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt
@@ -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
}
diff --git a/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt b/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt
index 944204deb125..122ca03be844 100644
--- a/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt
+++ b/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt
@@ -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,
+ val childModules: ImmutableList,
)
@Serializable
diff --git a/python/python-sdk-configurator/frontend/resources/intellij.python.sdkConfigurator.frontend.xml b/python/python-sdk-configurator/frontend/resources/intellij.python.sdkConfigurator.frontend.xml
index aee2f07687da..59e41e772185 100644
--- a/python/python-sdk-configurator/frontend/resources/intellij.python.sdkConfigurator.frontend.xml
+++ b/python/python-sdk-configurator/frontend/resources/intellij.python.sdkConfigurator.frontend.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties b/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties
index fbc30c7f8b5b..6b48342713fe 100644
--- a/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties
+++ b/python/python-sdk-configurator/frontend/resources/messages/PySdkConfiguratorFrontendBundle.properties
@@ -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
diff --git a/python/python-sdk-configurator/frontend/src/components/ModuleList.kt b/python/python-sdk-configurator/frontend/src/components/ModuleList.kt
index 3b896b3b555c..c953758be9a0 100644
--- a/python/python-sdk-configurator/frontend/src/components/ModuleList.kt
+++ b/python/python-sdk-configurator/frontend/src/components/ModuleList.kt
@@ -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,
- icons: ImmutableMap,
- checked: SnapshotStateSet,
- 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,
+ 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)
+}
\ No newline at end of file
diff --git a/python/python-sdk-configurator/frontend/src/components/OpenArrow.kt b/python/python-sdk-configurator/frontend/src/components/OpenArrow.kt
index 76d10fb3b80e..8321b7c60cd5 100644
--- a/python/python-sdk-configurator/frontend/src/components/OpenArrow.kt
+++ b/python/python-sdk-configurator/frontend/src/components/OpenArrow.kt
@@ -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 = {
diff --git a/python/python-sdk-configurator/frontend/src/frontendLib.kt b/python/python-sdk-configurator/frontend/src/frontendLib.kt
index 5ac24f91e4fa..e80de62a8693 100644
--- a/python/python-sdk-configurator/frontend/src/frontendLib.kt
+++ b/python/python-sdk-configurator/frontend/src/frontendLib.kt
@@ -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 = persistentMapOf(*modulesDTO.modules
+ val icons: ImmutableMap = 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 = modulesDTO.modules.toPersistentList()
- val checked: SnapshotStateSet = mutableStateSetOf()
+ private val modules: List = modulesDTO.modules.sortedBy { it.name }
+
+ var filteredModules: List by mutableStateOf(modules)
+ val checkedModules: SnapshotStateSet = mutableStateSetOf()
+ val moduleFilter = TextFieldState()
// parent -> children
- private val children: ImmutableMap> = checkBoxItems.associate {
+ private val children: ImmutableMap> = 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 }
+ }
+ }
+ }
}
diff --git a/python/python-sdk-configurator/frontend/src/swingBridge.kt b/python/python-sdk-configurator/frontend/src/swingBridge.kt
index 2a646cfade57..04b57cbc8852 100644
--- a/python/python-sdk-configurator/frontend/src/swingBridge.kt
+++ b/python/python-sdk-configurator/frontend/src/swingBridge.kt
@@ -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"))