diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index a8b77ea27da3..9b09e44f89dd 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -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} diff --git a/python/src/com/jetbrains/python/sdk/PySdkToInstallManager.kt b/python/src/com/jetbrains/python/sdk/PySdkToInstallManager.kt index b753df728e39..67225ae60c9f 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkToInstallManager.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkToInstallManager.kt @@ -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, diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt index 3a9b5481d31f..6d6d3bc07b50 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt @@ -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(initial = null) - private val basePythonHomePath = basePythonVersion.transformToHomePathProperty(state.basePythonSdks) private lateinit var pipEnvPathField: TextFieldWithBrowseButton - private lateinit var basePythonComboBox: ComboBox + private lateinit var basePythonComboBox: ComboBox 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) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt index eabfe7264229..448e492ea33a 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt @@ -29,14 +29,14 @@ class PoetryNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Py val executable = propertyGraph.property(UNKNOWN_EXECUTABLE) private val basePythonVersion = propertyGraph.property(initial = null) - private val basePythonHomePath = basePythonVersion.transformToHomePathProperty(state.basePythonSdks) - private lateinit var basePythonComboBox: ComboBox + private lateinit var basePythonComboBox: ComboBox 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) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt index 0426c5ce27f5..5e91a153b0c2 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt @@ -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 = _detectingCondaExecutable.asStateFlow() + private val _sdksToInstall: MutableStateFlow> = 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> = - 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, sdkList: List): List { + 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 } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt index d87fa55a5fb1..007677f17af1 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt @@ -13,6 +13,7 @@ class PythonAddInterpreterState( val scope: CoroutineScope, val basePythonSdks: ObservableMutableProperty>, val allExistingSdks: ObservableMutableProperty>, + val installableSdks: ObservableMutableProperty>, val selectedVenv: ObservableMutableProperty, val condaExecutable: ObservableMutableProperty, ) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt index 0428418e6a93..1f2b19a240b2 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt @@ -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) private val allExistingSdks = propertyGraph.property>(emptyList()) private val basePythonSdks = propertyGraph.property>(emptyList()) + private val installableSdks = propertyGraph.property>(emptyList()) private val pythonBaseVersion = propertyGraph.property(null) private val selectedVenv = propertyGraph.property(null) @@ -58,6 +60,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty) service().coroutineScope, basePythonSdks, allExistingSdks, + installableSdks, selectedVenv, condaExecutable) @@ -78,11 +81,10 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty) }.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) ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks) } allExistingSdks.set(allValidSdks) + installableSdks.set(getSdksToInstall()) updateVenvLocationHint() } @@ -126,7 +129,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty) } } - fun getSdk(): Sdk? { + fun getSdk(): Sdk { presenter.navigator.saveLastState() return when (selectedMode.get()) { PROJECT_VENV -> presenter.setupVirtualenv(Path.of(projectPath.get(), ".venv"), diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt index 14e725889443..735b0405d48e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt @@ -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) } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt index 674b879cdcdc..111428210cf6 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt @@ -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(initial = null) - private val basePythonHomePath = basePythonVersion.transformToHomePathProperty(state.basePythonSdks) - private lateinit var versionComboBox: ComboBox + private lateinit var versionComboBox: ComboBox 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 } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkComboBoxWithBrowseButtonEditor.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkComboBoxWithBrowseButtonEditor.kt new file mode 100644 index 000000000000..7062ec6c749e --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkComboBoxWithBrowseButtonEditor.kt @@ -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, + 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?) {} +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 0101285c46a1..ba0e7a32eeeb 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -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 { + return setupSdkIfDetected(installSdkIfNeeded(sdk, null, existingSdks)!!, existingSdks) +} + internal fun setupSdkIfDetected(sdk: Sdk, existingSdks: List): Sdk = when (sdk) { is PyDetectedSdk -> { val newSdk = sdk.setup(existingSdks)!! diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt index 8d87d1188baa..388c3f6a409a 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt @@ -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() { override fun customizeCellRenderer(list: JList, 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() { } } -internal fun Row.nonEditablePythonInterpreterComboBox(sdksFlow: StateFlow>, - scope: CoroutineScope, - uiContext: CoroutineContext): Cell> = +internal fun Row.pythonInterpreterComboBox(selectedSdkProperty: ObservableMutableProperty, + presenter: PythonAddInterpreterPresenter, + sdksFlow: StateFlow>, + onPathSelected: (String) -> Unit): Cell> = comboBox(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>, - loadingFlow: StateFlow, - pathToSelectedSdk: ObservableMutableProperty, - onSdkChosen: (String) -> Unit): Cell> = - comboBox(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 > Cell.withSdkItems(sdksFlow: StateFlow>, - mapper: (Sdk) -> T, - scope: CoroutineScope, - uiContext: CoroutineContext): Cell = - applyToComponent { withSdkItems(sdksFlow, scope, uiContext, mapper) } - -private fun ComboBox.withSdkItems(sdksFlow: StateFlow>, - 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? { + // 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 by lazy { Key.create("ITEM_TO_SELECT_AFTER_MODEL_UPDATED") } +private val KEY_PATH_TO_SELECT_AFTER_MODEL_UPDATED: Key by lazy { Key.create("PATH_TO_SELECT_AFTER_MODEL_UPDATED") } -private fun ComboBox.tryGetAndRemoveItemToSelectAfterModelUpdate(): @NlsSafe Any? = - getUserData(KEY_ITEM_TO_SELECT_AFTER_MODEL_UPDATED)?.also { - putUserData(KEY_ITEM_TO_SELECT_AFTER_MODEL_UPDATED, null) +internal fun ComboBox.tryGetAndRemovePathToSelectAfterModelUpdate(): @NlsSafe Any? = + getUserData(KEY_PATH_TO_SELECT_AFTER_MODEL_UPDATED)?.also { + putUserData(KEY_PATH_TO_SELECT_AFTER_MODEL_UPDATED, null) } -private fun ComboBox.setItemToSelectAfterModelUpdate(targetPath: @NlsSafe Any) { - putUserData(KEY_ITEM_TO_SELECT_AFTER_MODEL_UPDATED, targetPath) -} - -internal fun Cell>.withBrowsableSdk(presenter: PythonAddInterpreterPresenter, - onSdkChosen: (String) -> Unit): Cell> = - applyToComponent { withBrowsableSdk(presenter, onSdkChosen) } - -private fun ComboBox.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) } /**