[python] new ui for python interpreter creation in new project wizard (PY-60214)

- covered by registry key `python.new.interpreter.creation.ui` -- enabled by default
- missing proper validation and state save (tbd)

GitOrigin-RevId: 4035bb82d526be7a164715f7567d63319f4be499
This commit is contained in:
Aleksei Kniazev
2023-10-10 22:19:09 +00:00
committed by intellij-monorepo-bot
parent 6629923a74
commit 2b96defa3b
19 changed files with 1238 additions and 1 deletions
@@ -4,6 +4,7 @@ package com.jetbrains.python.newProject.steps;
import com.intellij.ide.util.projectWizard.AbstractNewProjectStep;
import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.platform.DirectoryProjectGenerator;
import com.intellij.util.ObjectUtils;
import com.jetbrains.python.newProject.*;
@@ -41,6 +42,9 @@ public final class PyCharmNewProjectStep extends AbstractNewProjectStep<PyNewPro
//noinspection unchecked
return new NewProjectWizardProjectSettingsStep<PyNewProjectSettings>(npwGenerator);
}
else if (Registry.is("python.new.interpreter.creation.ui")) {
return new PythonProjectSpecificSettingsStep<>(projectGenerator, callback);
}
else {
return new ProjectSpecificSettingsStep<>(projectGenerator, callback);
}
@@ -15,12 +15,16 @@
*/
package com.jetbrains.python.newProject.steps;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.ide.util.projectWizard.AbstractNewProjectStep;
import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase;
import com.intellij.ide.util.projectWizard.WebProjectTemplate;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.platform.DirectoryProjectGenerator;
@@ -29,15 +33,26 @@ import com.intellij.util.BooleanFunction;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.newProject.PyNewProjectSettings;
import com.jetbrains.python.newProject.PythonProjectGenerator;
import com.jetbrains.python.newProject.welcome.PyWelcomeSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Optional;
public class PythonGenerateProjectCallback<T> extends AbstractNewProjectStep.AbstractCallback<T> {
@Override
public void consume(@Nullable ProjectSettingsStepBase<T> step, @NotNull ProjectGeneratorPeer<T> projectGeneratorPeer) {
if (!(step instanceof ProjectSpecificSettingsStep settingsStep)) return;
if (step instanceof PythonProjectSpecificSettingsStep) {
// has to be set before project generation
boolean welcomeScript = PropertiesComponent.getInstance().getBoolean("PyCharm.NewProject.Welcome", false);
PyWelcomeSettings.getInstance().setCreateWelcomeScriptForEmptyProject(welcomeScript);
}
final DirectoryProjectGenerator generator = settingsStep.getProjectGenerator();
Sdk sdk = settingsStep.getSdk();
@@ -68,6 +83,20 @@ public class PythonGenerateProjectCallback<T> extends AbstractNewProjectStep.Abs
SdkConfigurationUtil.setDirectoryProjectSdk(newProject, sdk);
((PythonProjectGenerator<?>)generator).afterProjectGenerated(newProject);
}
if (step instanceof PythonProjectSpecificSettingsStep newStep) {
// init git repostory
if (PropertiesComponent.getInstance().getBoolean("PyCharm.NewProject.Git", false)) {
ModuleManager moduleManager = ModuleManager.getInstance(newProject);
Optional<Module> module = Arrays.stream(moduleManager.getModules()).findFirst();
module.ifPresent((value -> {
ModuleRootManager rootManager = ModuleRootManager.getInstance(value);
Arrays.stream(rootManager.getContentRoots()).findFirst().ifPresent(root -> {
PythonProjectSpecificSettingsStep.initializeGit(newProject, root);
});
}));
}
}
}
@Nullable
@@ -477,6 +477,51 @@ sdk.create.venv.conda.dialog.label.interpreter=Interpreter:
sdk.create.venv.conda.dialog.label.location=Location:
sdk.create.venv.conda.dialog.label.python.version=Python version:
sdk.create.interpreter.type=Interpreter type:
sdk.create.type.project.venv=Project venv
sdk.create.type.base.conda=Base conda
sdk.create.type.custom=Custom environment
sdk.create.python.version=Python version:
sdk.create.conda.executable.path=Path to conda:
sdk.create.simple.venv.hint=Python virtual environment will be created in the project root: {0}.venv
sdk.create.simple.conda.hint=To create a new conda environment or choose an existing one, proceed with Custom environment
sdk.create.custom.develop.on=Develop on:
sdk.create.custom.env.creation.type=Environment:
sdk.create.custom.generate.new=Generate new
sdk.create.custom.select.existing=Select existing
sdk.create.custom.type=Type:
sdk.create.custom.base.python=Base python:
sdk.create.custom.location=Location:
sdk.create.custom.inherit.packages=Inherit packages from base interpreter
sdk.create.custom.make.available=Make available to all projects
sdk.create.custom.python.path=Python path:
sdk.create.custom.python.browse.tooltip="Select Python executable"
sdk.create.custom.venv.location.browse.title=Select virtualenv location
sdk.create.custom.conda.browse.title=Select Conda Executable
sdk.create.custom.conda.env.name=Name:
sdk.create.custom.conda.create.progress=Creating conda interpreter
sdk.create.custom.conda.select.progress=Selecting conda interpreter
sdk.create.custom.conda.refresh.envs=Reload environments
sdk.create.custom.pipenv.path=Path to pipenv:
sdk.create.custom.pipenv.missing.text=Pipenv executable non found. <a href='selectPipEnv'>Select executable</a>
sdk.create.custom.poetry.path=Path to poetry:
sdk.create.custom.poetry.missing.text=Poetry executable non found. <a href='selectPipEnv'>Select executable</a>
sdk.create.targets.local=Local Machine
sdk.create.custom.virtualenv=Virtualenv
sdk.create.custom.conda=Conda
sdk.create.custom.pipenv=Pipenv
sdk.create.custom.poetry=Poetry
sdk.create.custom.python=Python
new.project.name=Name:
new.project.location=Location:
new.project.location.hint=Project will be created in: {0}
new.project.git=Create Git repository
new.project.welcome=Create a main.py welcome script
# remote interpreters
remote.interpreter.configure.title=Configure Remote Python Interpreter
remote.interpreter.configure.path.title=Select Python Interpreter
@@ -573,6 +573,9 @@
key="black.configurable.name"/>
<dependencyCollector kind="python" implementation="com.jetbrains.python.packaging.PyDependencyCollector"/>
<registryKey key="python.new.interpreter.creation.ui" defaultValue="true"
description="Enable new UI for adding Python interpreter in New Project dialog." />
</extensions>
<extensionPoints>
@@ -892,6 +895,8 @@
<!--suppress PluginXmlI18n -->
<action id="PyUpdateProjectSdk" internal="true" class="com.jetbrains.python.sdk.PyUpdateProjectSdkAction"
text="Update Python SDK" description="Forcibly update all configured Python SDKs in the project"/>
<action id="PySetPropertyAction" internal="true" class="com.jetbrains.python.sdk.add.v2.PySetPropertyAction"
text="Set PropertiesComponent Value" description="Set value in application-level PropertiesComponent"/>
<add-to-group group-id="Internal"/>
</group>
</actions>
@@ -144,7 +144,7 @@ public class ProjectSpecificSettingsStep<T> extends ProjectSettingsStepBase<T> i
* @return path for project on remote side provided by user
*/
@Nullable
public final String getRemotePath() {
public String getRemotePath() {
final PyAddSdkGroupPanel interpreterPanel = myInterpreterPanel;
if (interpreterPanel == null) return null;
final PyAddExistingSdkPanel panel = ObjectUtils.tryCast(interpreterPanel.getSelectedPanel(), PyAddExistingSdkPanel.class);
@@ -0,0 +1,154 @@
// 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.newProject.steps
import com.intellij.ide.IdeBundle
import com.intellij.ide.impl.ProjectUtil
import com.intellij.ide.impl.ProjectUtil.getUserHomeProjectDir
import com.intellij.ide.util.projectWizard.AbstractNewProjectStep
import com.intellij.openapi.GitRepositoryInitializer
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.bindBooleanStorage
import com.intellij.openapi.observable.util.joinSystemDependentPath
import com.intellij.openapi.progress.runBackgroundableTask
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.*
import com.intellij.util.PlatformUtils
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.PythonProjectGenerator
import com.jetbrains.python.psi.PyUtil
import com.jetbrains.python.sdk.PyLazySdk
import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel
import java.io.File
import java.nio.file.Paths
import java.util.*
import javax.swing.JPanel
class PythonProjectSpecificSettingsStep<T>(projectGenerator: DirectoryProjectGenerator<T>,
callback: AbstractNewProjectStep.AbstractCallback<T>)
: ProjectSpecificSettingsStep<T>(projectGenerator, callback), DumbAware {
private val propertyGraph = PropertyGraph()
private val projectName = propertyGraph.property("")
private val projectLocation = propertyGraph.property("")
private val locationHint = propertyGraph.property("").apply {
dependsOn(projectName, ::updateHint)
dependsOn(projectLocation, ::updateHint)
}
private val createRepository = propertyGraph.property(false)
.bindBooleanStorage("PyCharm.NewProject.Git")
private val createScript = propertyGraph.property(false)
.bindBooleanStorage("PyCharm.NewProject.Welcome")
private lateinit var projectNameFiled: JBTextField
lateinit var mainPanel: DialogPanel
override fun createAndFillContentPanel(): JPanel {
return createContentPanelWithAdvancedSettingsPanel()
}
override fun getProjectLocation(): String {
return FileUtil.expandUserHome(projectLocation.joinSystemDependentPath(projectName).get())
}
override fun getRemotePath(): String? {
return null
}
private val interpreterPanel = PythonAddNewEnvironmentPanel(projectLocation.joinSystemDependentPath(projectName))
override fun createBasePanel(): JPanel {
if (myProjectGenerator !is PythonProjectGenerator<*>) return super.createBasePanel()
myLocationField = TextFieldWithBrowseButton()
val nextProjectName = myProjectDirectory.get()
projectName.set(nextProjectName.nameWithoutExtension)
projectLocation.set(nextProjectName.parent)
mainPanel = panel {
row(message("new.project.name")) {
projectNameFiled = textField()
.bindText(projectName)
.component
}
row(message("new.project.location")) {
textFieldWithBrowseButton()
.bindText(projectLocation)
.align(Align.FILL)
}
row("") {
comment("", maxLineLength = 60).bindText(locationHint)
}
row("") {
checkBox(message("new.project.git")).bindSelected(createRepository)
checkBox(message("new.project.welcome")).bindSelected(createScript)
}
panel {
interpreterPanel.buildPanel(this)
}
}
mainPanel.registerValidators(this)
myCreateButton.addActionListener { mainPanel.apply() }
return mainPanel
}
override fun findSequentNonExistingUntitled(): File {
return Optional
.ofNullable(PyUtil.`as`(myProjectGenerator, PythonProjectGenerator::class.java))
.map { it.newProjectPrefix }
.map { FileUtil.findSequentNonexistentFile(getBaseDir(), it!!, "") }
.orElseGet { super.findSequentNonExistingUntitled() }
}
private fun getBaseDir(): File {
if (PlatformUtils.isDataSpell() && FileUtil.isAncestor(PathManager.getConfigDir(), Paths.get(ProjectUtil.getBaseDir()), false)) {
return File(getUserHomeProjectDir())
}
return File(ProjectUtil.getBaseDir())
}
private fun updateHint(): String {
return message("new.project.location.hint", Paths.get(projectLocation.get(), projectName.get()))
}
override fun checkValid(): Boolean {
// todo add proper validation with custom component
return true
}
override fun installFramework(): Boolean {
return true
}
override fun onPanelSelected() {
interpreterPanel.onShown()
}
override fun getSdk(): Sdk {
return PyLazySdk("Uninitialized environment") { interpreterPanel.getSdk() }
}
companion object {
@JvmStatic
fun initializeGit(project: Project, root: VirtualFile) {
runBackgroundableTask(IdeBundle.message("progress.title.creating.git.repository"), project) {
GitRepositoryInitializer.getInstance()?.initRepository(project, root, true)
}
}
}
}
@@ -0,0 +1,119 @@
// 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.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.observable.util.not
import com.intellij.openapi.progress.ModalTaskOwner
import com.intellij.openapi.progress.TaskCancellation
import com.intellij.openapi.progress.runWithModalProgressBlocking
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.components.fields.ExtendableTextComponent
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindText
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.add.target.conda.TargetEnvironmentRequestCommandExecutor
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CondaExistingEnvironmentSelector(state: PythonAddInterpreterState) : PythonAddEnvironment(state) {
private lateinit var envComboBox: ComboBox<PyCondaEnv?>
private lateinit var condaExecutableComboBox: TextFieldWithBrowseButton
private var selectedEnvironment = propertyGraph.property<PyCondaEnv?>(null)
private var lastLoadedConda = propertyGraph.property("")
private var condaEnvironmentsLoaded = propertyGraph.property(true)
override fun buildOptions(panel: Panel) {
with(panel) {
row(message("sdk.create.conda.executable.path")) {
condaExecutableComboBox = textFieldWithBrowseButton()
.bindText(state.condaExecutable)
.align(Align.FILL)
.component
}
row(message("sdk.create.custom.env.creation.type")) {
envComboBox = comboBox<PyCondaEnv?>(emptyList(), CondaEnvComboBoxListCellRenderer())
.bindItem(selectedEnvironment)
.enabledIf(condaEnvironmentsLoaded)
.component
link(message("sdk.create.custom.conda.refresh.envs")) {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.Default + modalityState) {
val commandExecutor = TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest())
val environments = PyCondaEnv.getEnvs(commandExecutor, state.condaExecutable.get())
withContext(Dispatchers.Main + modalityState) {
envComboBox.removeAllItems()
val envs = environments.getOrThrow() // todo error handling
selectedEnvironment.set(envs.first())
envs.forEach(envComboBox::addItem)
lastLoadedConda.set(state.condaExecutable.get())
}
}
}.visibleIf(!condaEnvironmentsLoaded)
}
}
}
override fun onShown() {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.Default + modalityState) {
val pathOnTarget = suggestCondaPath()!! // todo flow for undiscovered conda
val commandExecutor = TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest())
val environments = PyCondaEnv.getEnvs(commandExecutor, pathOnTarget)
withContext(Dispatchers.Main + modalityState) {
lastLoadedConda.set(pathOnTarget)
envComboBox.removeAllItems()
val envs = environments.getOrThrow()
selectedEnvironment.set(envs.first())
envs.forEach(envComboBox::addItem)
val refreshIcon = ExtendableTextComponent.Extension.create(AllIcons.Actions.Refresh, AllIcons.Actions.Refresh, "Refresh conda") {
// todo decide whether we need icon or just link
}
val textComponent = condaExecutableComboBox.childComponent as ExtendableTextComponent
condaEnvironmentsLoaded.dependsOn(state.condaExecutable, ::condasMatch)
condaEnvironmentsLoaded.dependsOn(lastLoadedConda, ::condasMatch)
condaEnvironmentsLoaded.afterChange {
if (it) textComponent.removeExtension(refreshIcon)
else textComponent.addExtension(refreshIcon)
}
}
}
}
private fun condasMatch(): Boolean = state.condaExecutable.get() == lastLoadedConda.get()
override fun getOrCreateSdk(): Sdk {
return runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.conda.select.progress"), TaskCancellation.nonCancellable()) {
PyCondaCommand(state.condaExecutable.get(), null)
.createCondaSdkFromExistingEnv(selectedEnvironment.get()!!.envIdentity, state.basePythonSdks.get(),
ProjectManager.getInstance().defaultProject)
}
}
}
@@ -0,0 +1,69 @@
// 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.openapi.application.EDT
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.progress.ModalTaskOwner
import com.intellij.openapi.progress.TaskCancellation
import com.intellij.openapi.progress.runWithModalProgressBlocking
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.add.target.conda.condaSupportedLanguages
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkAlongWithNewEnv
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import kotlinx.coroutines.Dispatchers
import java.io.File
class CondaNewEnvironmentCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) {
private val envName = propertyGraph.property("")
private lateinit var pythonVersion: ObservableMutableProperty<LanguageLevel>
private lateinit var versionComboBox: ComboBox<LanguageLevel>
override fun buildOptions(panel: Panel) {
with(panel) {
row(message("sdk.create.python.version")) {
pythonVersion = propertyGraph.property(condaSupportedLanguages.first())
versionComboBox = comboBox(condaSupportedLanguages, textListCellRenderer { it!!.toPythonVersion() })
.bindItem(pythonVersion)
.component
}
row(message("sdk.create.custom.conda.env.name")) {
textField()
.bindText(envName)
}
row(message("sdk.create.conda.executable.path")) {
textFieldWithBrowseButton(message("sdk.create.custom.conda.browse.title"),
null,
FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor())
.align(Align.FILL)
.bindText(state.condaExecutable)
}
}
}
override fun onShown() {
envName.set(state.projectPath.get().substringAfterLast(File.separator))
}
override fun getOrCreateSdk(): Sdk {
return runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.conda.create.progress"), TaskCancellation.nonCancellable()) {
PyCondaCommand(state.condaExecutable.get(), null, null)
.createCondaSdkAlongWithNewEnv(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), envName.get()),
Dispatchers.EDT,
state.basePythonSdks.get(),
ProjectManager.getInstance().defaultProject).getOrThrow()
}
}
}
@@ -0,0 +1,59 @@
// 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.ide.util.PropertiesComponent
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PipEnvNewEnvironmentCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) {
private val executable = propertyGraph.property(UNKNOWN_EXECUTABLE)
private lateinit var pipEnvPathField: TextFieldWithBrowseButton
private lateinit var basePythonComboBox: ComboBox<String>
override fun buildOptions(panel: Panel) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox = pythonBaseInterpreterComboBox(state.basePythonHomePaths, state.basePythonHomePath)
}
pipEnvPathField = executableSelector(message("sdk.create.custom.pipenv.path"), executable,
message("sdk.create.custom.pipenv.missing.text"))
}
}
override fun onShown() {
val savedPath = PropertiesComponent.getInstance().pipEnvPath
if (savedPath != null) {
executable.set(savedPath)
}
else {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.Default + modalityState) {
val detectedExecutable = detectPipEnvExecutable()
withContext(Dispatchers.Main + modalityState) {
detectedExecutable?.let { executable.set(it.path) }
}
}
}
}
override fun getOrCreateSdk(): Sdk {
PropertiesComponent.getInstance().pipEnvPath = pipEnvPathField.text.nullize()
return setupPipEnvSdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(),
state.basePythonVersion.get()!!.homePath, false)!!
}
}
@@ -0,0 +1,60 @@
// 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.ide.util.PropertiesComponent
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.poetry.detectPoetryExecutable
import com.jetbrains.python.sdk.poetry.poetryPath
import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PoetryNewEnvironmentCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) {
val executable = propertyGraph.property(UNKNOWN_EXECUTABLE)
private lateinit var poetryPathField: TextFieldWithBrowseButton
private lateinit var basePythonComboBox: ComboBox<String>
override fun buildOptions(panel: Panel) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox = pythonBaseInterpreterComboBox(state.basePythonHomePaths, state.basePythonHomePath)
}
poetryPathField = executableSelector(message("sdk.create.custom.poetry.path"), executable,
message("sdk.create.custom.poetry.missing.text"))
}
}
override fun onShown() {
val savedPath = PropertiesComponent.getInstance().poetryPath
if (savedPath != null) {
executable.set(savedPath)
}
else {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.Default + modalityState) {
val poetryExecutable = detectPoetryExecutable()
withContext(Dispatchers.Main + modalityState) {
poetryExecutable?.let { executable.set(it.path) }
}
}
}
}
override fun getOrCreateSdk(): Sdk {
PropertiesComponent.getInstance().poetryPath = executable.get().nullize()
return setupPoetrySdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(),
state.basePythonVersion.get()!!.homePath, false)!!
}
}
@@ -0,0 +1,32 @@
// 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.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.observable.properties.AtomicProperty
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.ui.components.dialog
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
class PySetPropertyAction : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) {
val key = AtomicProperty("")
val value = AtomicProperty("")
val panel = panel {
row("Key:") {
textField().bindText(key).align(Align.FILL)
}
row("Value:") {
textField().bindText(value).align(Align.FILL)
}
}
val result = dialog("Set value in PropertiesComponent", panel, false).showAndGet()
if (result) {
PropertiesComponent.getInstance().setValue(key.get(), value.get())
}
}
}
@@ -0,0 +1,47 @@
// 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.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets.LOCAL_MACHINE
class PythonAddCustomInterpreter(private val settings: PythonAddInterpreterState) {
private lateinit var targetSelector: ComboBox<PythonInterpreterCreationTargets>
private val targets = mapOf(
LOCAL_MACHINE to PythonLocalEnvironmentCreator(settings),
)
fun buildPanel(outerPanel: Panel) {
with(outerPanel) {
// todo uncomment for all available targets
//row(message("sdk.create.custom.develop.on")) {
// targetSelector = comboBox(targets.keys, PythonEnvironmentComboBoxRenderer())
// .widthGroup("env_aligned")
// .component
//}
//targets.forEach { target ->
// rowsRange {
// target.value.buildPanel(this)
// }.visibleIf(targetSelector.selectedValueMatches { it == target.key })
//}
rowsRange {
targets[LOCAL_MACHINE]!!.buildPanel(this)
}
}
}
fun onShown() {
targets.values.forEach(PythonLocalEnvironmentCreator::onShown)
}
fun getSdk(): Sdk {
return targets[targetSelector.selectedItem]!!.getSdk()
}
}
@@ -0,0 +1,40 @@
// 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.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.transform
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.sdk.add.target.createDetectedSdk
import kotlinx.coroutines.CoroutineScope
class PythonAddInterpreterState(
val propertyGraph: PropertyGraph,
val projectPath: ObservableProperty<String>,
val scope: CoroutineScope,
val basePythonSdks: ObservableMutableProperty<List<Sdk>>,
val allExistingSdks: ObservableMutableProperty<List<Sdk>>,
val basePythonVersion: ObservableMutableProperty<Sdk?>,
val condaExecutable: ObservableMutableProperty<String>,
) {
val basePythonHomePath = basePythonVersion.transform({ sdk -> sdk?.homePath ?: "" },
{ path -> basePythonSdks.get().find { it.homePath == path }!! })
val basePythonHomePaths = transformSdksToHomePath(basePythonSdks)
val allValidSdkPaths = transformSdksToHomePath(allExistingSdks)
private fun transformSdksToHomePath(sdkList: ObservableMutableProperty<List<Sdk>>): ObservableMutableProperty<List<String>> {
return sdkList.transform({ sdks -> sdks.map { it.homePath!! } },
{ paths ->
val existing = sdkList.get()
paths.map { path ->
existing.find { it.homePath == path } ?: createDetectedSdk(path, isLocal = true)
}
})
}
}
@@ -0,0 +1,185 @@
// 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.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.components.service
import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.or
import com.intellij.openapi.progress.ModalTaskOwner
import com.intellij.openapi.progress.TaskCancellation
import com.intellij.openapi.progress.runWithModalProgressBlocking
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.ui.dsl.builder.*
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep
import com.jetbrains.python.sdk.add.target.conda.TargetEnvironmentRequestCommandExecutor
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.*
import com.jetbrains.python.sdk.configuration.PyProjectVirtualEnvConfiguration
import com.jetbrains.python.sdk.findBaseSdks
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.file.Paths
import kotlin.io.path.exists
class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>) {
private val propertyGraph = PropertyGraph()
private var selectedMode = propertyGraph.property(PROJECT_VENV)
private var _projectVenv = propertyGraph.booleanProperty(selectedMode, PROJECT_VENV)
private var _baseConda = propertyGraph.booleanProperty(selectedMode, BASE_CONDA)
private var _custom = propertyGraph.booleanProperty(selectedMode, CUSTOM)
private val allExistingSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val basePythonSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val pythonBaseVersion = propertyGraph.property<Sdk?>(null)
private val condaExecutable = propertyGraph.property("")
private var venvHint = propertyGraph.property("")
private lateinit var pythonBaseVersionComboBox: ComboBox<Sdk?>
private lateinit var baseCondaEnv: PyCondaEnv
private var initialized = false
private fun updateVenvLocationHint() {
if (selectedMode.get() == PROJECT_VENV) venvHint.set(message("sdk.create.simple.venv.hint", projectPath.get() + File.separator))
else if (selectedMode.get() == BASE_CONDA) venvHint.set(message("sdk.create.simple.conda.hint"))
}
val state = PythonAddInterpreterState(propertyGraph,
projectPath,
service<PythonAddSdkService>().coroutineScope,
basePythonSdks,
allExistingSdks,
pythonBaseVersion,
condaExecutable)
private val custom = PythonAddCustomInterpreter(state)
fun buildPanel(outerPanel: Panel) {
with(outerPanel) {
row(message("sdk.create.interpreter.type")) {
segmentedButton(PythonInterpreterSelectionMode.entries) { text = message(it.nameKey) }
.bind(selectedMode)
}.topGap(TopGap.SMALL)
row(message("sdk.create.python.version")) {
pythonBaseVersionComboBox = comboBox<Sdk?>(emptyList(), PythonSdkComboBoxListCellRenderer())
.bindItem(pythonBaseVersion)
.align(AlignX.FILL)
.component
}.visibleIf(_projectVenv)
row(message("sdk.create.conda.executable.path")) {
textFieldWithBrowseButton()
.bindText(condaExecutable)
.validationOnInput {
if (!Paths.get(it.text).exists()) error("Executable does not exist") else null
}
.align(AlignX.FILL)
}.visibleIf(_baseConda)
row("") {
comment("").bindText(venvHint)
}.visibleIf(_projectVenv or _baseConda)
rowsRange {
custom.buildPanel(this)
}.visibleIf(_custom)
}
basePythonSdks.afterChange {
pythonBaseVersionComboBox.removeAllItems()
it.forEach { pythonBaseVersionComboBox.addItem(it) }
}
projectPath.afterChange { updateVenvLocationHint() }
selectedMode.afterChange { updateVenvLocationHint() }
// todo why doesn't work?
//venvHint.dependsOn(projectPath, ::updateVenvLocationHint)
//venvHint.dependsOn(selectedMode, ::updateVenvLocationHint)
}
fun onShown() {
if (!initialized) {
initialized = true
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.Default + modalityState) {
val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList()
val baseSdks = findBaseSdks(existingSdks, null, UserDataHolderBase())
val allValidSdks = ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks)
val validBaseSdks = ProjectSpecificSettingsStep.getValidPythonSdks(baseSdks)
withContext(Dispatchers.Main + modalityState) {
pythonBaseVersionComboBox.removeAllItems()
validBaseSdks.forEach { pythonBaseVersionComboBox.addItem(it) }
basePythonSdks.set(validBaseSdks)
allExistingSdks.set(allValidSdks)
updateVenvLocationHint()
}
}
// todo maybe do this when env requested
state.scope.launch(Dispatchers.Default + modalityState) {
val condaPath = suggestCondaPath() ?: return@launch // todo make conda a executableSelector component
val commandExecutor = TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest())
val environments = PyCondaEnv.getEnvs(commandExecutor, condaPath)
val baseConda = environments.getOrThrow().find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
withContext(Dispatchers.Main + modalityState) {
condaExecutable.set(condaPath)
baseCondaEnv = baseConda!!
}
}
custom.onShown()
}
}
//fun validateCurrent(): Boolean {
// return when (selectedMode.get()) {
// PROJECT_VENV -> pythonBaseVersion.get().let { it != null && it.sdkSeemsValid }
// BASE_CONDA -> {
// val condaPath = condaExecutable.get()
// return condaPath != null
// }
// CUSTOM -> error("")
// }
//}
fun getSdk(): Sdk? {
return when (selectedMode.get()) {
PROJECT_VENV -> {
val venvPath = projectPath.get() + File.separator + ".venv"
return PyProjectVirtualEnvConfiguration.createVirtualEnvSynchronously(pythonBaseVersion.get(), basePythonSdks.get(), venvPath,
projectPath.get(),
null, null)
}
BASE_CONDA -> {
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.conda.select.progress"), TaskCancellation.nonCancellable()) {
PyCondaCommand(condaExecutable.get(), null)
.createCondaSdkFromExistingEnv(baseCondaEnv.envIdentity, basePythonSdks.get(), ProjectManager.getInstance().defaultProject)
}
}
CUSTOM -> custom.getSdk()
}
}
}
@@ -0,0 +1,27 @@
// 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.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.PyBundle.message
class PythonExistingEnvironmentSelector(state: PythonAddInterpreterState) : PythonAddEnvironment(state) {
private lateinit var versionComboBox: ComboBox<String>
private val selectedPath = propertyGraph.property("")
override fun buildOptions(panel: Panel) {
with(panel) {
row(message("sdk.create.custom.python.path")) {
versionComboBox = pythonBaseInterpreterComboBox(state.allValidSdkPaths, selectedPath)
}
}
selectedPath.dependsOn(state.basePythonHomePath) {
state.basePythonHomePath.get()
}
}
override fun getOrCreateSdk(): Sdk {
return state.allExistingSdks.get().find { it.homePath == selectedPath.get() }!!
}
}
@@ -0,0 +1,87 @@
// 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.openapi.observable.util.and
import com.intellij.openapi.observable.util.equalsTo
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bind
import com.intellij.ui.dsl.builder.bindItem
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
class PythonLocalEnvironmentCreator(private val settings: PythonAddInterpreterState) : PythonTargetEnvironmentInterpreterCreator {
private val selectionMethod = settings.propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW)
private val _createNew = settings.propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.CREATE_NEW)
private val _selectExisting = settings.propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.SELECT_EXISTING)
private val newInterpreterManager = settings.propertyGraph.property(VIRTUALENV)
private val existingInterpreterManager = settings.propertyGraph.property(PYTHON)
private val newInterpreterCreators = mapOf(
VIRTUALENV to PythonNewVirtualenvCreator(settings),
CONDA to CondaNewEnvironmentCreator(settings),
PIPENV to PipEnvNewEnvironmentCreator(settings),
POETRY to PoetryNewEnvironmentCreator(settings),
)
private val existingInterpreterSelectors = mapOf(
PYTHON to PythonExistingEnvironmentSelector(settings),
CONDA to CondaExistingEnvironmentSelector(settings),
)
override fun buildPanel(outerPanel: Panel) {
with(outerPanel) {
buttonsGroup {
row(message("sdk.create.custom.env.creation.type")) {
radioButton(message("sdk.create.custom.generate.new"), PythonInterpreterSelectionMethod.CREATE_NEW).onChanged {
selectionMethod.set(if (it.isSelected) PythonInterpreterSelectionMethod.CREATE_NEW else PythonInterpreterSelectionMethod.SELECT_EXISTING)
}
radioButton(message("sdk.create.custom.select.existing"), PythonInterpreterSelectionMethod.SELECT_EXISTING).onChanged {
selectionMethod.set(if (it.isSelected) PythonInterpreterSelectionMethod.SELECT_EXISTING else PythonInterpreterSelectionMethod.CREATE_NEW)
}
}
}.bind({ selectionMethod.get() }, { selectionMethod.set(it) } )
row(message("sdk.create.custom.type")) {
comboBox(newInterpreterCreators.keys, PythonEnvironmentComboBoxRenderer())
.bindItem(newInterpreterManager)
.widthGroup("env_aligned")
.visibleIf(_createNew)
comboBox(existingInterpreterSelectors.keys, PythonEnvironmentComboBoxRenderer())
.bindItem(existingInterpreterManager)
.widthGroup("env_aligned")
.visibleIf(_selectExisting)
}
newInterpreterCreators.forEach { (type, creator) ->
rowsRange {
creator.buildOptions(this)
}.visibleIf(_createNew and newInterpreterManager.equalsTo(type))
}
existingInterpreterSelectors.forEach { (type, selector) ->
rowsRange {
selector.buildOptions(this)
}.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type))
}
}
}
override fun onShown() {
newInterpreterCreators.values.forEach(PythonAddEnvironment::onShown)
existingInterpreterSelectors.values.forEach(PythonAddEnvironment::onShown)
}
override fun getSdk(): Sdk {
val sdkManager =
if (_selectExisting.get()) existingInterpreterSelectors[existingInterpreterManager.get()]
else newInterpreterCreators[newInterpreterManager.get()]
return sdkManager!!.getOrCreateSdk()
}
}
@@ -0,0 +1,81 @@
// 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.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.dsl.builder.*
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.configuration.PyProjectVirtualEnvConfiguration
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.file.Paths
import kotlin.io.path.exists
class PythonNewVirtualenvCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) {
private val location = propertyGraph.property("")
private val inheritSitePackages = propertyGraph.property(false)
private val makeAvailable = propertyGraph.property(false)
private lateinit var versionComboBox: ComboBox<String>
private var locationModified = false
override fun buildOptions(panel: Panel) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
versionComboBox = pythonBaseInterpreterComboBox(state.basePythonHomePaths,
state.basePythonHomePath)
}
row(message("sdk.create.custom.location")) {
textFieldWithBrowseButton(message("sdk.create.custom.venv.location.browse.title"), fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor())
.bindText(location)
.whenTextChangedFromUi { locationModified = true }
.cellValidation {
addInputRule("This directory already exists") {
Paths.get(it.text).exists() // todo validation with custom component, once available
}
}
.align(Align.FILL)
}
row("") {
checkBox(message("sdk.create.custom.inherit.packages"))
.bindSelected(inheritSitePackages)
}
row("") {
checkBox(message("sdk.create.custom.make.available"))
.bindSelected(makeAvailable)
}
}
state.projectPath.afterChange {
if (!locationModified) {
location.set(PySdkSettings.instance.getPreferredVirtualEnvBasePath(it))
}
}
}
override fun onShown() {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.Default + modalityState) {
val basePath = PySdkSettings.instance.getPreferredVirtualEnvBasePath(state.projectPath.get())
withContext(Dispatchers.Main + modalityState) {
location.set(basePath)
}
}
}
override fun getOrCreateSdk(): Sdk {
return PyProjectVirtualEnvConfiguration.createVirtualEnvSynchronously(state.basePythonVersion.get(),
state.basePythonSdks.get(),
location.get(),
state.projectPath.get(),
null, null,
inheritSitePackages = inheritSitePackages.get(),
makeShared = makeAvailable.get())!!
}
}
@@ -0,0 +1,57 @@
// 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.components.Service
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.sdk.pipenv.PIPENV_ICON
import com.jetbrains.python.sdk.poetry.POETRY_ICON
import icons.PythonIcons
import icons.PythonSdkIcons
import kotlinx.coroutines.CoroutineScope
import javax.swing.Icon
@Service(Service.Level.APP)
class PythonAddSdkService(val coroutineScope: CoroutineScope)
interface PythonTargetEnvironmentInterpreterCreator {
fun buildPanel(outerPanel: Panel)
fun onShown() {}
fun getSdk(): Sdk
}
abstract class PythonAddEnvironment(val state: PythonAddInterpreterState) {
internal val propertyGraph
get() = state.propertyGraph
abstract fun buildOptions(panel: Panel)
open fun onShown() {}
abstract fun getOrCreateSdk(): Sdk
}
enum class PythonSupportedEnvironmentManagers(val nameKey: String, val icon: Icon) {
VIRTUALENV("sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv),
CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda),
POETRY("sdk.create.custom.poetry", POETRY_ICON),
PIPENV("sdk.create.custom.pipenv", PIPENV_ICON),
PYTHON("sdk.create.custom.python", PythonSdkIcons.Python)
}
enum class PythonInterpreterSelectionMode(val nameKey: String) {
PROJECT_VENV("sdk.create.type.project.venv"),
BASE_CONDA("sdk.create.type.base.conda"),
CUSTOM("sdk.create.type.custom")
}
enum class PythonInterpreterCreationTargets(val nameKey: String, val icon: Icon) {
LOCAL_MACHINE("sdk.create.targets.local", AllIcons.Nodes.HomeFolder),
SSH("", AllIcons.Nodes.HomeFolder),
DOCKER("", AllIcons.Nodes.HomeFolder)
}
enum class PythonInterpreterSelectionMethod {
CREATE_NEW, SELECT_EXISTING
}
@@ -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.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.*
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.fields.ExtendableTextComponent
import com.intellij.ui.components.fields.ExtendableTextField
import com.intellij.ui.dsl.builder.*
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.PyDetectedSdk
import com.jetbrains.python.sdk.flavors.conda.*
import org.jetbrains.annotations.Nls
import javax.swing.JList
import javax.swing.JTextField
import javax.swing.plaf.basic.BasicComboBoxEditor
internal fun <T> PropertyGraph.booleanProperty(dependency: ObservableProperty<T>, value: T) =
lazyProperty { false }.apply { dependsOn(dependency) { dependency.get() == value } }
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("")
}
}
}
class CondaEnvComboBoxListCellRenderer : ColoredListCellRenderer<PyCondaEnv>() {
@Suppress("HardCodedStringLiteral")
override fun customizeCellRenderer(list: JList<out PyCondaEnv>, value: PyCondaEnv?, index: Int, selected: Boolean, hasFocus: Boolean) {
when (val identity = value?.envIdentity) {
is PyCondaEnvIdentity.NamedEnv -> append(identity.userReadableName)
is PyCondaEnvIdentity.UnnamedEnv -> append(identity.envPath)
else -> append("")
}
}
}
class PythonEnvironmentComboBoxRenderer : ColoredListCellRenderer<Any>() {
override fun customizeCellRenderer(list: JList<out Any>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) {
when (value) {
is PythonSupportedEnvironmentManagers, -> {
icon = value.icon
append(message(value.nameKey))
}
is PythonInterpreterCreationTargets -> {
icon = value.icon
append(message(value.nameKey))
}
}
}
}
fun Row.pythonBaseInterpreterComboBox(sdkHomePaths: ObservableMutableProperty<List<String>>,
selectedPath: ObservableMutableProperty<String>): ComboBox<String> {
val component = comboBox<String>(emptyList())
.bindItem(selectedPath)
.align(Align.FILL)
.component
val browseExtension = ExtendableTextComponent.Extension.create(AllIcons.General.OpenDisk,
AllIcons.General.OpenDiskHover,
message("sdk.create.custom.python.browse.tooltip")) {
FileChooser.chooseFile(FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), null, null) { file ->
val path = file?.path ?: return@chooseFile
val homePaths = sdkHomePaths.get()
if (!homePaths.contains(path)) {
sdkHomePaths.set(listOf(path) + homePaths)
}
selectedPath.set(path)
}
}
component.isEditable = true
component.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
}
}
sdkHomePaths.afterChange {
component.removeAllItems()
it.forEach(component::addItem)
}
return component
}
const val UNKNOWN_EXECUTABLE = "<unknown_executable>"
fun Panel.executableSelector(labelText: @Nls String, executable: ObservableMutableProperty<String>, missingExecutableText: @Nls String): TextFieldWithBrowseButton {
var textFieldComponent: TextFieldWithBrowseButton? = null
row("") {
icon(AllIcons.General.Warning)
text(missingExecutableText) {
FileChooser.chooseFile(FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), null, null) {
executable.set(it.path)
}
}
}.visibleIf(executable.equalsTo(UNKNOWN_EXECUTABLE))
row(labelText) {
textFieldComponent = textFieldWithBrowseButton()
.bindText(executable)
.align(AlignX.FILL)
.component
}.visibleIf(executable.notEqualsTo(UNKNOWN_EXECUTABLE))
return textFieldComponent!!
}