[python] PY-87445 Make pyproject.toml setting visible across Project Structure pages before Apply

GitOrigin-RevId: 8fd3c7a89dc5c3b65759c38e775c95c68900da42
This commit is contained in:
Vitaly Legchilkin
2026-02-25 01:20:01 +00:00
committed by intellij-monorepo-bot
parent 110fe78aab
commit 52d2828b6d
4 changed files with 177 additions and 132 deletions
@@ -1,131 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.configuration;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.impl.ModuleConfigurationStateImpl;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ModuleRootModel;
import com.intellij.openapi.roots.ui.configuration.DefaultModulesProvider;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.module.PyContentEntriesEditor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.model.java.JavaSourceRootType;
import javax.swing.JComponent;
import javax.swing.JPanel;
import java.awt.BorderLayout;
public class PyContentEntriesModuleConfigurable extends SearchableConfigurable.Parent.Abstract {
private final Module myModule;
private final JPanel myTopPanel = new JPanel(new BorderLayout());
protected ModifiableRootModel myModifiableModel;
protected PyContentEntriesEditor myEditor;
public PyContentEntriesModuleConfigurable(final Module module) {
myModule = module;
}
@Override
public String getDisplayName() {
return PyBundle.message("configurable.PyContentEntriesModuleConfigurable.display.name");
}
@Override
public String getHelpTopic() {
return "reference.settingsdialog.project.structure";
}
@Override
public JComponent createComponent() {
createEditor();
return myTopPanel;
}
private void createEditor() {
if (myModule == null) return;
myModifiableModel =
ReadAction.compute(() -> ModuleRootManager.getInstance(myModule).getModifiableModel());
final ModuleConfigurationStateImpl moduleConfigurationState =
new ModuleConfigurationStateImpl(myModule.getProject(), new DefaultModulesProvider(myModule.getProject())) {
@Override
public ModifiableRootModel getModifiableRootModel() {
return myModifiableModel;
}
@Override
public ModuleRootModel getCurrentRootModel() {
return myModifiableModel;
}
};
myEditor = createEditor(myModule, moduleConfigurationState);
JComponent component = ReadAction.compute(() -> myEditor.createComponent());
myTopPanel.add(component, BorderLayout.CENTER);
}
protected PyContentEntriesEditor createEditor(@NotNull Module module, @NotNull ModuleConfigurationStateImpl state) {
return new PyContentEntriesEditor(module, state, true, JavaSourceRootType.SOURCE, JavaSourceRootType.TEST_SOURCE);
}
@Override
public boolean isModified() {
return myEditor != null && myEditor.isModified();
}
@Override
public void apply() throws ConfigurationException {
if (myEditor == null) return;
final boolean editorWasModified = myEditor.isModified();
myEditor.apply();
if (editorWasModified) {
ApplicationManager.getApplication().runWriteAction(() -> myModifiableModel.commit());
resetEditor();
}
}
@Override
public void reset() {
if (myEditor == null) return;
if (myModifiableModel != null) {
myModifiableModel.dispose();
}
resetEditor();
}
private void resetEditor() {
myEditor.disposeUIResources();
myTopPanel.remove(myEditor.getComponent());
createEditor();
}
@Override
public void disposeUIResources() {
if (myEditor != null) {
myEditor.disposeUIResources();
myTopPanel.remove(myEditor.getComponent());
myEditor = null;
}
if (myModifiableModel != null) {
myModifiableModel.dispose();
myModifiableModel = null;
}
}
@Override
protected @NotNull Configurable @NotNull [] buildConfigurables() {
return new Configurable[0];
}
@Override
public @NotNull String getId() {
return "python.project.structure";
}
}
@@ -0,0 +1,158 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.pycharm.community.ide.impl.configuration
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.impl.ModuleConfigurationStateImpl
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ui.configuration.DefaultModulesProvider
import com.intellij.openapi.ui.Splitter
import com.intellij.python.pyproject.model.PyProjectModelSettings
import com.intellij.ui.AncestorListenerAdapter
import com.intellij.ui.roots.IconActionComponent
import com.intellij.ui.roots.ToolbarPanel
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.module.PyContentEntriesEditor
import org.jetbrains.jps.model.java.JavaSourceRootType
import java.awt.BorderLayout
import java.awt.Component
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JPopupMenu
import javax.swing.JScrollPane
import javax.swing.JTextField
import javax.swing.JTree
import javax.swing.event.AncestorEvent
internal class PyContentEntriesModuleConfigurable(private val module: Module) : SearchableConfigurable.Parent.Abstract() {
private val topPanel = JPanel(BorderLayout())
//.apply {
// addAncestorListener(object : AncestorListenerAdapter() {
// override fun ancestorAdded(event: AncestorEvent) {
// updateEnabledState()
// }
// })
//}
private var modifiableModel: ModifiableRootModel? = null
private var editor: PyContentEntriesEditor? = null
private val isPyProjectTomlManaged: Boolean
get() {
val pending = module.project.getUserData(PythonContentEntriesConfigurable.PYPROJECT_TOML_PENDING_KEY)
return pending ?: PyProjectModelSettings.getInstance(module.project).usePyprojectToml
}
override fun getDisplayName(): String = PyBundle.message("configurable.PyContentEntriesModuleConfigurable.display.name")
override fun getHelpTopic(): String = "reference.settingsdialog.project.structure"
override fun createComponent(): JComponent {
createEditor()
return topPanel
}
private fun createEditor() {
val model = ReadAction.computeBlocking<ModifiableRootModel, RuntimeException> {
ModuleRootManager.getInstance(module).modifiableModel
}
modifiableModel = model
val state = object : ModuleConfigurationStateImpl(module.project, DefaultModulesProvider(module.project)) {
override fun getModifiableRootModel(): ModifiableRootModel = model
override fun getCurrentRootModel(): ModifiableRootModel = model
}
val newEditor = createEditor(module, state)
editor = newEditor
val component = ReadAction.computeBlocking<JComponent, RuntimeException> { newEditor.createComponent() }
topPanel.add(component, BorderLayout.CENTER)
}
private fun createEditor(module: Module, state: ModuleConfigurationStateImpl): PyContentEntriesEditor =
PyContentEntriesEditor(module, state, true, JavaSourceRootType.SOURCE, JavaSourceRootType.TEST_SOURCE)
override fun isModified(): Boolean = !isPyProjectTomlManaged && editor?.isModified == true
override fun apply() {
val currentEditor = editor ?: return
val editorWasModified = currentEditor.isModified
currentEditor.apply()
if (editorWasModified) {
ApplicationManager.getApplication().runWriteAction { modifiableModel?.commit() }
resetEditor()
}
//updateEnabledState()
}
override fun reset() {
if (isPyProjectTomlManaged) return
editor ?: return
modifiableModel?.dispose()
resetEditor()
}
private fun resetEditor() {
val currentEditor = editor ?: return
currentEditor.disposeUIResources()
topPanel.remove(currentEditor.component)
createEditor()
}
private fun updateEnabledState() {
val visible = !isPyProjectTomlManaged
UIUtil.findComponentOfType(topPanel, Splitter::class.java)?.let { splitter ->
// Hide the "Mark as" label and action toolbar (siblings of the splitter in contentPanel)
splitter.parent?.let { contentPanel ->
contentPanel.components.filter { it != splitter }.forEach { it.isVisible = visible }
}
// In the right panel (ToolbarPanel): hide the "Add Content Root" toolbar and remove-root icons
val contentRootsPanel = splitter.secondComponent
(contentRootsPanel as? ToolbarPanel)?.let {
it.getComponent(0).isVisible = visible
}
UIUtil.findComponentsOfType(contentRootsPanel, IconActionComponent::class.java).forEach {
it.isVisible = visible
}
}
// Suppress tree context menu without removing PopupHandler listeners
UIUtil.findComponentOfType(topPanel, JTree::class.java)?.let { tree ->
tree.componentPopupMenu = if (visible) null else EMPTY_POPUP
}
// Hide "Exclude patterns" field, its label, and comment (all are siblings of the tree JScrollPane)
UIUtil.findComponentOfType(topPanel, JTextField::class.java)?.let { excludeField ->
excludeField.parent?.let { dialogPanel ->
dialogPanel.components.filter { it !is JScrollPane }.forEach { it.isVisible = visible }
}
}
}
override fun disposeUIResources() {
editor?.let {
it.disposeUIResources()
topPanel.remove(it.component)
}
editor = null
modifiableModel?.dispose()
modifiableModel = null
}
override fun buildConfigurables(): Array<Configurable> = emptyArray()
override fun getId(): String = "python.project.structure"
companion object {
private val EMPTY_POPUP = object : JPopupMenu() {
override fun show(invoker: Component?, x: Int, y: Int) {}
}
}
}
@@ -82,7 +82,8 @@ public class PyModuleDependenciesConfigurable implements UnnamedConfigurable {
}
private void updateEnabledState() {
boolean managed = PyProjectModelSettings.getInstance(myModule.getProject()).getUsePyprojectToml();
Boolean pending = myModule.getProject().getUserData(PythonContentEntriesConfigurable.PYPROJECT_TOML_PENDING_KEY);
boolean managed = pending != null ? pending : PyProjectModelSettings.getInstance(myModule.getProject()).getUsePyprojectToml();
myDependenciesList.setEnabled(!managed);
if (managed) {
myTitleLabel.setText(PyBundle.message("python.pyproject.toml.dependencies.managed.hint"));
@@ -7,6 +7,7 @@ import com.intellij.openapi.options.Configurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ui.configuration.PlatformContentEntriesConfigurable
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.util.Key
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
@@ -24,6 +25,11 @@ class PythonContentEntriesConfigurable(project: Project) : ModuleAwareProjectCon
private var pyprojectPanel: DialogPanel? = null
companion object {
@JvmField
val PYPROJECT_TOML_PENDING_KEY: Key<Boolean> = Key.create("PythonContentEntriesConfigurable.pendingUsePyprojectToml")
}
override fun createModuleConfigurable(module: Module): Configurable {
if (PlatformUtils.isPyCharmCommunity()) {
return PlatformContentEntriesConfigurable(module, JavaSourceRootType.SOURCE)
@@ -41,6 +47,9 @@ class PythonContentEntriesConfigurable(project: Project) : ModuleAwareProjectCon
row {
checkBox(PyBundle.message("python.pyproject.toml.based.project.model"))
.bindSelected(settings::usePyprojectToml)
.applyToComponent {
addActionListener { project.putUserData(PYPROJECT_TOML_PENDING_KEY, isSelected) }
}
.contextHelp(PyBundle.message("python.pyproject.toml.based.project.model.comment"))
}
super.createComponent()?.let { parentComponent ->
@@ -59,11 +68,19 @@ class PythonContentEntriesConfigurable(project: Project) : ModuleAwareProjectCon
override fun apply() {
pyprojectPanel?.apply()
project.putUserData(PYPROJECT_TOML_PENDING_KEY, null)
super.apply()
}
override fun reset() {
pyprojectPanel?.reset()
project.putUserData(PYPROJECT_TOML_PENDING_KEY, null)
super.reset()
}
override fun disposeUIResources() {
project.putUserData(PYPROJECT_TOML_PENDING_KEY, null)
pyprojectPanel = null
super.disposeUIResources()
}
}