diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyContentEntriesModuleConfigurable.java b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyContentEntriesModuleConfigurable.java deleted file mode 100644 index 1c53d6759033..000000000000 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyContentEntriesModuleConfigurable.java +++ /dev/null @@ -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"; - } - -} diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyContentEntriesModuleConfigurable.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyContentEntriesModuleConfigurable.kt new file mode 100644 index 000000000000..6bcd183c8841 --- /dev/null +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyContentEntriesModuleConfigurable.kt @@ -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 { + 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 { 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 = 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) {} + } + } +} diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyModuleDependenciesConfigurable.java b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyModuleDependenciesConfigurable.java index 33a7edff9475..f634e59f6b80 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyModuleDependenciesConfigurable.java +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyModuleDependenciesConfigurable.java @@ -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")); diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PythonContentEntriesConfigurable.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PythonContentEntriesConfigurable.kt index f3080b121d10..326adf9f24fb 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PythonContentEntriesConfigurable.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PythonContentEntriesConfigurable.kt @@ -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 = 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() + } }