mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
295938dc46
commit
2222b97280
@@ -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
|
||||
}
|
||||
|
||||
+137
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user