diff --git a/python/ide/impl/resources/META-INF/pycharm-community-customization.xml b/python/ide/impl/resources/META-INF/pycharm-community-customization.xml index 204543648e70..d1d22990df92 100644 --- a/python/ide/impl/resources/META-INF/pycharm-community-customization.xml +++ b/python/ide/impl/resources/META-INF/pycharm-community-customization.xml @@ -57,6 +57,9 @@ + + diff --git a/python/ide/impl/src/com/jetbrains/python/PyIdeCommonFragmentsBuilder.kt b/python/ide/impl/src/com/jetbrains/python/PyIdeCommonFragmentsBuilder.kt new file mode 100644 index 000000000000..90ce09d67b5a --- /dev/null +++ b/python/ide/impl/src/com/jetbrains/python/PyIdeCommonFragmentsBuilder.kt @@ -0,0 +1,75 @@ +// 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 + +import com.intellij.application.options.ModulesComboBox +import com.intellij.execution.ui.CommandLinePanel +import com.intellij.execution.ui.SettingsEditorFragment +import com.intellij.execution.ui.SettingsEditorFragmentType +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import com.jetbrains.python.run.PyCommonFragmentsBuilder +import com.jetbrains.python.run.configuration.PyPathMappingsEditorFragment +import com.jetbrains.python.run.configuration.PySdkComboBox +import com.jetbrains.python.sdk.PySdkListCellRenderer +import java.awt.event.ItemEvent + +class PyIdeCommonFragmentsBuilder : PyCommonFragmentsBuilder() { + + override fun > createEnvironmentFragments(fragments: MutableList>, + config: T) { + val modules = ModuleManager.getInstance(config.project).modules + var modulesComboBox: ModulesComboBox? = null + if (modules.size > 1) { + modulesComboBox = ModulesComboBox() + val modulesFragment = SettingsEditorFragment( + "py.ide.project", + PyBundle.message("python.run.configuration.fragments.project"), + null, + modulesComboBox, SettingsEditorFragmentType.COMMAND_LINE, + { s: T, modulesCombo: ModulesComboBox -> + val validModules: List = config.validModules + modulesCombo.setModules(validModules) + modulesCombo.selectedModule = config.module + }, + { s: T, modulesCombo: ModulesComboBox -> + modulesCombo.selectedModule?.let { selected -> + s.module = selected + } + }, + { true } + ) + modulesFragment.isRemovable = false + modulesFragment.setHint(PyBundle.message("python.run.configuration.fragments.project.hint")) + fragments.add(modulesFragment) + } + + val sdkComboBox = PySdkComboBox(true) { modulesComboBox?.selectedModule } + val minimumSize = CommandLinePanel.setMinimumWidth(sdkComboBox, 400) + sdkComboBox.preferredSize = minimumSize + sdkComboBox.renderer = PySdkListCellRenderer() + val interpreterFragment: SettingsEditorFragment = SettingsEditorFragment( + "py.ide.interpreter", + PyBundle.message("python.run.configuration.fragments.interpreter.field"), + null, + sdkComboBox, SettingsEditorFragmentType.COMMAND_LINE, + { s: T, c: PySdkComboBox -> c.reset(s) }, + { s: T, c: PySdkComboBox -> c.apply(s) }, + { true }) + interpreterFragment.isRemovable = false + interpreterFragment.setHint(PyBundle.message("python.run.configuration.fragments.interpreter.field")) + fragments.add(interpreterFragment) + + modulesComboBox?.addItemListener { e -> + if (e?.stateChange == ItemEvent.SELECTED) { + (e.item as? Module)?.let { + sdkComboBox.setModule(it) + } + } + } + + fragments.add(createWorkingDirectoryFragment(config.project)) + fragments.add(createEnvParameters()) + fragments.add(PyPathMappingsEditorFragment(sdkComboBox)) + } +} \ No newline at end of file diff --git a/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml b/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml index a9b9537b72ac..c24f1f3f2fb3 100644 --- a/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml +++ b/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml @@ -5,6 +5,8 @@ + diff --git a/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginCommonFragmentsBuilder.kt b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginCommonFragmentsBuilder.kt new file mode 100644 index 000000000000..60d1943e5b86 --- /dev/null +++ b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginCommonFragmentsBuilder.kt @@ -0,0 +1,19 @@ +// 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.run + +import com.intellij.execution.ui.SettingsEditorFragment +import com.jetbrains.python.run.configuration.PyInterpreterModeNotifier +import com.jetbrains.python.run.configuration.PyPathMappingsEditorFragment +import javax.swing.JPanel + +class PyPluginCommonFragmentsBuilder: PyCommonFragmentsBuilder() { + override fun > createEnvironmentFragments(fragments: MutableList>, + config: T) { + val sdkFragment: SettingsEditorFragment = PyPluginSdkFragment() + fragments.add(sdkFragment) + + fragments.add(createWorkingDirectoryFragment(config.project)) + fragments.add(createEnvParameters()) + fragments.add(PyPathMappingsEditorFragment(sdkFragment as PyInterpreterModeNotifier)) + } +} \ No newline at end of file diff --git a/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginSdkFragment.kt b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginSdkFragment.kt new file mode 100644 index 000000000000..d8bdaef94508 --- /dev/null +++ b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginSdkFragment.kt @@ -0,0 +1,165 @@ +// 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.run + +import com.intellij.application.options.ModulesComboBox +import com.intellij.execution.ui.CommandLinePanel +import com.intellij.execution.ui.SettingsEditorFragment +import com.intellij.execution.ui.SettingsEditorFragmentType +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.ui.ComboBox +import com.jetbrains.python.PyBundle +import com.jetbrains.python.run.configuration.PyInterpreterModeNotifier +import com.jetbrains.python.run.configuration.PySdkComboBox +import com.jetbrains.python.sdk.PythonSdkUtil +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.event.ItemEvent +import java.util.function.Consumer +import javax.swing.JComboBox +import javax.swing.JComponent +import javax.swing.JPanel + +class PyPluginSdkFragment> : SettingsEditorFragment( + "py.plugin.interpreter", null, null, JPanel(GridBagLayout()), SettingsEditorFragmentType.COMMAND_LINE, null, null, + { true }), PyInterpreterModeNotifier { + + private val typeNames = listOf(PyBundle.message("python.run.configuration.fragments.plugin.sdk.of.module"), + PyBundle.message("python.run.configuration.fragments.plugin.specified.interpreter")) + private val SDK_OF_MODULE = 0 + private val SDK_FROM_LIST = 1 + private var currentMode: Int? = null + + private var modulesCombo: ModulesComboBox? = null + private var interpreterCombo: PySdkComboBox? = null + + val fields: MutableList = MutableList(SDK_FROM_LIST + 1) { null } + private val hints: MutableMap = mutableMapOf() + private val sdkChooser: JComboBox + private val interpreterModeListeners: MutableList> = mutableListOf() + + init { + sdkChooser = JComboBox() + sdkChooser.addItem(typeNames[SDK_OF_MODULE]) + sdkChooser.addItem(typeNames[SDK_FROM_LIST]) + CommandLinePanel.setMinimumWidth(sdkChooser, 200) + component().add(sdkChooser, GridBagConstraints()) + hints[sdkChooser] = PyBundle.message("python.run.configuration.fragments.plugin.sdk.chooser.hint") + + sdkChooser.addItemListener { e -> + if (e?.stateChange == ItemEvent.SELECTED) { + if ((e.item as? String) == typeNames[SDK_FROM_LIST]) { + showField(SDK_FROM_LIST) + currentMode = SDK_FROM_LIST + } + else { + showField(SDK_OF_MODULE) + currentMode = SDK_OF_MODULE + } + } + } + sdkChooser.addActionListener { updateRemoteInterpreterMode() } + } + + override fun resetEditorFrom(config: T) { + if (currentMode == null) { + initComponents(config) + } + val type = if (config.isUseModuleSdk) SDK_OF_MODULE else SDK_FROM_LIST + if (currentMode != type) { + sdkChooser.selectedItem = typeNames[type] + (fields[SDK_OF_MODULE] as? ComboBox<*>)?.selectedItem = config.module + interpreterCombo?.reset(config) + showField(type) + } + updateRemoteInterpreterMode() + } + + private fun showField(type: Int) { + for (i in 0 until fields.size) { + fields[i]?.isVisible = i == type + } + } + + private fun initComponents(config: T) { + val modulesComponent = ModulesComboBox() + initComponent(modulesComponent, SDK_OF_MODULE) + val validModules: List = config.commonOptionsFormData.validModules + modulesComponent.setModules(validModules) + modulesComponent.selectedModule = config.module + fields[SDK_OF_MODULE] = modulesComponent + modulesCombo = modulesComponent + CommandLinePanel.setMinimumWidth(modulesCombo, 400) + hints[modulesComponent] = PyBundle.message("python.run.configuration.fragments.plugin.sdk.of.module.hint") + modulesCombo?.addActionListener { updateRemoteInterpreterMode() } + + val interpreterComponent = PySdkComboBox(false) { modulesComponent.selectedModule } + initComponent(interpreterComponent, SDK_FROM_LIST) + interpreterComponent.reset(config) + fields[SDK_FROM_LIST] = interpreterComponent + interpreterCombo = interpreterComponent + CommandLinePanel.setMinimumWidth(interpreterCombo, 400) + hints[interpreterComponent] = PyBundle.message("python.run.configuration.fragments.plugin.specified.interpreter.hint") + interpreterCombo?.addActionListener { updateRemoteInterpreterMode() } + } + + private fun initComponent(field: JComponent, index: Int) { + val constraints = GridBagConstraints() + constraints.fill = GridBagConstraints.BOTH + constraints.weightx = 1.0 + component().add(field, constraints) + fields[index] = field + } + + override fun applyEditorTo(s: T) { + if (currentMode == SDK_OF_MODULE) { + s.isUseModuleSdk = true + val module = (modulesCombo?.selectedItem) as? Module + s.module = module + module?.let { + val moduleSdk = ModuleRootManager.getInstance(it).sdk + s.setSdk(moduleSdk) + } + } + else if (currentMode == SDK_FROM_LIST) { + s.isUseModuleSdk = false + interpreterCombo?.apply(s) + } + } + + override fun isRemovable(): Boolean = false + + override fun getAllComponents(): Array { + return fields.filterNotNull().toTypedArray() + sdkChooser + } + + override fun getHint(component: JComponent?): String? { + return if (component == null) null else hints[component] + } + + override fun isRemoteSelected(): Boolean = PythonSdkUtil.isRemote(getSelectedSdk()) + + override fun addInterpreterModeListener(listener: Consumer) { + interpreterModeListeners.add(listener) + } + + private fun updateRemoteInterpreterMode() { + val isRemote = isRemoteSelected() + for (listener in interpreterModeListeners) { + listener.accept(isRemote) + } + } + + private fun getSelectedSdk(): Sdk? { + if (currentMode == SDK_OF_MODULE) { + ((modulesCombo?.selectedItem) as? Module)?.let { + return ModuleRootManager.getInstance(it).sdk + } + } + else if (currentMode == SDK_FROM_LIST) { + return interpreterCombo?.getSelectedSdk() + } + return null + } +} \ No newline at end of file diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 4186388c6126..a028739818cf 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -1030,6 +1030,31 @@ form.tox.configuration.environments.to.run=Environments to run: form.tox.configuration.runcfg.tox=tox py.module.dependencies.configurable.list.title=Project depends on these projects: py.sdk.editor.python.interpreter.label.text=Python Interpreter: + +python.run.configuration.fragments.chooser.hint=Run Python script or module +python.run.configuration.fragments.script.path.hint=Path to a Python script +python.run.configuration.fragments.module.name.hint=Name of a module +python.run.configuration.fragments.project=Project +python.run.configuration.fragments.project.hint=Select a project +python.run.configuration.fragments.interpreter.field=Python interpreter +python.run.configuration.fragments.interpreter.options=Interpreter options +python.run.configuration.fragments.interpreter.options.hint=Python interpreter options +python.run.configuration.fragments.script.parameters=Parameters +python.run.configuration.fragments.script.parameters.hint=Script parameters +python.run.configuration.fragments.run.with.python.console=Run with Python Console +python.run.configuration.fragments.run.with.python.console.hint=Execute run configuration in Python Console +python.run.configuration.fragments.emulate.terminal=Emulate terminal in output console +python.run.configuration.fragments.emulate.terminal.hint=Emulate terminal in output console +python.run.configuration.fragments.content.roots=Add content roots to PYTHONPATH +python.run.configuration.fragments.content.roots.hint=Add content roots to PYTHONPATH +python.run.configuration.fragments.source.roots=Add source roots to PYTHONPATH +python.run.configuration.fragments.source.roots.hint=Add source roots to PYTHONPATH +python.run.configuration.fragments.python.group=Python +python.run.configuration.fragments.plugin.sdk.chooser.hint=Python interpreter +python.run.configuration.fragments.plugin.sdk.of.module=Use SDK of module +python.run.configuration.fragments.plugin.sdk.of.module.hint=Select module +python.run.configuration.fragments.plugin.specified.interpreter=Use specified interpreter +python.run.configuration.fragments.plugin.specified.interpreter.hint=Select Python interpreter # Python reStructuredText forms rest.configuration.editor.open.output.file.in.browser.label.text=Open output file in browser sphinx.ask.for.working.directory.label.text=Sphinx working directory diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index ac8a734cdf38..b5e79fa2735e 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -508,6 +508,8 @@ + + diff --git a/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java b/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java index 4f76634eef13..92f794ce395f 100644 --- a/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java +++ b/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java @@ -19,6 +19,7 @@ import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.JDOMExternalizerUtil; import com.intellij.openapi.util.WriteExternalException; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.PathMappingSettings; @@ -119,6 +120,11 @@ public abstract class AbstractPythonRunConfiguration getConfigurationEditor() { + if (Registry.is("ide.new.run.config", false)) { + // TODO: actually, we should return result of `PythonExtendedConfigurationEditor.create()` call, but it produces side effects + // investigation needed PY-17716 + return createConfigurationEditor(); + } final SettingsEditor runConfigurationEditor = PythonExtendedConfigurationEditor.create(createConfigurationEditor()); final SettingsEditorGroup group = new SettingsEditorGroup<>(); diff --git a/python/src/com/jetbrains/python/run/PyCommonFragmentsBuilder.kt b/python/src/com/jetbrains/python/run/PyCommonFragmentsBuilder.kt new file mode 100644 index 000000000000..9b667c78cf71 --- /dev/null +++ b/python/src/com/jetbrains/python/run/PyCommonFragmentsBuilder.kt @@ -0,0 +1,80 @@ +// 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.run + +import com.intellij.execution.ExecutionBundle +import com.intellij.execution.configuration.EnvironmentVariablesComponent +import com.intellij.execution.ui.CommonParameterFragments +import com.intellij.execution.ui.SettingsEditorFragment +import com.intellij.openapi.components.Service +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.LabeledComponent +import com.intellij.openapi.ui.TextComponentAccessor +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.util.Predicates +import com.intellij.ui.components.fields.ExtendableTextField +import java.awt.BorderLayout +import javax.swing.JComponent + +@Service +abstract class PyCommonFragmentsBuilder { + abstract fun > createEnvironmentFragments(fragments: MutableList>, + config: T) + + fun > createWorkingDirectoryFragment(project: Project): + SettingsEditorFragment> { + val textField = ExtendableTextField(10) + val workingDirectoryField = TextFieldWithBrowseButton(textField) + workingDirectoryField.addBrowseFolderListener( + ExecutionBundle.message("select.working.directory.message"), + null, + project, + FileChooserDescriptorFactory.createSingleFolderDescriptor(), + TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT + ) + val field = LabeledComponent.create( + workingDirectoryField, + ExecutionBundle.message("run.configuration.working.directory.label"), + BorderLayout.WEST + ) + val workingDirectorySettings = SettingsEditorFragment( + "workingDirectory", + ExecutionBundle.message("run.configuration.working.directory.name"), null, field, + { config: T, component: LabeledComponent -> component.component.text = config.workingDirectory }, + { config: T, component: LabeledComponent -> config.workingDirectory = component.component.text }, + Predicates.alwaysTrue() + ) + workingDirectorySettings.isRemovable = false + return workingDirectorySettings + } + + fun > createEnvParameters(): SettingsEditorFragment { + val env = EnvironmentVariablesComponent() + env.labelLocation = BorderLayout.WEST + CommonParameterFragments.setMonospaced(env.component.textField) + val fragment = SettingsEditorFragment( + "environmentVariables", + ExecutionBundle.message("environment.variables.fragment.name"), + ExecutionBundle.message("group.operating.system"), env, + { config: T, c: JComponent? -> + env.envs = config.envs + env.isPassParentEnvs = config.isPassParentEnvs + }, + { config: T, _: JComponent? -> + if (!env.isVisible) { + config.envs = emptyMap() + config.isPassParentEnvs = true + } + else { + config.envs = env.envs + config.isPassParentEnvs = env.isPassParentEnvs + } + }, + { true }) + fragment.isCanBeHidden = true + fragment.setHint(ExecutionBundle.message("environment.variables.fragment.hint")) + fragment.actionHint = ExecutionBundle.message("set.custom.environment.variables.for.the.process") + return fragment + } + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/PythonRunConfiguration.java b/python/src/com/jetbrains/python/run/PythonRunConfiguration.java index e1a40ca7dc11..8ec03a1fd903 100644 --- a/python/src/com/jetbrains/python/run/PythonRunConfiguration.java +++ b/python/src/com/jetbrains/python/run/PythonRunConfiguration.java @@ -11,6 +11,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.JDOMExternalizerUtil; import com.intellij.openapi.util.WriteExternalException; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; @@ -18,6 +19,7 @@ import com.intellij.psi.PsiFile; import com.intellij.refactoring.listeners.RefactoringElementAdapter; import com.intellij.refactoring.listeners.RefactoringElementListener; import com.jetbrains.python.PyBundle; +import com.jetbrains.python.run.configuration.PythonConfigurationFragmentedEditor; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -54,6 +56,9 @@ public class PythonRunConfiguration extends AbstractPythonRunConfiguration @Override protected SettingsEditor createConfigurationEditor() { + if (Registry.is("python.new.run.config", false)) { + return new PythonConfigurationFragmentedEditor(this); + } return new PythonRunConfigurationEditor(this); } diff --git a/python/src/com/jetbrains/python/run/configuration/AbstractPythonConfigurationFragmentedEditor.kt b/python/src/com/jetbrains/python/run/configuration/AbstractPythonConfigurationFragmentedEditor.kt new file mode 100644 index 000000000000..bf5ae3fb0a5a --- /dev/null +++ b/python/src/com/jetbrains/python/run/configuration/AbstractPythonConfigurationFragmentedEditor.kt @@ -0,0 +1,76 @@ +// 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.run.configuration + +import com.intellij.diagnostic.logging.LogsGroupFragment +import com.intellij.execution.ui.* +import com.intellij.openapi.components.service +import com.intellij.ui.RawCommandLineEditor +import com.jetbrains.python.PyBundle +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import com.jetbrains.python.run.PyCommonFragmentsBuilder +import com.jetbrains.python.run.PythonRunConfigurationExtensionsManager + +abstract class AbstractPythonConfigurationFragmentedEditor>(val runConfiguration: T) : + RunConfigurationFragmentedEditor(runConfiguration, PythonRunConfigurationExtensionsManager.instance) { + + override fun createRunFragments(): MutableList> { + val fragments: MutableList> = ArrayList() + val beforeRunComponent = BeforeRunComponent(this) + fragments.add(BeforeRunFragment.createBeforeRun(beforeRunComponent, null)) + fragments.addAll(BeforeRunFragment.createGroup()) + fragments.add(CommonParameterFragments.createRunHeader()) + + createEnvironmentFragments(fragments, runConfiguration) + addInterpreterOptions(fragments) + addContentSourceRoots(fragments) + + customizeFragments(fragments) + + fragments.add(LogsGroupFragment()) + return fragments + } + + private fun > createEnvironmentFragments(fragments: MutableList>, + runConfiguration: T) { + service().createEnvironmentFragments(fragments, runConfiguration) + } + + abstract fun customizeFragments(fragments: MutableList>) + + private fun > addInterpreterOptions(fragments: MutableList>) { + val interpreterOptionsField = RawCommandLineEditor() + val interpreterOptionsFragment: SettingsEditorFragment = SettingsEditorFragment( + "py.interpreter.options", + PyBundle.message("python.run.configuration.fragments.interpreter.options"), + PyBundle.message("python.run.configuration.fragments.python.group"), + interpreterOptionsField, SettingsEditorFragmentType.COMMAND_LINE, + { config: T, field: RawCommandLineEditor -> field.text = config.interpreterOptions }, + { config: T, field: RawCommandLineEditor -> config.interpreterOptions = field.text.trim() }, + { config: T -> !config.interpreterOptions.trim().isEmpty() }) + interpreterOptionsFragment.setHint(PyBundle.message("python.run.configuration.fragments.interpreter.options.hint")) + interpreterOptionsFragment.actionHint = PyBundle.message("python.run.configuration.fragments.interpreter.options.hint") + fragments.add(interpreterOptionsFragment) + } + + private fun > addContentSourceRoots(fragments: MutableList>) { + val addContentRoots = SettingsEditorFragment.createTag( + "py.add.content.roots", + PyBundle.message("python.run.configuration.fragments.content.roots"), + PyBundle.message("python.run.configuration.fragments.python.group"), + { it.shouldAddContentRoots() }, + { config, value -> config.setAddContentRoots(value) }) + + addContentRoots.actionHint = PyBundle.message("python.run.configuration.fragments.content.roots.hint") + fragments.add(addContentRoots) + + val addSourceRoots = SettingsEditorFragment.createTag( + "py.add.content.roots", + PyBundle.message("python.run.configuration.fragments.source.roots"), + PyBundle.message("python.run.configuration.fragments.python.group"), + { it.shouldAddContentRoots() }, + { config, value -> config.setAddContentRoots(value) }) + + addSourceRoots.actionHint = PyBundle.message("python.run.configuration.fragments.source.roots.hint") + fragments.add(addSourceRoots) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/configuration/PyInterpreterModeNotifier.kt b/python/src/com/jetbrains/python/run/configuration/PyInterpreterModeNotifier.kt new file mode 100644 index 000000000000..d0d6aaa69c0a --- /dev/null +++ b/python/src/com/jetbrains/python/run/configuration/PyInterpreterModeNotifier.kt @@ -0,0 +1,10 @@ +// 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.run.configuration + +import java.util.function.Consumer + +interface PyInterpreterModeNotifier { + fun addInterpreterModeListener(listener: Consumer) + + fun isRemoteSelected(): Boolean +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/configuration/PyPathMappingsEditorFragment.kt b/python/src/com/jetbrains/python/run/configuration/PyPathMappingsEditorFragment.kt new file mode 100644 index 000000000000..3bc7de22f3ae --- /dev/null +++ b/python/src/com/jetbrains/python/run/configuration/PyPathMappingsEditorFragment.kt @@ -0,0 +1,49 @@ +// 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.run.configuration + +import com.intellij.execution.ui.CommandLinePanel +import com.intellij.execution.ui.SettingsEditorFragment +import com.intellij.execution.util.PathMappingsComponent +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import java.awt.BorderLayout +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.JComponent +import javax.swing.JPanel + +class PyPathMappingsEditorFragment>(val notifier: PyInterpreterModeNotifier) : + SettingsEditorFragment("py.path.mappings", null, null, JPanel(GridBagLayout()), null, null, { true }) { + private val pathMappingsComponent = PathMappingsComponent() + + init { + pathMappingsComponent.labelLocation = BorderLayout.WEST + CommandLinePanel.setMinimumWidth(component(), 500) + val constrains = GridBagConstraints() + constrains.fill = GridBagConstraints.HORIZONTAL + constrains.weightx = 1.0 + constrains.gridx = 0 + + component().add(pathMappingsComponent, constrains) + pathMappingsComponent.isVisible = notifier.isRemoteSelected() + notifier.addInterpreterModeListener { isRemote -> + pathMappingsComponent.isVisible = isRemote + } + } + + override fun getAllComponents(): Array { + return arrayOf(pathMappingsComponent) + } + + override fun resetEditorFrom(config: T) { + pathMappingsComponent.setMappingSettings(config.mappingSettings) + } + + override fun applyEditorTo(s: T) { + s.mappingSettings = pathMappingsComponent.mappingSettings + } + + override fun isRemovable(): Boolean = false + + override fun isSelected(): Boolean = true +} + diff --git a/python/src/com/jetbrains/python/run/configuration/PyScriptOrModuleFragment.kt b/python/src/com/jetbrains/python/run/configuration/PyScriptOrModuleFragment.kt new file mode 100644 index 000000000000..69a51781782d --- /dev/null +++ b/python/src/com/jetbrains/python/run/configuration/PyScriptOrModuleFragment.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.run.configuration + +import com.intellij.execution.ui.CommandLinePanel +import com.intellij.execution.ui.SettingsEditorFragment +import com.intellij.execution.ui.SettingsEditorFragmentType +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.ui.TextBrowseFolderListener +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.psi.PsiFileSystemItem +import com.intellij.ui.TextAccessor +import com.jetbrains.PySymbolFieldWithBrowseButton +import com.jetbrains.extensions.ModuleBasedContextAnchor +import com.jetbrains.extensions.ProjectSdkContextAnchor +import com.jetbrains.isPythonModule +import com.jetbrains.python.PyBundle +import com.jetbrains.python.run.PythonRunConfiguration +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.event.ItemEvent +import javax.swing.JComboBox +import javax.swing.JComponent +import javax.swing.JPanel + + +class PyScriptOrModuleFragment : SettingsEditorFragment( + "py.script.field", null, null, JPanel(GridBagLayout()), SettingsEditorFragmentType.COMMAND_LINE, null, null, { true }) { + + private val typeNames = listOf("script", "module") + private val SCRIPT_MODE = 0 + private val MODULE_MODE = 1 + private var currentMode: Int? = null + + val fields: MutableList = MutableList(MODULE_MODE + 1) { null } + val hints: MutableMap = mutableMapOf() + + private val moduleModeChooser: JComboBox + + init { + CommandLinePanel.setMinimumWidth(component(), 500) + moduleModeChooser = JComboBox() + moduleModeChooser.addItem(typeNames[SCRIPT_MODE]) + moduleModeChooser.addItem(typeNames[MODULE_MODE]) + component().add(moduleModeChooser, GridBagConstraints()) + hints[moduleModeChooser] = PyBundle.message("python.run.configuration.fragments.chooser.hint") + + moduleModeChooser.addItemListener { e -> + if (e?.stateChange == ItemEvent.SELECTED) { + if ((e.item as? String) == typeNames[MODULE_MODE]) { + showField(MODULE_MODE) + currentMode = MODULE_MODE + } + else { + showField(SCRIPT_MODE) + currentMode = SCRIPT_MODE + } + } + } + } + + override fun resetEditorFrom(config: PythonRunConfiguration) { + if (currentMode == null) { + initComponents(config) + } + val type = if (config.isModuleMode) MODULE_MODE else SCRIPT_MODE + if (currentMode != type) { + currentMode = type + moduleModeChooser.selectedItem = typeNames[type] + showField(type) + (fields[type] as TextAccessor).text = config.scriptName + } + } + + private fun showField(type: Int) { + for (i in 0 until fields.size) { + fields[i]?.isVisible = i == type + } + } + + private fun initComponents(config: PythonRunConfiguration) { + val scriptField = TextFieldWithBrowseButton() + scriptField.addBrowseFolderListener(TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor(), config.project)) + initComponent(scriptField, SCRIPT_MODE) + fields[SCRIPT_MODE] = scriptField + hints[scriptField] = PyBundle.message("python.run.configuration.fragments.script.path.hint") + + val module = config.module + val contentAnchor = if (module == null) ProjectSdkContextAnchor(config.project, config.sdk) else ModuleBasedContextAnchor(module) + val workingDirectory = config.workingDirectory + val moduleField = PySymbolFieldWithBrowseButton( + contentAnchor, + { element -> (element is PsiFileSystemItem && isPythonModule(element)) }, + if (StringUtil.isEmpty(workingDirectory)) { + null + } + else { + { LocalFileSystem.getInstance().findFileByPath(workingDirectory)!! } + }) + initComponent(moduleField, MODULE_MODE) + fields[MODULE_MODE] = moduleField + hints[moduleField] = PyBundle.message("python.run.configuration.fragments.module.name.hint") + } + + private fun initComponent(field: JComponent, index: Int) { + val constraints = GridBagConstraints() + constraints.fill = GridBagConstraints.BOTH + constraints.weightx = 1.0 + component().add(field, constraints) + fields[index] = field + } + + override fun applyEditorTo(s: PythonRunConfiguration) { + val mode = currentMode + if (mode == null) return + s.isModuleMode = mode == MODULE_MODE + val text = (fields[mode] as? TextAccessor)?.text?.trim() + if (mode == MODULE_MODE) { + s.scriptName = text + } + else { + s.scriptName = text?.let { FileUtil.toSystemIndependentName(it) } + } + } + + override fun isRemovable(): Boolean = false + + override fun getAllComponents(): Array { + return fields.filterNotNull().toTypedArray() + moduleModeChooser + } + + override fun getHint(component: JComponent?): String? { + return if (component == null) null else hints[component] + } +} diff --git a/python/src/com/jetbrains/python/run/configuration/PySdkComboBox.kt b/python/src/com/jetbrains/python/run/configuration/PySdkComboBox.kt new file mode 100644 index 000000000000..e1ff8764128c --- /dev/null +++ b/python/src/com/jetbrains/python/run/configuration/PySdkComboBox.kt @@ -0,0 +1,93 @@ +// 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.run.configuration + +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.Computable +import com.jetbrains.python.PyBundle +import com.jetbrains.python.run.AbstractPythonRunConfigurationParams +import com.jetbrains.python.sdk.PySdkListCellRenderer +import com.jetbrains.python.sdk.PythonSdkUtil +import java.util.function.Consumer + +class PySdkComboBox(private val addDefault: Boolean, + private val moduleProvider: Computable) : ComboBox(), PyInterpreterModeNotifier { + private val interpreterModeListeners: MutableList> = mutableListOf() + + fun reset(config: AbstractPythonRunConfigurationParams) { + initList() + selectedItem = config.sdk + setUseModuleSdk(config.isUseModuleSdk) + setModule(config.module) + addActionListener { + updateRemoteInterpreterMode() + } + updateRemoteInterpreterMode() + } + + private fun initList() { + val pythonSdks: MutableList = PythonSdkUtil.getAllSdks().toMutableList() + if (addDefault) { + pythonSdks.add(0, null) + } + for (item in pythonSdks) { + addItem(item) + } + } + + fun apply(config: AbstractPythonRunConfigurationParams) { + config.sdk = selectedItem as? Sdk + config.isUseModuleSdk = isUseModuleSdk() + } + + fun setModule(module: Module?) { + updateDefaultInterpreter(module) + updateRemoteInterpreterMode() + } + + private fun updateDefaultInterpreter(module: Module?) { + val sdk = if (module == null) null else ModuleRootManager.getInstance(module).sdk + setRenderer( + if (sdk == null) PySdkListCellRenderer() + else PySdkListCellRenderer(PyBundle.message("python.sdk.rendering.project.default.0", sdk.name), sdk) + ) + } + + private fun setUseModuleSdk(useModuleSdk: Boolean) { + if (selectedItem != null && useModuleSdk) { + selectedItem = null + } + } + + private fun isUseModuleSdk(): Boolean = addDefault && selectedItem == null + + fun getSelectedSdk(): Sdk? { + val selectedSdk = selectedItem as? Sdk + if (selectedSdk != null) { + return selectedSdk + } + else { + if (isUseModuleSdk()) { + moduleProvider.get()?.let { + return@getSelectedSdk PythonSdkUtil.findPythonSdk(it) + } + } + } + return null + } + + override fun isRemoteSelected(): Boolean = PythonSdkUtil.isRemote(getSelectedSdk()) + + private fun updateRemoteInterpreterMode() { + val isRemote = isRemoteSelected() + for (listener in interpreterModeListeners) { + listener.accept(isRemote) + } + } + + override fun addInterpreterModeListener(listener: Consumer) { + interpreterModeListeners.add(listener) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/configuration/PythonConfigurationFragmentedEditor.kt b/python/src/com/jetbrains/python/run/configuration/PythonConfigurationFragmentedEditor.kt new file mode 100644 index 000000000000..80918abc91cb --- /dev/null +++ b/python/src/com/jetbrains/python/run/configuration/PythonConfigurationFragmentedEditor.kt @@ -0,0 +1,100 @@ +// 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.run.configuration + +import com.intellij.execution.ExecutionBundle +import com.intellij.execution.ui.SettingsEditorFragment +import com.intellij.execution.ui.SettingsEditorFragmentType +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.ui.LabeledComponent +import com.intellij.openapi.ui.TextBrowseFolderListener +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.RawCommandLineEditor +import com.jetbrains.python.PyBundle +import com.jetbrains.python.run.PythonRunConfiguration +import java.awt.BorderLayout +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent + +class PythonConfigurationFragmentedEditor(runConfiguration: PythonRunConfiguration) : + AbstractPythonConfigurationFragmentedEditor(runConfiguration) { + + override fun customizeFragments(fragments: MutableList>) { + fragments.add(PyScriptOrModuleFragment()) + + val scriptParametersFragment: SettingsEditorFragment = SettingsEditorFragment( + "py.script.parameters", + PyBundle.message("python.run.configuration.fragments.script.parameters"), + PyBundle.message("python.run.configuration.fragments.python.group"), + RawCommandLineEditor(), SettingsEditorFragmentType.COMMAND_LINE, + { config: PythonRunConfiguration, field: RawCommandLineEditor -> field.text = config.scriptParameters }, + { config: PythonRunConfiguration, field: RawCommandLineEditor -> config.scriptParameters = field.text.trim() }, + { config: PythonRunConfiguration -> !config.scriptParameters.trim().isEmpty() }) + scriptParametersFragment.setHint(PyBundle.message("python.run.configuration.fragments.script.parameters.hint")) + scriptParametersFragment.actionHint = PyBundle.message("python.run.configuration.fragments.script.parameters.hint") + fragments.add(scriptParametersFragment) + + + val runWithConsole = SettingsEditorFragment.createTag( + "py.run.with.python.console", + PyBundle.message("python.run.configuration.fragments.run.with.python.console"), + PyBundle.message("python.run.configuration.fragments.python.group"), + { it.showCommandLineAfterwards() }, + { config, value -> config.setShowCommandLineAfterwards(value) }) + runWithConsole.actionHint = PyBundle.message("python.run.configuration.fragments.run.with.python.console.hint") + fragments.add(runWithConsole) + + val emulateTerminal = SettingsEditorFragment.createTag( + "py.emulate.terminal", + PyBundle.message("python.run.configuration.fragments.emulate.terminal"), + PyBundle.message("python.run.configuration.fragments.python.group"), + { it.emulateTerminal() }, + { config, value -> config.setEmulateTerminal(value) }) + emulateTerminal.actionHint = PyBundle.message("python.run.configuration.fragments.emulate.terminal.hint") + fragments.add(emulateTerminal) + + val inputFile = TextFieldWithBrowseButton() + inputFile.addBrowseFolderListener(TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor(), + runConfiguration.project)) + val labeledComponent = LabeledComponent.create(inputFile, ExecutionBundle.message("redirect.input.from")) + labeledComponent.labelLocation = BorderLayout.WEST + val redirectInputFrom: SettingsEditorFragment> = + SettingsEditorFragment>( + "py.redirect.input", + ExecutionBundle.message("redirect.input.from.name"), + ExecutionBundle.message("group.operating.system"), + labeledComponent, SettingsEditorFragmentType.COMMAND_LINE, + { config: PythonRunConfiguration, component: LabeledComponent -> + component.component.text = config.inputFile + }, + { config: PythonRunConfiguration, component: LabeledComponent -> + val filePath = component.component.text + config.isRedirectInput = component.isVisible && StringUtil.isNotEmpty(filePath) + config.inputFile = filePath + }, + { config: PythonRunConfiguration -> config.isRedirectInput }) + redirectInputFrom.actionHint = ExecutionBundle.message("read.input.from.the.specified.file") + redirectInputFrom.setHint(ExecutionBundle.message("read.input.from.the.specified.file")) + fragments.add(redirectInputFrom) + + val editors = mutableListOf>() + editors.add(runWithConsole) + editors.add(emulateTerminal) + editors.add(redirectInputFrom) + addSingleSelectionListeners(editors) + } + + private fun addSingleSelectionListeners(editors: MutableList>) { + for ((i, editor) in editors.withIndex()) { + editor.component().addComponentListener(object : ComponentAdapter() { + override fun componentShown(e: ComponentEvent?) { + for ((j, otherEditor) in editors.withIndex()) { + if (i != j) { + otherEditor.isSelected = false + } + } + } + }) + } + } +} \ No newline at end of file