[python] install python from NPW (PY-60214, PY-63083)

- python distributions are available for selection as base interpreter
- sdks are installed and registered with modal progress after creating base dir for project
- new ui for sdk combobox, supporting path selection and custom rendering
- same combobox used for all python environment selection

GitOrigin-RevId: de2ea95247eab93f17f94cc580a41b13471bb49f
This commit is contained in:
Aleksei Kniazev
2023-11-29 03:20:14 +00:00
committed by intellij-monorepo-bot
parent 295938dc46
commit 2222b97280
12 changed files with 262 additions and 130 deletions
@@ -514,7 +514,7 @@ sdk.create.custom.venv.select.existing.link=Select existing
sdk.create.not.executable.empty.error=Specify path to executable
sdk.create.not.executable.does.not.exist.error=Executable does not exist
sdk.create.executable.directory.error=Path can't be a directory
sdk.create.tooltip.browse=Browse\u2026
sdk.create.targets.local=Local Machine
@@ -524,6 +524,9 @@ sdk.create.custom.pipenv=Pipenv
sdk.create.custom.poetry=Poetry
sdk.create.custom.python=Python
sdk.rendering.detected.grey.text=detected in the system
sdk.rendering.installable.grey.text=download and install
new.project.name=Name:
new.project.location=Location:
new.project.location.hint=Project will be created in: {0}
@@ -13,6 +13,7 @@ import com.intellij.webcore.packaging.PackageManagementService
import com.intellij.webcore.packaging.PackagesNotificationPanel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.installer.*
import java.util.*
@@ -132,7 +133,12 @@ object PySdkToInstallManager {
.also { sdks ->
LOGGER.debug { sdks.joinToString(prefix = "Detected system-wide sdks: ") { it.homePath ?: it.name } }
}
.filter { languageLevel?.equals(PySdkUtil.getLanguageLevelForSdk(it)) ?: true }
.filter {
val detectedLevel = PythonSdkFlavor.getFlavor(it)?.let { flavor ->
flavor.getLanguageLevelFromVersionString(flavor.getVersionString(it.homePath!!))
}
languageLevel?.equals(detectedLevel) ?: true
}
.also {
PySdkToInstallCollector.logSdkLookup(
project,
@@ -29,18 +29,18 @@ import kotlinx.coroutines.withContext
class PipEnvNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) {
private val executable = propertyGraph.property(UNKNOWN_EXECUTABLE)
private val basePythonVersion = propertyGraph.property<Sdk?>(initial = null)
private val basePythonHomePath = basePythonVersion.transformToHomePathProperty(state.basePythonSdks)
private lateinit var pipEnvPathField: TextFieldWithBrowseButton
private lateinit var basePythonComboBox: ComboBox<String?>
private lateinit var basePythonComboBox: ComboBox<Sdk?>
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox =
pythonBaseInterpreterComboBox(presenter, presenter.basePythonSdksFlow, presenter.detectingSdks, basePythonHomePath,
presenter::addBasePythonInterpreter)
.align(Align.FILL)
.component
basePythonComboBox = pythonInterpreterComboBox(basePythonVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
.align(Align.FILL)
.component
}
pipEnvPathField = executableSelector(executable,
@@ -70,7 +70,7 @@ class PipEnvNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Py
override fun getOrCreateSdk(): Sdk {
PropertiesComponent.getInstance().pipEnvPath = pipEnvPathField.text.nullize()
val baseSdk = setupSdkIfDetected(basePythonVersion.get()!!, state.allSdks.get())
val baseSdk = setupBaseSdk(basePythonVersion.get()!!, state.allSdks.get())
val newSdk = setupPipEnvSdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(),
baseSdk.homePath, false)!!
SdkConfigurationUtil.addSdk(newSdk)
@@ -29,14 +29,14 @@ class PoetryNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Py
val executable = propertyGraph.property(UNKNOWN_EXECUTABLE)
private val basePythonVersion = propertyGraph.property<Sdk?>(initial = null)
private val basePythonHomePath = basePythonVersion.transformToHomePathProperty(state.basePythonSdks)
private lateinit var basePythonComboBox: ComboBox<String?>
private lateinit var basePythonComboBox: ComboBox<Sdk?>
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox =
pythonBaseInterpreterComboBox(presenter, presenter.basePythonSdksFlow, presenter.detectingSdks, basePythonHomePath,
presenter::addBasePythonInterpreter)
basePythonComboBox = pythonInterpreterComboBox(basePythonVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
.align(Align.FILL)
.component
}
@@ -67,7 +67,7 @@ class PoetryNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Py
override fun getOrCreateSdk(): Sdk {
PropertiesComponent.getInstance().poetryPath = executable.get().nullize()
val baseSdk = setupSdkIfDetected(basePythonVersion.get()!!, state.allSdks.get())
val baseSdk = setupBaseSdk(basePythonVersion.get()!!, state.allSdks.get())
val newSdk = setupPoetrySdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(),
baseSdk.homePath, false)!!
SdkConfigurationUtil.addSdk(newSdk)
@@ -15,18 +15,17 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.flow.mapStateIn
import com.intellij.util.text.nullize
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.PyDetectedSdk
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.LocalContext
import com.jetbrains.python.sdk.add.ProjectLocationContext
import com.jetbrains.python.sdk.add.ProjectLocationContexts
import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath
import com.jetbrains.python.sdk.add.target.createDetectedSdk
import com.jetbrains.python.sdk.configuration.createVirtualEnvSynchronously
import com.jetbrains.python.sdk.detectSystemWideSdksSuspended
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.prepareSdkList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.nio.file.InvalidPathException
@@ -46,7 +45,7 @@ internal fun PythonAddInterpreterPresenter.getPathOnTarget(path: Path): @NlsSafe
internal fun PythonAddInterpreterPresenter.setupVirtualenv(venvPath: Path, projectPath: String, baseSdk: Sdk): Sdk {
val venvPathOnTarget = getPathOnTarget(venvPath)
val savedSdk = setupSdkIfDetected(baseSdk, state.allSdks.get())
val savedSdk = setupBaseSdk(baseSdk, state.allSdks.get())
val sdk = createVirtualEnvSynchronously(savedSdk, state.allSdks.get(), venvPathOnTarget,
projectPath, null, null) ?: error("Failed to create SDK")
SdkConfigurationUtil.addSdk(sdk)
@@ -80,6 +79,8 @@ class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val ui
private val _detectingCondaExecutable = MutableStateFlow(value = false)
val detectingCondaExecutable: StateFlow<Boolean> = _detectingCondaExecutable.asStateFlow()
private val _sdksToInstall: MutableStateFlow<List<Sdk>> = MutableStateFlow(emptyList())
val navigator = PythonNewEnvironmentDialogNavigator()
/**
@@ -125,9 +126,10 @@ class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val ui
.stateIn(scope + uiContext, started = SharingStarted.Lazily, initialValue = emptyList())
val basePythonSdksFlow: StateFlow<List<Sdk>> =
combine(_allExistingSdksFlow, manuallyAddedBaseSdksFlow, detectedSdksFlow) { existingSdks, addedBaseSdks, (context, detectedSdks) ->
combine(_allExistingSdksFlow, manuallyAddedBaseSdksFlow, detectedSdksFlow, _sdksToInstall) { existingSdks, addedBaseSdks, (context, detectedSdks), sdkListToInstall ->
val sdkList = addedBaseSdks + withContext(Dispatchers.IO) {
prepareSdkList(detectedSdks, existingSdks, context.targetEnvironmentConfiguration)
val baseSdks = prepareSdkList(detectedSdks, existingSdks, context.targetEnvironmentConfiguration)
baseSdks + filterInstallableSdks(sdkListToInstall, baseSdks)
}
state.basePythonSdks.set(sdkList)
sdkList
@@ -135,6 +137,15 @@ class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val ui
.logException(LOG)
.stateIn(scope + uiContext, started = SharingStarted.Lazily, initialValue = emptyList())
private fun filterInstallableSdks(sdkListToInstall: List<Sdk>, sdkList: List<Sdk>): List<Sdk> {
val languageLevels = sdkList.map { PySdkUtil.getLanguageLevelForSdk(it) }
return sdkListToInstall
.map { LanguageLevel.fromPythonVersion(it.versionString) to it }
.filter { it.first !in languageLevels }
.sortedByDescending { it.first }
.map { it.second }
}
val targetEnvironmentConfiguration: TargetEnvironmentConfiguration?
get() = projectLocationContext.targetEnvironmentConfiguration
var baseConda: PyCondaEnv? = null
@@ -149,6 +160,7 @@ class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val ui
}
state.allExistingSdks.afterChange { _allExistingSdksFlow.tryEmit(it) }
state.installableSdks.afterChange { _sdksToInstall.tryEmit(it) }
state.condaExecutable.afterChange {
if (!it.startsWith("<")) _currentCondaExecutableFlow.tryEmit(it.tryConvertToPath()) // skip possible <unknown_executable>
}
@@ -13,6 +13,7 @@ class PythonAddInterpreterState(
val scope: CoroutineScope,
val basePythonSdks: ObservableMutableProperty<List<Sdk>>,
val allExistingSdks: ObservableMutableProperty<List<Sdk>>,
val installableSdks: ObservableMutableProperty<List<Sdk>>,
val selectedVenv: ObservableMutableProperty<Sdk?>,
val condaExecutable: ObservableMutableProperty<String>,
) {
@@ -19,6 +19,7 @@ import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.*
import com.jetbrains.python.sdk.getSdksToInstall
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
@@ -38,6 +39,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>)
private val allExistingSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val basePythonSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val installableSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val pythonBaseVersion = propertyGraph.property<Sdk?>(null)
private val selectedVenv = propertyGraph.property<Sdk?>(null)
@@ -58,6 +60,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>)
service<PythonAddSdkService>().coroutineScope,
basePythonSdks,
allExistingSdks,
installableSdks,
selectedVenv,
condaExecutable)
@@ -78,11 +81,10 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>)
}.topGap(TopGap.MEDIUM)
row(message("sdk.create.python.version")) {
pythonBaseVersionComboBox = nonEditablePythonInterpreterComboBox(presenter.basePythonSdksFlow, scope = presenter.state.scope,
uiContext = presenter.uiContext)
.bindItem(pythonBaseVersion)
.displayLoaderWhen(presenter.detectingSdks, makeTemporaryEditable = true, scope = presenter.scope,
uiContext = presenter.uiContext)
pythonBaseVersionComboBox = pythonInterpreterComboBox(pythonBaseVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
.align(AlignX.FILL)
.component
}.visibleIf(_projectVenv)
@@ -118,6 +120,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>)
ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks)
}
allExistingSdks.set(allValidSdks)
installableSdks.set(getSdksToInstall())
updateVenvLocationHint()
}
@@ -126,7 +129,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>)
}
}
fun getSdk(): Sdk? {
fun getSdk(): Sdk {
presenter.navigator.saveLastState()
return when (selectedMode.get()) {
PROJECT_VENV -> presenter.setupVirtualenv(Path.of(projectPath.get(), ".venv"),
@@ -17,8 +17,10 @@ class PythonExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.python.path")) {
pythonBaseInterpreterComboBox(presenter, presenter.allSdksFlow, presenter.detectingSdks, presenter.state.selectedVenvPath,
presenter::addPythonInterpreter)
pythonInterpreterComboBox(presenter.state.selectedVenv,
presenter,
presenter.allSdksFlow,
presenter::addPythonInterpreter)
.align(Align.FILL)
}
}
@@ -49,8 +49,7 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt
private val locationValidationFailed = propertyGraph.property(false)
private val locationValidationMessage = propertyGraph.property("Current location already exists")
private val basePythonVersion = propertyGraph.property<Sdk?>(initial = null)
private val basePythonHomePath = basePythonVersion.transformToHomePathProperty(state.basePythonSdks)
private lateinit var versionComboBox: ComboBox<String?>
private lateinit var versionComboBox: ComboBox<Sdk?>
private var locationModified = false
private var suggestedVenvName: String = ""
private var suggestedLocation: Path = Path.of("")
@@ -73,9 +72,10 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt
with(panel) {
row(message("sdk.create.custom.base.python")) {
versionComboBox =
pythonBaseInterpreterComboBox(presenter, presenter.basePythonSdksFlow, presenter.detectingSdks, basePythonHomePath,
presenter::addBasePythonInterpreter)
versionComboBox = pythonInterpreterComboBox(basePythonVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
.align(Align.FILL)
.component
}
@@ -0,0 +1,137 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.v2
import com.intellij.icons.AllIcons
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.observable.util.addMouseHoverListener
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.JBColor
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
import com.intellij.ui.hover.HoverListener
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.PythonSdkType
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.awt.Component
import java.awt.Cursor
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.BorderFactory
import javax.swing.ComboBoxEditor
import javax.swing.JComponent
import javax.swing.JLabel
class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox<Sdk?>,
val presenter: PythonAddInterpreterPresenter,
onPathSelected: (String) -> Unit) : ComboBoxEditor {
private val component = SimpleColoredComponent()
private val panel: JComponent
private lateinit var iconLabel: JLabel
private var _item: Any? = null
private var isBusy = false
init {
panel = panel {
row {
cell(component)
.customize(UnscaledGaps(0))
.applyToComponent { border = BorderFactory.createEmptyBorder() }
iconLabel = cell(JLabel(AllIcons.General.OpenDisk))
.customize(UnscaledGaps(0))
.applyToComponent {
isOpaque = true
toolTipText = message("sdk.create.tooltip.browse")
addMouseHoverListener(null, object : HoverListener() {
val defaultBackground = this@applyToComponent.background
override fun mouseEntered(component: Component, x: Int, y: Int) {
if (!isBusy) {
(component as JLabel).let {
icon = AllIcons.General.OpenDiskHover
background = JBColor.namedColor("ComboBox.nonEditableBackground")
}
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
}
}
override fun mouseMoved(component: Component, x: Int, y: Int) {}
override fun mouseExited(component: Component) {
if (!isBusy) {
(component as JLabel).let {
icon = AllIcons.General.OpenDisk
background = defaultBackground
}
cursor = Cursor.getDefaultCursor()
}
}
})
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
if (!isBusy) {
val currentBaseSdkVirtualFile = (_item as? Sdk)?.let { sdk ->
val currentBaseSdkPathOnTarget = sdk.homePath.nullize(nullizeSpaces = true)
currentBaseSdkPathOnTarget?.let { presenter.tryGetVirtualFile(it) }
}
FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor,
null,
currentBaseSdkVirtualFile) { file ->
val nioPath = file?.toNioPath() ?: return@chooseFile
val targetPath = presenter.getPathOnTarget(nioPath)
comboBox.setPathToSelectAfterModelUpdate(targetPath)
onPathSelected(targetPath)
}
}
}
})
}
.align(AlignX.RIGHT)
.component
}
}
panel.border = null
presenter.scope.launch(start = CoroutineStart.UNDISPATCHED) {
presenter.detectingSdks.collectLatest {
withContext(presenter.uiContext) {
setBusy(it)
}
}
}
}
override fun setItem(anObject: Any?) {
if (_item == anObject) return
_item = anObject
component.clear()
if (anObject is Sdk) component.customizeForPythonSdk(anObject)
}
private fun setBusy(busy: Boolean) {
isBusy = busy
iconLabel.icon = if (isBusy) AnimatedIcon.Default.INSTANCE else AllIcons.General.OpenDisk
component.isEnabled = !isBusy
comboBox.isEnabled = !isBusy
}
override fun getEditorComponent(): Component = panel
override fun getItem(): Any? = _item
override fun selectAll() {}
override fun addActionListener(l: ActionListener?) {}
override fun removeActionListener(l: ActionListener?) {}
}
@@ -9,6 +9,7 @@ import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.PyDetectedSdk
import com.jetbrains.python.sdk.installSdkIfNeeded
import com.jetbrains.python.sdk.pipenv.PIPENV_ICON
import com.jetbrains.python.sdk.poetry.POETRY_ICON
import com.jetbrains.python.sdk.setup
@@ -74,6 +75,10 @@ enum class PythonInterpreterSelectionMethod {
CREATE_NEW, SELECT_EXISTING
}
internal fun setupBaseSdk(sdk: Sdk, existingSdks: List<Sdk>): Sdk {
return setupSdkIfDetected(installSdkIfNeeded(sdk, null, existingSdks)!!, existingSdks)
}
internal fun setupSdkIfDetected(sdk: Sdk, existingSdks: List<Sdk>): Sdk = when (sdk) {
is PyDetectedSdk -> {
val newSdk = sdk.setup(existingSdks)!!
@@ -18,28 +18,28 @@ import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.util.IconLoader
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.*
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.fields.ExtendableTextComponent
import com.intellij.ui.components.fields.ExtendableTextField
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.components.ValidationType
import com.intellij.ui.dsl.builder.components.validationTooltip
import com.intellij.util.text.nullize
import com.intellij.util.ui.JBUI
import com.intellij.ui.util.preferredHeight
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.PyDetectedSdk
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.PySdkToInstall
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.CREATE_NEW
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.CUSTOM
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.VIRTUALENV
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import icons.PythonIcons
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.SharedFlow
@@ -49,9 +49,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
import java.nio.file.Paths
import javax.swing.JList
import javax.swing.JPanel
import javax.swing.JTextField
import javax.swing.*
import javax.swing.plaf.basic.BasicComboBoxEditor
import kotlin.coroutines.CoroutineContext
import kotlin.io.path.exists
@@ -125,17 +123,31 @@ class PythonNewEnvironmentDialogNavigator {
}
}
internal fun SimpleColoredComponent.customizeForPythonSdk(sdk: Sdk) {
when (sdk) {
is PyDetectedSdk -> {
icon = IconLoader.getTransparentIcon(PythonIcons.Python.Python)
append(sdk.homePath!!)
append(" " + message("sdk.rendering.detected.grey.text"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}
is PySdkToInstall -> {
icon = AllIcons.Actions.Download
append(sdk.name)
append(" " + message("sdk.rendering.installable.grey.text"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}
else -> {
icon = PythonIcons.Python.Python
append(sdk.versionString!!)
append(" " + sdk.homePath!!, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}
}
}
class PythonSdkComboBoxListCellRenderer : ColoredListCellRenderer<Any>() {
override fun customizeCellRenderer(list: JList<out Any>, value: Any?, index: Int, selected: Boolean, hasFocus: Boolean) {
when (value) {
is PyDetectedSdk -> append(value.homePath!!)
is Sdk -> {
append(value.versionString!!)
append(" " + value.homePath!!, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}
else -> append("")
}
if (value !is Sdk) error("Not an Sdk")
customizeForPythonSdk(value)
}
}
@@ -167,96 +179,47 @@ class PythonEnvironmentComboBoxRenderer : ColoredListCellRenderer<Any>() {
}
}
internal fun Row.nonEditablePythonInterpreterComboBox(sdksFlow: StateFlow<List<Sdk>>,
scope: CoroutineScope,
uiContext: CoroutineContext): Cell<ComboBox<Sdk?>> =
internal fun Row.pythonInterpreterComboBox(selectedSdkProperty: ObservableMutableProperty<Sdk?>,
presenter: PythonAddInterpreterPresenter,
sdksFlow: StateFlow<List<Sdk>>,
onPathSelected: (String) -> Unit): Cell<ComboBox<Sdk?>> =
comboBox<Sdk?>(emptyList(), PythonSdkComboBoxListCellRenderer())
.withSdkItems(sdksFlow, { it }, scope, uiContext)
.bindItem(selectedSdkProperty)
.applyToComponent {
editor = object : BasicComboBoxEditor() {
override fun createEditorComponent(): JTextField = ExtendableTextField().apply { border = null }
}
}
preferredHeight = 30
isEditable = true
editor = PythonSdkComboBoxWithBrowseButtonEditor(this, presenter, onPathSelected)
internal fun Row.pythonBaseInterpreterComboBox(presenter: PythonAddInterpreterPresenter,
sdksFlow: StateFlow<List<Sdk>>,
loadingFlow: StateFlow<Boolean>,
pathToSelectedSdk: ObservableMutableProperty<String?>,
onSdkChosen: (String) -> Unit): Cell<ComboBox<String?>> =
comboBox<String?>(emptyList())
.bindItem(pathToSelectedSdk)
.withSdkItems(sdksFlow, mapper = { sdk -> sdk.homePath.orEmpty() }, scope = presenter.scope, uiContext = presenter.uiContext)
.displayLoaderWhen(loadingFlow, scope = presenter.scope, uiContext = presenter.uiContext)
.withBrowsableSdk(presenter, onSdkChosen)
presenter.scope.launch(start = CoroutineStart.UNDISPATCHED) {
sdksFlow.collectLatest { sdks ->
withContext(presenter.uiContext) {
removeAllItems()
sdks.forEach(this@applyToComponent::addItem)
private fun <T, C : ComboBox<T>> Cell<C>.withSdkItems(sdksFlow: StateFlow<List<Sdk>>,
mapper: (Sdk) -> T,
scope: CoroutineScope,
uiContext: CoroutineContext): Cell<C> =
applyToComponent { withSdkItems(sdksFlow, scope, uiContext, mapper) }
private fun <T> ComboBox<T>.withSdkItems(sdksFlow: StateFlow<List<Sdk>>,
scope: CoroutineScope,
uiContext: CoroutineContext,
mapper: (Sdk) -> T) {
scope.launch(start = CoroutineStart.UNDISPATCHED) {
sdksFlow.collectLatest { sdks ->
withContext(uiContext) {
val itemToSelectAfterModelUpdate = tryGetAndRemoveItemToSelectAfterModelUpdate() ?: selectedItem
removeAllItems()
val items = sdks.map(mapper)
items.forEach(this@withSdkItems::addItem)
if (itemToSelectAfterModelUpdate != null && items.any { it == itemToSelectAfterModelUpdate }) {
// restore previously selected item if it (still) presents in the combobox
selectedItem = itemToSelectAfterModelUpdate
val pathToSelect = tryGetAndRemovePathToSelectAfterModelUpdate() as? String
val newValue = if (pathToSelect != null) sdks.find { it.homePath == pathToSelect } else findPrioritySdk(sdks)
selectedSdkProperty.set(newValue)
}
}
}
}
}
private fun findPrioritySdk(sdkList: List<Sdk>): Sdk? {
// todo[akniazev] save last used base sdk path to suggest first
return sdkList.firstOrNull { it !is PyDetectedSdk && it !is PySdkToInstall }
?: sdkList.firstOrNull { it is PyDetectedSdk }
?: sdkList.firstOrNull { it is PySdkToInstall }
}
private val KEY_ITEM_TO_SELECT_AFTER_MODEL_UPDATED: Key<Any?> by lazy { Key.create("ITEM_TO_SELECT_AFTER_MODEL_UPDATED") }
private val KEY_PATH_TO_SELECT_AFTER_MODEL_UPDATED: Key<String> by lazy { Key.create("PATH_TO_SELECT_AFTER_MODEL_UPDATED") }
private fun <T> ComboBox<T>.tryGetAndRemoveItemToSelectAfterModelUpdate(): @NlsSafe Any? =
getUserData(KEY_ITEM_TO_SELECT_AFTER_MODEL_UPDATED)?.also {
putUserData(KEY_ITEM_TO_SELECT_AFTER_MODEL_UPDATED, null)
internal fun <T> ComboBox<T>.tryGetAndRemovePathToSelectAfterModelUpdate(): @NlsSafe Any? =
getUserData(KEY_PATH_TO_SELECT_AFTER_MODEL_UPDATED)?.also {
putUserData(KEY_PATH_TO_SELECT_AFTER_MODEL_UPDATED, null)
}
private fun ComboBox<String?>.setItemToSelectAfterModelUpdate(targetPath: @NlsSafe Any) {
putUserData(KEY_ITEM_TO_SELECT_AFTER_MODEL_UPDATED, targetPath)
}
internal fun Cell<ComboBox<String?>>.withBrowsableSdk(presenter: PythonAddInterpreterPresenter,
onSdkChosen: (String) -> Unit): Cell<ComboBox<String?>> =
applyToComponent { withBrowsableSdk(presenter, onSdkChosen) }
private fun ComboBox<String?>.withBrowsableSdk(presenter: PythonAddInterpreterPresenter, onSdkChosen: (String) -> Unit) {
val thisComboBox = this@withBrowsableSdk
val browseExtension = ExtendableTextComponent.Extension.create(AllIcons.General.OpenDisk,
AllIcons.General.OpenDiskHover,
message("sdk.create.custom.python.browse.tooltip")) {
val currentBaseSdkPathOnTarget = thisComboBox.item.nullize(nullizeSpaces = true)
val currentBaseSdkVirtualFile = currentBaseSdkPathOnTarget?.let { presenter.tryGetVirtualFile(it) }
FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor, null,
currentBaseSdkVirtualFile) { file ->
val nioPath = file?.toNioPath() ?: return@chooseFile
val targetPath = presenter.getPathOnTarget(nioPath)
thisComboBox.setItemToSelectAfterModelUpdate(targetPath)
onSdkChosen(targetPath)
}
}
isEditable = true
editor = object : BasicComboBoxEditor() {
override fun createEditorComponent(): JTextField {
val field = ExtendableTextField()
field.addExtension(browseExtension)
field.setBorder(null)
field.isEditable = false
field.background = JBUI.CurrentTheme.Arrow.backgroundColor(true, true)
return field
}
}
internal fun ComboBox<*>.setPathToSelectAfterModelUpdate(targetPath: @NlsSafe String) {
putUserData(KEY_PATH_TO_SELECT_AFTER_MODEL_UPDATED, targetPath)
}
/**