diff --git a/python/build/pycharm_build.gant b/python/build/pycharm_build.gant index 6ca80732d84d..73910d56afaf 100644 --- a/python/build/pycharm_build.gant +++ b/python/build/pycharm_build.gant @@ -26,8 +26,8 @@ private List pycharmPlatformApiModules() { private List pycharmImplementationModules() { //modules to put into pycharm.jar - return ["yaml", "graph", "coverage-common", "python-community", "python", "python-ide", "python-openapi", "python-psi-api", - "duplicates", "duplicates-xml"] + return ["yaml", "graph", "coverage-common", "python-community", "python", "python-ide", "python-ide-community", + "python-openapi", "python-psi-api", "duplicates", "duplicates-xml"] } class Paths { diff --git a/python/ide/python-ide-community.iml b/python/ide/python-ide-community.iml new file mode 100644 index 000000000000..9cfa2aa868d5 --- /dev/null +++ b/python/ide/python-ide-community.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/python/ide/src/com/jetbrains/python/PlatformPythonModuleType.java b/python/ide/src/com/jetbrains/python/PlatformPythonModuleType.java new file mode 100644 index 000000000000..130e3e0ad974 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PlatformPythonModuleType.java @@ -0,0 +1,21 @@ +package com.jetbrains.python; + +import com.intellij.ide.util.projectWizard.EmptyModuleBuilder; +import com.intellij.openapi.module.ModuleType; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class PlatformPythonModuleType extends PythonModuleTypeBase { + @NotNull + @Override + public EmptyModuleBuilder createModuleBuilder() { + return new EmptyModuleBuilder() { + @Override + public ModuleType getModuleType() { + return getInstance(); + } + }; + } +} diff --git a/python/ide/src/com/jetbrains/python/PyCharmInitialConfigurator.java b/python/ide/src/com/jetbrains/python/PyCharmInitialConfigurator.java new file mode 100644 index 000000000000..2d18eb67e6dd --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PyCharmInitialConfigurator.java @@ -0,0 +1,79 @@ +package com.jetbrains.python; + +import com.intellij.application.options.InitialConfigurationDialog; +import com.intellij.codeInsight.CodeInsightSettings; +import com.intellij.ide.AppLifecycleListener; +import com.intellij.ide.OpenJdkNotifier; +import com.intellij.ide.ui.UISettings; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.wm.WindowManager; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.util.messages.MessageBus; +import com.jetbrains.python.codeInsight.PyCodeInsightSettings; +import org.jetbrains.annotations.NonNls; + +import javax.swing.*; + +/** + * @author yole + */ +@SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "UtilityClassWithPublicConstructor"}) +public class PyCharmInitialConfigurator { + @NonNls private static final String DISPLAYED_PROPERTY = "PyCharm.initialConfigurationShown"; + + public PyCharmInitialConfigurator(MessageBus bus, final PropertiesComponent propertiesComponent, final FileTypeManager fileTypeManager) { + OpenJdkNotifier.checkJdk(bus); + if (!propertiesComponent.getBoolean("PyCharm.InitialConfiguration", false)) { + propertiesComponent.setValue("PyCharm.InitialConfiguration", "true"); + EditorSettingsExternalizable.getInstance().setVirtualSpace(false); + } + if (!propertiesComponent.getBoolean("PyCharm.InitialConfiguration.V2", false)) { + propertiesComponent.setValue("PyCharm.InitialConfiguration.V2", "true"); + final CodeStyleSettings settings = CodeStyleSettingsManager.getInstance().getCurrentSettings(); + settings.ALIGN_MULTILINE_PARAMETERS_IN_CALLS = true; + settings.getCommonSettings(PythonLanguage.getInstance()).ALIGN_MULTILINE_PARAMETERS_IN_CALLS = true; + UISettings.getInstance().SHOW_DIRECTORY_FOR_NON_UNIQUE_FILENAMES = true; + } + if (!propertiesComponent.getBoolean("PyCharm.InitialConfiguration.V3", false)) { + propertiesComponent.setValue("PyCharm.InitialConfiguration.V3", "true"); + UISettings.getInstance().SHOW_MEMORY_INDICATOR = false; + final String ignoredFilesList = fileTypeManager.getIgnoredFilesList(); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + FileTypeManager.getInstance().setIgnoredFilesList(ignoredFilesList + ";*$py.class"); + } + }); + } + if (!propertiesComponent.getBoolean("PyCharm.InitialConfiguration.V4", false)) { + propertiesComponent.setValue("PyCharm.InitialConfiguration.V4", "true"); + PyCodeInsightSettings.getInstance().SHOW_IMPORT_POPUP = false; + } + if (!propertiesComponent.getBoolean("PyCharm.InitialConfiguration.V5", false)) { + propertiesComponent.setValue("PyCharm.InitialConfiguration.V5", "true"); + CodeInsightSettings.getInstance().REFORMAT_ON_PASTE = CodeInsightSettings.NO_REFORMAT; + } + if (!propertiesComponent.isValueSet(DISPLAYED_PROPERTY)) { + bus.connect().subscribe(AppLifecycleListener.TOPIC, new AppLifecycleListener.Adapter() { + @Override + public void welcomeScreenDisplayed() { + ApplicationManager.getApplication().invokeLater(new Runnable() { + public void run() { + propertiesComponent.setValue(DISPLAYED_PROPERTY, "true"); + showInitialConfigurationDialog(); + } + }); + } + }); + } + } + + private static void showInitialConfigurationDialog() { + final JFrame frame = WindowManager.getInstance().findVisibleFrame(); + new InitialConfigurationDialog(frame, "Python").show(); + } +} diff --git a/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsForm.form b/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsForm.form new file mode 100644 index 000000000000..108b9af1902f --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsForm.form @@ -0,0 +1,128 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsForm.java b/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsForm.java new file mode 100644 index 000000000000..0ee44d57b756 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsForm.java @@ -0,0 +1,352 @@ +package com.jetbrains.python; + +import com.intellij.execution.configuration.EnvironmentVariablesComponent; +import com.intellij.execution.util.PathMappingsComponent; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +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.SdkModel; +import com.intellij.openapi.projectRoots.impl.SdkListCellRenderer; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.CollectionComboBoxModel; +import com.intellij.ui.HideableDecorator; +import com.intellij.ui.RawCommandLineEditor; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.PathMappingSettings; +import com.jetbrains.python.configuration.PyConfigurableInterpreterList; +import com.jetbrains.python.configuration.PyConfigureInterpretersLinkPanel; +import com.jetbrains.python.run.AbstractPyCommonOptionsForm; +import com.intellij.application.options.ModuleListCellRenderer; +import com.jetbrains.python.run.PyCommonOptionsFormData; +import com.jetbrains.python.sdk.PySdkUtil; +import com.jetbrains.python.sdk.PythonSdkType; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author yole + */ +public class PyIdeCommonOptionsForm implements AbstractPyCommonOptionsForm { + private JPanel myMainPanel; + private EnvironmentVariablesComponent myEnvsComponent; + private RawCommandLineEditor myInterpreterOptionsTextField; + private TextFieldWithBrowseButton myWorkingDirectoryTextField; + private JComboBox myInterpreterComboBox; + private JBLabel myPythonInterpreterJBLabel; + private JLabel myProjectLabel; + private JComboBox myModuleCombo; + private JPanel myConfigureInterpretersPanel; + private String mySelectedSdkHome = null; + private PathMappingsComponent myPathMappingsComponent; + private JPanel myHideablePanel; + private JBCheckBox myAddContentRootsCheckbox; + private JBCheckBox myAddSourceRootsCheckbox; + + private JComponent labelAnchor; + private final Project myProject; + private List myPythonSdks; + private boolean myInterpreterRemote; + private final HideableDecorator myDecorator; + + public PyIdeCommonOptionsForm(PyCommonOptionsFormData data) { + myProject = data.getProject(); + myWorkingDirectoryTextField.addBrowseFolderListener("Select Working Directory", "", data.getProject(), + FileChooserDescriptorFactory.createSingleFolderDescriptor()); + + myPythonSdks = new ArrayList(PythonSdkType.getAllSdks()); + myPythonSdks.add(0, null); + + myInterpreterComboBox.setModel(new CollectionComboBoxModel(myPythonSdks, null)); + + final Module[] modules = ModuleManager.getInstance(data.getProject()).getModules(); + if (modules.length == 1) { + setModule(modules[0]); + myProjectLabel.setVisible(false); + myModuleCombo.setVisible(false); + } + else { + final List validModules = data.getValidModules(); + Module selection = validModules.size() > 0 ? validModules.get(0) : null; + myModuleCombo.setModel(new CollectionComboBoxModel(validModules, selection)); + myModuleCombo.setRenderer(new ModuleListCellRenderer()); + myModuleCombo.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateDefaultInterpreter((Module)myModuleCombo.getSelectedItem()); + } + }); + updateDefaultInterpreter((Module)myModuleCombo.getSelectedItem()); + } + + setAnchor(myEnvsComponent.getLabel()); + myPathMappingsComponent.setAnchor(myEnvsComponent.getLabel()); + + if (data.showConfigureInterpretersLink()) { + myConfigureInterpretersPanel.add(new PyConfigureInterpretersLinkPanel(myMainPanel)); + } + + addInterpreterComboBoxActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + updateRemoteInterpreterMode(); + } + } + ); + + updateRemoteInterpreterMode(); + + myDecorator = new HideableDecorator(myHideablePanel, "Environment", false) { + @Override + protected void on() { + super.on(); + storeState(); + } + + @Override + protected void off() { + super.off(); + storeState(); + } + private void storeState() { + PropertiesComponent.getInstance().setValue(EXPAND_PROPERTY_KEY, String.valueOf(isExpanded())); + } + }; + myDecorator.setOn(PropertiesComponent.getInstance().getBoolean(EXPAND_PROPERTY_KEY, true)); + myDecorator.setContentComponent(myMainPanel); + } + + @Override + public JComponent getMainPanel() { + return myHideablePanel; + } + + @Override + public void subscribe() { + PyConfigurableInterpreterList myInterpreterList = PyConfigurableInterpreterList.getInstance(myProject); + ProjectSdksModel myProjectSdksModel = myInterpreterList.getModel(); + myProjectSdksModel.addListener(new MyListener(this, myInterpreterList)); + updateSdkList(true, myInterpreterList); + } + + @Override + public JComponent getAnchor() { + return labelAnchor; + } + + @Override + public void setAnchor(JComponent anchor) { + labelAnchor = anchor; + myEnvsComponent.setAnchor(anchor); + myPythonInterpreterJBLabel.setAnchor(anchor); + } + + public String getInterpreterOptions() { + return myInterpreterOptionsTextField.getText().trim(); + } + + public void setInterpreterOptions(String interpreterOptions) { + myInterpreterOptionsTextField.setText(interpreterOptions); + } + + public String getWorkingDirectory() { + return FileUtil.toSystemIndependentName(myWorkingDirectoryTextField.getText().trim()); + } + + public void setWorkingDirectory(String workingDirectory) { + myWorkingDirectoryTextField.setText(workingDirectory == null ? "" : FileUtil.toSystemDependentName(workingDirectory)); + } + + @Override + public String getSdkHome() { + final Sdk selectedSdk = (Sdk)myInterpreterComboBox.getSelectedItem(); + return selectedSdk == null ? null : selectedSdk.getHomePath(); + } + + @Override + public void setSdkHome(String sdkHome) { + mySelectedSdkHome = sdkHome; + } + + @Nullable + @Override + public Module getModule() { + final Module selectedItem = (Module)myModuleCombo.getSelectedItem(); + if (selectedItem != null) { + return selectedItem; + } + final Module[] modules = ModuleManager.getInstance(myProject).getModules(); + if (modules.length == 1) { + return modules[0]; + } + return null; + } + + @Override + public void setModule(Module module) { + myModuleCombo.setSelectedItem(module); + updateDefaultInterpreter(module); + } + + private void updateDefaultInterpreter(Module module) { + final Sdk sdk = module == null ? null : ModuleRootManager.getInstance(module).getSdk(); + String projectSdkName = sdk == null ? "none" : sdk.getName(); + myInterpreterComboBox.setRenderer(new SdkListCellRenderer("Project Default (" + projectSdkName + ")")); + } + + public void updateSdkList(boolean preserveSelection, PyConfigurableInterpreterList myInterpreterList) { + myPythonSdks = myInterpreterList.getAllPythonSdks(); + Sdk selection = preserveSelection ? (Sdk)myInterpreterComboBox.getSelectedItem() : null; + if (!myPythonSdks.contains(selection)) { + selection = null; + } + myPythonSdks.add(0, null); + myInterpreterComboBox.setModel(new CollectionComboBoxModel(myPythonSdks, selection)); + } + + @Override + public boolean isUseModuleSdk() { + return myInterpreterComboBox.getSelectedItem() == null; + } + + @Override + public void setUseModuleSdk(boolean useModuleSdk) { + myInterpreterComboBox.setSelectedItem(useModuleSdk ? null : PythonSdkType.findSdkByPath(myPythonSdks, mySelectedSdkHome)); + } + + public boolean isPassParentEnvs() { + return myEnvsComponent.isPassParentEnvs(); + } + + public void setPassParentEnvs(boolean passParentEnvs) { + myEnvsComponent.setPassParentEnvs(passParentEnvs); + } + + public Map getEnvs() { + return myEnvsComponent.getEnvs(); + } + + public void setEnvs(Map envs) { + myEnvsComponent.setEnvs(envs); + } + + @Override + @Nullable + public PathMappingSettings getMappingSettings() { + if (myInterpreterRemote) { + return myPathMappingsComponent.getMappingSettings(); + } + else { + return null; + } + } + + @Override + public void setMappingSettings(@Nullable PathMappingSettings mappingSettings) { + myPathMappingsComponent.setMappingSettings(mappingSettings); + } + + @Override + public boolean addContentRoots() { + return myAddContentRootsCheckbox.isSelected(); + } + + @Override + public boolean addSourceRoots() { + return myAddSourceRootsCheckbox.isSelected(); + } + + @Override + public void addContentRoots(boolean add) { + myAddContentRootsCheckbox.setSelected(add); + } + + @Override + public void addSourceRoots(boolean add) { + myAddSourceRootsCheckbox.setSelected(add); + } + + private void createUIComponents() { + myInterpreterComboBox = new ComboBox(100); + } + + private void setRemoteInterpreterMode(boolean isInterpreterRemote) { + myInterpreterRemote = isInterpreterRemote; + myPathMappingsComponent.setVisible(isInterpreterRemote); + } + + private void updateRemoteInterpreterMode() { + setRemoteInterpreterMode(PySdkUtil.isRemote(getSdkSelected())); + } + + @Nullable + private Sdk getSdkSelected() { + String sdkHome = getSdkHome(); + if (StringUtil.isEmptyOrSpaces(sdkHome)) { + final Sdk projectJdk = PythonSdkType.findPythonSdk(getModule()); + if (projectJdk != null) { + sdkHome = projectJdk.getHomePath(); + } + } + + return PythonSdkType.findSdkByPath(sdkHome); + } + + @Override + public void addInterpreterComboBoxActionListener(ActionListener listener) { + myInterpreterComboBox.addActionListener(listener); + } + + @Override + public void removeInterpreterComboBoxActionListener(ActionListener listener) { + myInterpreterComboBox.removeActionListener(listener); + } + + private static class MyListener implements SdkModel.Listener { + private final PyIdeCommonOptionsForm myForm; + private PyConfigurableInterpreterList myInterpreterList; + + public MyListener(PyIdeCommonOptionsForm form, PyConfigurableInterpreterList interpreterList) { + myForm = form; + myInterpreterList = interpreterList; + } + + + private void update() { + myForm.updateSdkList(true, myInterpreterList); + } + + @Override + public void sdkAdded(Sdk sdk) { + update(); + } + + @Override + public void beforeSdkRemove(Sdk sdk) { + update(); + } + + @Override + public void sdkChanged(Sdk sdk, String previousName) { + update(); + } + + @Override + public void sdkHomeSelected(Sdk sdk, String newSdkHome) { + } + } +} diff --git a/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsFormFactory.java b/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsFormFactory.java new file mode 100644 index 000000000000..fee2954acc4b --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PyIdeCommonOptionsFormFactory.java @@ -0,0 +1,15 @@ +package com.jetbrains.python; + +import com.jetbrains.python.run.AbstractPyCommonOptionsForm; +import com.jetbrains.python.run.PyCommonOptionsFormData; +import com.jetbrains.python.run.PyCommonOptionsFormFactory; + +/** + * @author yole + */ +public class PyIdeCommonOptionsFormFactory extends PyCommonOptionsFormFactory { + @Override + public PyIdeCommonOptionsForm createForm(PyCommonOptionsFormData data) { + return new PyIdeCommonOptionsForm(data); + } +} diff --git a/python/ide/src/com/jetbrains/python/PyModuleListener.java b/python/ide/src/com/jetbrains/python/PyModuleListener.java new file mode 100644 index 000000000000..22edad8278e0 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PyModuleListener.java @@ -0,0 +1,36 @@ +package com.jetbrains.python; + +import com.intellij.ProjectTopics; +import com.intellij.execution.RunManagerEx; +import com.intellij.execution.RunnerAndConfigurationSettings; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.ModuleAdapter; +import com.intellij.openapi.project.Project; +import com.intellij.util.messages.MessageBus; +import com.jetbrains.python.run.AbstractPythonRunConfiguration; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * @author yole + */ +public class PyModuleListener { + public PyModuleListener(MessageBus messageBus) { + messageBus.connect().subscribe(ProjectTopics.MODULES, new ModuleAdapter() { + @Override + public void beforeModuleRemoved(Project project, Module module) { + final RunManagerEx runManager = RunManagerEx.getInstanceEx(project); + final Collection configurations = new ArrayList(runManager.getSortedConfigurations()); + for (RunnerAndConfigurationSettings configuration : configurations) { + if (configuration.getConfiguration() instanceof AbstractPythonRunConfiguration) { + final Module configModule = ((AbstractPythonRunConfiguration)configuration.getConfiguration()).getModule(); + if (configModule == module) { + runManager.removeConfiguration(configuration); + } + } + } + } + }); + } +} diff --git a/python/ide/src/com/jetbrains/python/PyOptionsApplicabilityFilter.java b/python/ide/src/com/jetbrains/python/PyOptionsApplicabilityFilter.java new file mode 100644 index 000000000000..7e27275e8e0e --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PyOptionsApplicabilityFilter.java @@ -0,0 +1,14 @@ +package com.jetbrains.python; + +import com.intellij.application.options.OptionId; +import com.intellij.application.options.OptionsApplicabilityFilter; + +/** + * @author yole + */ +public class PyOptionsApplicabilityFilter extends OptionsApplicabilityFilter { + @Override + public boolean isOptionApplicable(OptionId optionId) { + return optionId == OptionId.RENAME_IN_PLACE; + } +} diff --git a/python/ide/src/com/jetbrains/python/PythonFileTypeFactory.java b/python/ide/src/com/jetbrains/python/PythonFileTypeFactory.java new file mode 100644 index 000000000000..25a2ecc42ad4 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PythonFileTypeFactory.java @@ -0,0 +1,21 @@ +package com.jetbrains.python; + +import com.intellij.ide.highlighter.XmlFileType; +import com.intellij.openapi.fileTypes.FileTypeConsumer; +import com.intellij.openapi.fileTypes.FileTypeFactory; +import com.jetbrains.pyqt.QtUIFileType; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +/** + * IDE version of file type factory - does not use UltimateVerifier. + * + * @author yole + */ +public class PythonFileTypeFactory extends FileTypeFactory { + public void createFileTypes(@NonNls @NotNull final FileTypeConsumer consumer) { + consumer.consume(PythonFileType.INSTANCE, "py;pyw;"); + consumer.consume(QtUIFileType.INSTANCE, "ui"); + consumer.consume(XmlFileType.INSTANCE, "qrc"); + } +} \ No newline at end of file diff --git a/python/ide/src/com/jetbrains/python/PythonModuleTypeManager.java b/python/ide/src/com/jetbrains/python/PythonModuleTypeManager.java new file mode 100644 index 000000000000..89b682894aae --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PythonModuleTypeManager.java @@ -0,0 +1,14 @@ +package com.jetbrains.python; + +import com.intellij.openapi.module.ModuleType; +import com.intellij.openapi.module.impl.ModuleTypeManagerImpl; + +/** + * @author yole + */ +public class PythonModuleTypeManager extends ModuleTypeManagerImpl { + @Override + public ModuleType getDefaultModuleType() { + return new PlatformPythonModuleType(); + } +} diff --git a/python/ide/src/com/jetbrains/python/PythonSdkChooserCombo.java b/python/ide/src/com/jetbrains/python/PythonSdkChooserCombo.java new file mode 100644 index 000000000000..a295070cfb3d --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PythonSdkChooserCombo.java @@ -0,0 +1,74 @@ +package com.jetbrains.python; + +import com.intellij.openapi.options.ShowSettingsUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkType; +import com.intellij.openapi.projectRoots.impl.SdkListCellRenderer; +import com.intellij.openapi.util.Condition; +import com.intellij.ui.CollectionComboBoxModel; +import com.intellij.ui.ComboboxWithBrowseButton; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.python.configuration.PythonSdkConfigurable; +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; +import com.jetbrains.python.sdk.PythonSdkType; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +/** + * @author yole + */ +public class PythonSdkChooserCombo extends ComboboxWithBrowseButton { + private final List myChangedListeners = ContainerUtil.createLockFreeCopyOnWriteList(); + + //public PythonSdkChooserCombo(final Condition acceptableSdkCondition) { + // this(PythonSdkType.getAllSdks(), acceptableSdkCondition); + //} + + public PythonSdkChooserCombo(final Project project, List sdks, final Condition acceptableSdkCondition) { + Sdk initialSelection = null; + for (Sdk sdk : sdks) { + if (acceptableSdkCondition.value(sdk)) { + initialSelection = sdk; + break; + } + } + getComboBox().setModel(new CollectionComboBoxModel(sdks, initialSelection)); + getComboBox().setRenderer(new SdkListCellRenderer("") { + @Override + protected Icon getSdkIcon(Sdk sdk) { + final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdk); + return flavor != null ? flavor.getIcon() : ((SdkType)sdk.getSdkType()).getIcon(); + } + }); + addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + final PythonSdkConfigurable configurable = new PythonSdkConfigurable(project); + configurable.setNewProject(true); + ShowSettingsUtil.getInstance().editConfigurable(PythonSdkChooserCombo.this, configurable); + Sdk selection = configurable.getRealSelectedSdk(); + final List sdks = PythonSdkType.getAllSdks(); + getComboBox().setModel(new CollectionComboBoxModel(sdks, selection)); + notifyChanged(e); + } + }); + getComboBox().addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + notifyChanged(e); + } + }); + } + + private void notifyChanged(ActionEvent e) { + for (ActionListener changedListener : myChangedListeners) { + changedListener.actionPerformed(e); + } + } + + public void addChangedListener(ActionListener listener) { + myChangedListeners.add(listener); + } +} diff --git a/python/ide/src/com/jetbrains/python/PythonSdkConfigurator.java b/python/ide/src/com/jetbrains/python/PythonSdkConfigurator.java new file mode 100644 index 000000000000..8a5397dfb044 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PythonSdkConfigurator.java @@ -0,0 +1,79 @@ +package com.jetbrains.python; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkAdditionalData; +import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl; +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.DirectoryProjectConfigurator; +import com.intellij.util.SystemProperties; +import com.jetbrains.python.sdk.PreferredSdkComparator; +import com.jetbrains.python.sdk.PythonSdkAdditionalData; +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; +import com.jetbrains.python.sdk.PythonSdkType; +import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * @author dcheryasov + */ +public class PythonSdkConfigurator implements DirectoryProjectConfigurator { + + public void configureProject(final Project project, @NotNull final VirtualFile baseDir, Ref moduleRef) { + // it it a virtualenv? + final PythonSdkType py_sdk_type = PythonSdkType.getInstance(); + //find virtualEnv in project directory + final List candidates = new ArrayList(); + if (project != null) { + final VirtualFile rootDir = project.getBaseDir(); + if (rootDir != null) + candidates.addAll(VirtualEnvSdkFlavor.findInDirectory(rootDir)); + if (!candidates.isEmpty()) { + String filePath = candidates.get(0); + if (StringUtil.startsWithChar(filePath, '~')) { + final String home = SystemProperties.getUserHome(); + filePath = home + filePath.substring(1); + } + final Sdk virtualEnvSdk = SdkConfigurationUtil.createAndAddSDK(filePath, py_sdk_type); + if (virtualEnvSdk != null) { + SdkConfigurationUtil.setDirectoryProjectSdk(project, virtualEnvSdk); + SdkAdditionalData additionalData = virtualEnvSdk.getSdkAdditionalData(); + if (additionalData == null) { + additionalData = new PythonSdkAdditionalData(PythonSdkFlavor.getFlavor(virtualEnvSdk.getHomePath())); + ((ProjectJdkImpl)virtualEnvSdk).setSdkAdditionalData(additionalData); + } + ((PythonSdkAdditionalData) additionalData).associateWithProject(project); + return; + } + return; + } + } + + + final Sdk existing_sdk = ProjectRootManager.getInstance(project).getProjectSdk(); + if (existing_sdk != null && existing_sdk.getSdkType() == py_sdk_type) return; // SdkConfigurationUtil does the same + final File py_exe = PythonSdkType.findExecutableFile(new File(project.getBasePath(), "bin"), "python"); + if (py_exe != null) { + final File env_root = PythonSdkType.getVirtualEnvRoot(py_exe.getPath()); + if (env_root != null) { + // yes, an unknown virtualenv; set it up as SDK + final Sdk an_sdk = SdkConfigurationUtil.createAndAddSDK(py_exe.getPath(), py_sdk_type); + if (an_sdk != null) { + SdkConfigurationUtil.setDirectoryProjectSdk(project, an_sdk); + return; + } + } + } + // default + SdkConfigurationUtil.configureDirectoryProjectSdk(project, new PreferredSdkComparator(), py_sdk_type); + } +} diff --git a/python/ide/src/com/jetbrains/python/PythonSourceRootConfigurator.java b/python/ide/src/com/jetbrains/python/PythonSourceRootConfigurator.java new file mode 100644 index 000000000000..fd6d1615d12a --- /dev/null +++ b/python/ide/src/com/jetbrains/python/PythonSourceRootConfigurator.java @@ -0,0 +1,104 @@ +package com.jetbrains.python; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileEditor.impl.LoadTextUtil; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ContentEntry; +import com.intellij.openapi.roots.ModifiableRootModel; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.SourceFolder; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.DirectoryProjectConfigurator; +import com.intellij.psi.PsiFileFactory; +import com.jetbrains.python.psi.*; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class PythonSourceRootConfigurator implements DirectoryProjectConfigurator { + @NonNls private static final String SETUP_PY = "setup.py"; + + @Override + public void configureProject(Project project, @NotNull final VirtualFile baseDir, Ref moduleRef) { + VirtualFile setupPy = baseDir.findChild(SETUP_PY); + if (setupPy != null) { + final CharSequence content = LoadTextUtil.loadText(setupPy); + PyFile setupPyFile = (PyFile) PsiFileFactory.getInstance(project).createFileFromText(SETUP_PY, content.toString()); + final SetupCallVisitor visitor = new SetupCallVisitor(); + setupPyFile.accept(visitor); + String dir = visitor.getRootPackageDir(); + if (dir != null) { + final VirtualFile rootPackageVFile = baseDir.findFileByRelativePath(dir); + addSourceRoot(project, baseDir, rootPackageVFile, true); + } + } + + VirtualFile web2Py = baseDir.findChild("web2py"); + if (web2Py != null) { + addSourceRoot(project, baseDir, web2Py, false); + } + } + + private static void addSourceRoot(Project project, final VirtualFile baseDir, final VirtualFile root, final boolean unique) { + final Module[] modules = ModuleManager.getInstance(project).getModules(); + if (modules.length > 0 && root != null) { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + final ModifiableRootModel model = ModuleRootManager.getInstance(modules[0]).getModifiableModel(); + final ContentEntry[] contentEntries = model.getContentEntries(); + for (ContentEntry contentEntry : contentEntries) { + if (Comparing.equal(contentEntry.getFile(), baseDir)) { + final SourceFolder[] sourceFolders = contentEntry.getSourceFolders(); + if (!unique || sourceFolders.length == 0) { + contentEntry.addSourceFolder(root, false); + } + } + } + model.commit(); + } + }); + } + } + + private static class SetupCallVisitor extends PyRecursiveElementVisitor { + private String myRootPackageDir = null; + + @Override + public void visitPyCallExpression(PyCallExpression node) { + final PyArgumentList argList = node.getArgumentList(); + if (node.isCalleeText("setup") && argList != null) { + final PyKeywordArgument packageDirArg = argList.getKeywordArgument("package_dir"); + if (packageDirArg != null) { + final PyExpression valueExpression = packageDirArg.getValueExpression(); + if (valueExpression instanceof PyDictLiteralExpression) { + PyDictLiteralExpression packageDirDict = (PyDictLiteralExpression)valueExpression; + for (PyKeyValueExpression keyValue : packageDirDict.getElements()) { + final PyExpression keyExpr = keyValue.getKey(); + final PyExpression valueExpr = keyValue.getValue(); + if (keyExpr instanceof PyStringLiteralExpression && valueExpr instanceof PyStringLiteralExpression) { + String key = ((PyStringLiteralExpression) keyExpr).getStringValue(); + String value = ((PyStringLiteralExpression)valueExpr).getStringValue(); + if (key.length() == 0) { + myRootPackageDir = value; + } + } + } + } + } + } + else { + super.visitPyCallExpression(node); + } + } + + public String getRootPackageDir() { + return myRootPackageDir; + } + } +} diff --git a/python/ide/src/com/jetbrains/python/buildout/BuildoutConfigurable.form b/python/ide/src/com/jetbrains/python/buildout/BuildoutConfigurable.form new file mode 100644 index 000000000000..8da1065e40e0 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/buildout/BuildoutConfigurable.form @@ -0,0 +1,35 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ide/src/com/jetbrains/python/buildout/BuildoutConfigurable.java b/python/ide/src/com/jetbrains/python/buildout/BuildoutConfigurable.java new file mode 100644 index 000000000000..d8f48ee3dfff --- /dev/null +++ b/python/ide/src/com/jetbrains/python/buildout/BuildoutConfigurable.java @@ -0,0 +1,171 @@ +package com.jetbrains.python.buildout; + +import com.intellij.facet.FacetManager; +import com.intellij.facet.ModifiableFacetModel; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.NonDefaultProjectConfigurable; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.List; + +/** + * A silly configurable to add buildout facet configurator to PyCharm + * User: dcheryasov + * Date: Jul 28, 2010 4:34:58 PM + */ +public class BuildoutConfigurable implements Configurable, NonDefaultProjectConfigurable { + + private JCheckBox myEnabledCheckbox; + private JPanel myPlaceholder; + private JPanel myMainPanel; + private BuildoutConfigPanel mySettingsPanel; + private final Module myModule; + + public BuildoutConfigurable(@NotNull Module module) { + myModule = module; + BuildoutFacetConfiguration config = null; + BuildoutFacet facet = BuildoutFacet.getInstance(myModule); + if (facet != null) config = facet.getConfiguration(); + if (config == null) config = new BuildoutFacetConfiguration(null); + mySettingsPanel = new BuildoutConfigPanel(myModule, config); + myPlaceholder.add(mySettingsPanel, BorderLayout.CENTER); + final boolean isEnabled = !StringUtil.isEmptyOrSpaces(config.getScriptName()); + myEnabledCheckbox.setSelected(isEnabled); + updateFacetEnabled(isEnabled); + myEnabledCheckbox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + final boolean enabled = myEnabledCheckbox.isSelected(); + updateFacetEnabled(enabled); + } + }); + mySettingsPanel.addFocusListener( + new FocusListener() { + @Override + public void focusGained(FocusEvent focusEvent) { + switchNoticeText(); + } + + @Override + public void focusLost(FocusEvent focusEvent) { } + } + ); + } + + private void updateFacetEnabled(boolean enabled) { + mySettingsPanel.setFacetEnabled(enabled); + UIUtil.setEnabled(mySettingsPanel, enabled, true); + } + + @Nls + @Override + public String getDisplayName() { + return "Buildout Support"; + } + + @Override + public String getHelpTopic() { + return "reference-python-buildout"; + } + + @Override + public JComponent createComponent() { + switchNoticeText(); + return myMainPanel; + } + + @Override + public boolean isModified() { + if (! myEnabledCheckbox.isEnabled()) return false; + final BuildoutFacet facet = myModule != null? BuildoutFacet.getInstance(myModule) : null; + final boolean got_facet = facet != null; + if (myEnabledCheckbox.isSelected() != got_facet) return true; + if (got_facet && myEnabledCheckbox.isSelected()) { + return mySettingsPanel.isModified(facet.getConfiguration()); + } + return false; + } + + @Override + public void apply() throws ConfigurationException { + final BuildoutFacet facet = myModule != null? BuildoutFacet.getInstance(myModule) : null; + final boolean got_facet = facet != null; + boolean facet_is_desired = myEnabledCheckbox.isSelected(); + + mySettingsPanel.apply(); + List paths_from_script = null; + if (facet_is_desired) { + String script_name = mySettingsPanel.getScriptName(); + VirtualFile script_file = BuildoutConfigPanel.getScriptFile(script_name); + paths_from_script = BuildoutFacet.extractBuildoutPaths(script_file); + if (paths_from_script == null) { + throw new ConfigurationException("Failed to extract paths from '" + script_file.getPresentableName() + "'"); + } + mySettingsPanel.getConfiguration().setPaths(paths_from_script); + + } + if (facet_is_desired && ! got_facet) addFacet(mySettingsPanel.getConfiguration()); + if (! facet_is_desired && got_facet) removeFacet(facet); + if (facet_is_desired) BuildoutFacet.attachLibrary(myModule); + else BuildoutFacet.detachLibrary(myModule); + } + + private void addFacet(BuildoutFacetConfiguration config) { + BuildoutFacetConfigurator.setupFacet(myModule, config); + } + + private void removeFacet(BuildoutFacet facet) { + final ModifiableFacetModel model = FacetManager.getInstance(myModule).createModifiableModel(); + model.removeFacet(facet); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + model.commit(); + } + }); + } + + @Override + public void reset() { + // TODO: refactor, see AppEngineConfigurable + if (myModule == null) { + myEnabledCheckbox.setSelected(false); + myEnabledCheckbox.setEnabled(false); + } + else { + myEnabledCheckbox.setEnabled(true); + switchNoticeText(); + final BuildoutFacet instance = BuildoutFacet.getInstance(myModule); + if (instance != null) { + boolean selected = ! StringUtil.isEmptyOrSpaces(instance.getConfiguration().getScriptName()); + myEnabledCheckbox.setSelected(selected); + mySettingsPanel.setEnabled(selected); + mySettingsPanel.reset(); + } + else { + myEnabledCheckbox.setSelected(false); + mySettingsPanel.setEnabled(false); + } + } + } + + private void switchNoticeText() { + mySettingsPanel.showNoticeText(/*DjangoFacet.getInstance(myModule) != null*/ false); + } + + @Override + public void disposeUIResources() { + } + +} diff --git a/python/ide/src/com/jetbrains/python/buildout/BuildoutFacetConfigurator.java b/python/ide/src/com/jetbrains/python/buildout/BuildoutFacetConfigurator.java new file mode 100644 index 000000000000..4eca76e939b0 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/buildout/BuildoutFacetConfigurator.java @@ -0,0 +1,63 @@ +package com.jetbrains.python.buildout; + +import com.intellij.facet.FacetManager; +import com.intellij.facet.ModifiableFacetModel; +import com.intellij.facet.ProjectFacetManager; +import com.intellij.openapi.application.Result; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.DirectoryProjectConfigurator; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +/** + * Detects and configures a buildout facet. + * User: dcheryasov + * Date: Jul 26, 2010 6:10:39 PM + */ +public class BuildoutFacetConfigurator implements DirectoryProjectConfigurator { + @Override + public void configureProject(Project project, @NotNull VirtualFile baseDir, Ref moduleRef) { + final Module[] modules = ModuleManager.getInstance(project).getModules(); + if (modules.length > 0) { + final Module module = modules[0]; + if (BuildoutFacet.getInstance(module) == null) { + baseDir.refresh(false, false); + final VirtualFile runner = BuildoutFacet.getRunner(baseDir); + if (runner != null) { + // TODO parse buildout.cfg and find out the part to use for the default script + final File script = BuildoutFacet.findScript(null, "buildout", project.getBaseDir()); + if (script != null) { + final ProjectFacetManager facetManager = ProjectFacetManager.getInstance(module.getProject()); + final BuildoutFacetConfiguration config = facetManager.createDefaultConfiguration(BuildoutFacetType.getInstance()); + config.setScriptName(script.getPath()); + setupFacet(module, config); + } + } + } + } + } + + + static void setupFacet(Module module, @NotNull BuildoutFacetConfiguration config) { + //TODO: refactor, see other python facets + FacetManager facetManager = FacetManager.getInstance(module); + final ModifiableFacetModel model = facetManager.createModifiableModel(); + BuildoutFacetType facetType = BuildoutFacetType.getInstance(); + BuildoutFacet facet = facetManager.createFacet(facetType, facetType.getDefaultFacetName(), config, null); + model.addFacet(facet); + + new WriteAction() { + protected void run(final Result result) throws Throwable { + model.commit(); + } + }.execute(); + facet.updatePaths(); + BuildoutFacet.attachLibrary(module); + } +} diff --git a/python/ide/src/com/jetbrains/python/buildout/BuildoutModulesConfigurable.java b/python/ide/src/com/jetbrains/python/buildout/BuildoutModulesConfigurable.java new file mode 100644 index 000000000000..2e2805eb9745 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/buildout/BuildoutModulesConfigurable.java @@ -0,0 +1,23 @@ +package com.jetbrains.python.buildout; + +import com.intellij.application.options.ModuleAwareProjectConfigurable; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.NonDefaultProjectConfigurable; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class BuildoutModulesConfigurable extends ModuleAwareProjectConfigurable implements NonDefaultProjectConfigurable { + public BuildoutModulesConfigurable(Project project) { + super(project, "Buildout Support", "reference-python-buildout"); + } + + @NotNull + @Override + protected Configurable createModuleConfigurable(Module module) { + return new BuildoutConfigurable(module); + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/AddVEnvOptionsDialog.form b/python/ide/src/com/jetbrains/python/configuration/AddVEnvOptionsDialog.form new file mode 100644 index 000000000000..78c5657ecc11 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/AddVEnvOptionsDialog.form @@ -0,0 +1,35 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ide/src/com/jetbrains/python/configuration/AddVEnvOptionsDialog.java b/python/ide/src/com/jetbrains/python/configuration/AddVEnvOptionsDialog.java new file mode 100644 index 000000000000..43bc66f58513 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/AddVEnvOptionsDialog.java @@ -0,0 +1,35 @@ +package com.jetbrains.python.configuration; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.ui.components.JBCheckBox; + +import javax.swing.*; +import java.awt.*; + +/** + * @author yole + */ +public class AddVEnvOptionsDialog extends DialogWrapper { + private JBCheckBox myUseForThisProjectJBCheckBox; + private JBCheckBox myMakeAvailableToAllJBCheckBox; + private JPanel myMainPanel; + + public AddVEnvOptionsDialog(Component parent) { + super(parent, false); + init(); + setTitle("Add Virtualenv"); + } + + @Override + protected JComponent createCenterPanel() { + return myMainPanel; + } + + public boolean useForThisProject() { + return myUseForThisProjectJBCheckBox.isSelected(); + } + + public boolean makeAvailableToAll() { + return myMakeAvailableToAllJBCheckBox.isSelected(); + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/EditSdkDialog.form b/python/ide/src/com/jetbrains/python/configuration/EditSdkDialog.form new file mode 100644 index 000000000000..a5f3036f09dc --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/EditSdkDialog.form @@ -0,0 +1,69 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ide/src/com/jetbrains/python/configuration/EditSdkDialog.java b/python/ide/src/com/jetbrains/python/configuration/EditSdkDialog.java new file mode 100644 index 000000000000..8843c2e6635a --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/EditSdkDialog.java @@ -0,0 +1,106 @@ +package com.jetbrains.python.configuration; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.SdkModificator; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.ui.ClickListener; +import com.intellij.ui.DocumentAdapter; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.NullableFunction; +import com.jetbrains.python.sdk.PythonSdkAdditionalData; +import com.jetbrains.python.sdk.PythonSdkType; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import java.awt.event.MouseEvent; + +/** + * @author yole + */ +public class EditSdkDialog extends DialogWrapper { + private JPanel myMainPanel; + private JTextField myNameTextField; + private TextFieldWithBrowseButton myInterpreterPathTextField; + private JBCheckBox myAssociateCheckbox; + private JBLabel myRemoveAssociationLabel; + private final boolean myWasAssociated; + private boolean myAssociationRemoved = false; + + protected EditSdkDialog(Project project, SdkModificator sdk, final NullableFunction nameValidator) { + super(project, true); + setTitle("Edit Python Interpreter"); + myNameTextField.setText(sdk.getName()); + myNameTextField.getDocument().addDocumentListener(new DocumentAdapter() { + @Override + protected void textChanged(DocumentEvent e) { + String nameError = nameValidator.fun(getName()); + setErrorText(nameError); + setOKActionEnabled(nameError == null); + } + }); + myInterpreterPathTextField.setText(sdk.getHomePath()); + myInterpreterPathTextField.addBrowseFolderListener("Specify Interpreter Path", null, project, + PythonSdkType.getInstance().getHomeChooserDescriptor()); + myRemoveAssociationLabel.setVisible(false); + if (PythonSdkType.getVirtualEnvRoot(sdk.getHomePath()) == null) { + myAssociateCheckbox.setVisible(false); + } + else { + PythonSdkAdditionalData data = (PythonSdkAdditionalData) sdk.getSdkAdditionalData(); + if (data != null) { + final String path = data.getAssociatedProjectPath(); + if (path != null) { + myAssociateCheckbox.setSelected(true); + final String basePath = project.getBasePath(); + if (basePath != null && !path.equals(FileUtil.toSystemIndependentName(basePath))) { + myAssociateCheckbox.setEnabled(false); + myAssociateCheckbox.setText("Associate this virtual environment with " + FileUtil.toSystemDependentName(path)); + myRemoveAssociationLabel.setVisible(true); + } + } + } + } + myWasAssociated = myAssociateCheckbox.isSelected(); + init(); + new ClickListener() { + @Override + public boolean onClick(MouseEvent e, int clickCount) { + myAssociateCheckbox.setSelected(false); + myAssociateCheckbox.setEnabled(true); + myAssociateCheckbox.setText("Associate this environment with current project"); + myRemoveAssociationLabel.setVisible(false); + myAssociationRemoved = true; + return true; + } + }.installOn(myRemoveAssociationLabel); + } + + @Override + protected JComponent createCenterPanel() { + return myMainPanel; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myNameTextField; + } + + public String getName() { + return myNameTextField.getText(); + } + + public String getHomePath() { + return myInterpreterPathTextField.getText(); + } + + public boolean associateWithProject() { + return myAssociateCheckbox.isSelected(); + } + + public boolean isAssociateChanged() { + return myWasAssociated != associateWithProject() || myAssociationRemoved; + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.form b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.form new file mode 100644 index 000000000000..79a648b213a2 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.form @@ -0,0 +1,40 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java new file mode 100644 index 000000000000..98364d29bb28 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java @@ -0,0 +1,203 @@ +package com.jetbrains.python.configuration; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.highlighter.EditorHighlighter; +import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.UnnamedConfigurable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkModel; +import com.intellij.openapi.projectRoots.impl.SdkListCellRenderer; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.CollectionComboBoxModel; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.sdk.PySdkListCellRenderer; +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +/** + * @author yole + */ +public class PyActiveSdkConfigurable implements UnnamedConfigurable { + private JPanel myPanel; + private final Project myProject; + @Nullable private final Module myModule; + private JComboBox mySdkCombo; + private JPanel myConfigureInterpretersPanel; + private PyConfigurableInterpreterList myInterpreterList; + private ProjectSdksModel myProjectSdksModel; + private MyListener myListener; + + public PyActiveSdkConfigurable(Project project) { + myModule = null; + myProject = project; + init(); + } + + public PyActiveSdkConfigurable(@NotNull Module module) { + myModule = module; + myProject = module.getProject(); + init(); + } + + private void init() { + mySdkCombo.setRenderer(new SdkListCellRenderer("")); + + myInterpreterList = PyConfigurableInterpreterList.getInstance(myProject); + myProjectSdksModel = myInterpreterList.getModel(); + myListener = new MyListener(this); + myProjectSdksModel.addListener(myListener); + myConfigureInterpretersPanel.add(new PyConfigureInterpretersLinkPanel(myPanel), BorderLayout.CENTER); + myInterpreterList.setSdkCombo(mySdkCombo); + } + + @Override + public JComponent createComponent() { + return myPanel; + } + + @Override + public boolean isModified() { + final Sdk sdk = getSdk(); + return sdk != myProjectSdksModel.findSdk((Sdk)mySdkCombo.getSelectedItem()); + } + + @Nullable + private Sdk getSdk() { + if (myModule == null) { + return ProjectRootManager.getInstance(myProject).getProjectSdk(); + } + final ModuleRootManager rootManager = ModuleRootManager.getInstance(myModule); + return rootManager.getSdk(); + } + + @Override + public void apply() throws ConfigurationException { + final Sdk selectedSdk = myProjectSdksModel.findSdk((Sdk)mySdkCombo.getSelectedItem()); + if (myModule == null) { + final ProjectRootManager rootManager = ProjectRootManager.getInstance(myProject); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + rootManager.setProjectSdk(selectedSdk); + } + }); + } + else { + ModuleRootModificationUtil.setModuleSdk(myModule, selectedSdk); + } + final Sdk prevSdk = ProjectRootManager.getInstance(myProject).getProjectSdk(); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + ProjectRootManager.getInstance(myProject).setProjectSdk(selectedSdk); + } + }); + + myProjectSdksModel.setProjectSdk(selectedSdk); + + // update string literals if different LanguageLevel was selected + if (prevSdk != null && selectedSdk != null) { + final PythonSdkFlavor flavor1 = PythonSdkFlavor.getFlavor(selectedSdk); + final PythonSdkFlavor flavor2 = PythonSdkFlavor.getFlavor(prevSdk); + if (flavor1 != null && flavor2 != null) { + final LanguageLevel languageLevel1 = flavor1.getLanguageLevel(selectedSdk); + final LanguageLevel languageLevel2 = flavor2.getLanguageLevel(prevSdk); + if ((languageLevel1.isPy3K() && languageLevel2.isPy3K()) || + (!languageLevel1.isPy3K()) && !languageLevel2.isPy3K()) { + return; + } + } + } + rehighlightStrings(myProject); + } + + public static void rehighlightStrings(final @NotNull Project project) { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + + for (Editor editor : EditorFactory.getInstance().getAllEditors()) { + if (editor instanceof EditorEx && editor.getProject() == project) { + final VirtualFile vFile = ((EditorEx)editor).getVirtualFile(); + if (vFile != null) { + final EditorHighlighter highlighter = EditorHighlighterFactory.getInstance().createEditorHighlighter(project, vFile); + ((EditorEx)editor).setHighlighter(highlighter); + } + } + } + } + }); + } + + @Override + public void reset() { + updateSdkList(false); + + final Sdk sdk = getSdk(); + mySdkCombo.setSelectedItem(myProjectSdksModel.getProjectSdks().get(sdk)); + } + + private void updateSdkList(boolean preserveSelection) { + final List sdkList = myInterpreterList.getAllPythonSdks(); + Sdk selection = preserveSelection ? (Sdk)mySdkCombo.getSelectedItem() : null; + if (!sdkList.contains(selection)) { + selection = null; + } + VirtualEnvProjectFilter.removeNotMatching(myProject, sdkList); + // if the selection is a non-matching virtualenv, show it anyway + if (selection != null && !sdkList.contains(selection)) { + sdkList.add(0, selection); + } + sdkList.add(0, null); + mySdkCombo.setRenderer(new PySdkListCellRenderer()); + mySdkCombo.setModel(new CollectionComboBoxModel(sdkList, selection)); + } + + @Override + public void disposeUIResources() { + myProjectSdksModel.removeListener(myListener); + myInterpreterList.disposeModel(); + } + + private static class MyListener implements SdkModel.Listener { + private final PyActiveSdkConfigurable myConfigurable; + + public MyListener(PyActiveSdkConfigurable configurable) { + myConfigurable = configurable; + } + + @Override + public void sdkAdded(Sdk sdk) { + myConfigurable.reset(); + } + + @Override + public void beforeSdkRemove(Sdk sdk) { + myConfigurable.updateSdkList(true); + } + + @Override + public void sdkChanged(Sdk sdk, String previousName) { + myConfigurable.updateSdkList(true); + } + + @Override + public void sdkHomeSelected(Sdk sdk, String newSdkHome) { + } + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkModuleConfigurable.java b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkModuleConfigurable.java new file mode 100644 index 000000000000..2fd855c40551 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyActiveSdkModuleConfigurable.java @@ -0,0 +1,60 @@ +package com.jetbrains.python.configuration; + +import com.intellij.application.options.ModuleAwareProjectConfigurable; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.UnnamedConfigurable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ModuleRootManager; +import com.jetbrains.python.sdk.PythonSdkUpdater; +import com.jetbrains.python.testing.VFSTestFrameworkListener; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class PyActiveSdkModuleConfigurable extends ModuleAwareProjectConfigurable implements Configurable.Composite { + private final Project myProject; + private final Configurable mySdkConfigurable; + + public PyActiveSdkModuleConfigurable(Project project) { + super(project, "Project Interpreter", "reference.settings.project.interpreter"); + myProject = project; + mySdkConfigurable = new PythonSdkConfigurable(project); + } + + @NotNull + @Override + protected UnnamedConfigurable createModuleConfigurable(Module module) { + return new PyActiveSdkConfigurable(module); + } + + @Override + protected UnnamedConfigurable createDefaultProjectConfigurable() { + return new PyActiveSdkConfigurable(myProject); + } + + @Override + public void apply() throws ConfigurationException { + super.apply(); + + // TODO[catherine] proper per-module caching of framework installed state + for (Module module : ModuleManager.getInstance(myProject).getModules()) { + final Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); + if (sdk != null) { + VFSTestFrameworkListener.getInstance().updateAllTestFrameworks(sdk); + break; + } + + } + PythonSdkUpdater.getInstance().updateActiveSdks(myProject, 0); + } + + @Override + public Configurable[] getConfigurables() { + return new Configurable[] { mySdkConfigurable }; + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java b/python/ide/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java new file mode 100644 index 000000000000..b6a080e29072 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java @@ -0,0 +1,68 @@ +package com.jetbrains.python.configuration; + +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; +import com.intellij.openapi.util.Comparing; +import com.jetbrains.python.sdk.PythonSdkType; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Manages the SDK model shared between PythonSdkConfigurable and PyActiveSdkConfigurable. + * + * @author yole + */ +public class PyConfigurableInterpreterList { + private ProjectSdksModel myModel; + private JComboBox mySdkCombo; + + public void setSdkCombo(final JComboBox sdkCombo) { + mySdkCombo = sdkCombo; + } + + public void setSelectedSdk(final Sdk selectedSdk) { + if (mySdkCombo != null) + mySdkCombo.setSelectedItem(selectedSdk); + } + + public static PyConfigurableInterpreterList getInstance(Project project) { + return ServiceManager.getService(project, PyConfigurableInterpreterList.class); + } + + public ProjectSdksModel getModel() { + if (myModel == null) { + myModel = new ProjectSdksModel(); + myModel.reset(null); + } + return myModel; + } + + public void disposeModel() { + if (myModel != null) { + myModel.disposeUIResources(); + myModel = null; + } + } + + public List getAllPythonSdks() { + List result = new ArrayList(); + for (Sdk sdk : getModel().getSdks()) { + if (sdk.getSdkType() instanceof PythonSdkType) { + result.add(sdk); + } + } + Collections.sort(result, new Comparator() { + @Override + public int compare(Sdk o1, Sdk o2) { + return Comparing.compare(o1.getName(), o2.getName()); + } + }); + return result; + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PyConfigureInterpretersLinkPanel.java b/python/ide/src/com/jetbrains/python/configuration/PyConfigureInterpretersLinkPanel.java new file mode 100644 index 000000000000..0db63b1619be --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyConfigureInterpretersLinkPanel.java @@ -0,0 +1,41 @@ +package com.jetbrains.python.configuration; + +import com.intellij.ide.DataManager; +import com.intellij.openapi.options.newEditor.OptionsEditor; +import com.intellij.ui.ClickListener; +import com.intellij.ui.components.JBLabel; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; + +/** + * @author traff + */ +public class PyConfigureInterpretersLinkPanel extends JPanel { + private final JBLabel myConfigureLabel; + + public PyConfigureInterpretersLinkPanel(final JPanel parentPanel) { + super(new BorderLayout()); + myConfigureLabel = new JBLabel("Configure Interpreters"); + myConfigureLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + new ClickListener() { + @Override + public boolean onClick(MouseEvent e, int clickCount) { + if (clickCount == 1) { + final OptionsEditor optionsEditor = OptionsEditor.KEY.getData(DataManager.getInstance().getDataContext(parentPanel)); + if (optionsEditor != null) { + PythonSdkConfigurable configurable = optionsEditor.findConfigurable(PythonSdkConfigurable.class); + if (configurable != null) { + optionsEditor.clearSearchAndSelect(configurable); + } + return true; + } + } + return false; + } + }.installOn(myConfigureLabel); + + add(myConfigureLabel, BorderLayout.CENTER); + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PyDependenciesConfigurable.java b/python/ide/src/com/jetbrains/python/configuration/PyDependenciesConfigurable.java new file mode 100644 index 000000000000..b1269b9f1c5f --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyDependenciesConfigurable.java @@ -0,0 +1,32 @@ +package com.jetbrains.python.configuration; + +import com.intellij.application.options.ModuleAwareProjectConfigurable; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.options.OptionalConfigurable; +import com.intellij.openapi.options.UnnamedConfigurable; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class PyDependenciesConfigurable extends ModuleAwareProjectConfigurable implements OptionalConfigurable { + private final Project myProject; + + public PyDependenciesConfigurable(Project project) { + super(project, "Project Dependencies", "reference.settingsdialog.project.dependencies"); + myProject = project; + } + + @NotNull + @Override + protected UnnamedConfigurable createModuleConfigurable(Module module) { + return new PyModuleDependenciesConfigurable(module); + } + + @Override + public boolean needDisplay() { + return ModuleManager.getInstance(myProject).getModules().length > 1; + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PyDiffPreviewProvider.java b/python/ide/src/com/jetbrains/python/configuration/PyDiffPreviewProvider.java new file mode 100644 index 000000000000..63afbd83e537 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyDiffPreviewProvider.java @@ -0,0 +1,59 @@ +package com.jetbrains.python.configuration; + +import com.intellij.openapi.diff.DiffContent; +import com.intellij.openapi.diff.SimpleContent; +import com.intellij.openapi.diff.impl.settings.DiffPreviewProvider; +import com.jetbrains.python.PythonFileType; +import org.jetbrains.annotations.NonNls; + +/** + * @author oleg + */ +public class PyDiffPreviewProvider extends DiffPreviewProvider { + @Override + public DiffContent[] createContents() { + return new DiffContent[]{createContent(LEFT_TEXT), createContent(CENTER_TEXT), createContent(RIGHT_TEXT)}; + } + + private static SimpleContent createContent(final String text) { + return new SimpleContent(text, PythonFileType.INSTANCE); + } + + @NonNls private static final String LEFT_TEXT = "class MyClass\n" + + " value = 123\n" + + "\n" + + " def left_only(self):\n" + + " bar(123)\n" + + "\n" + + " def bar(self, a)\n" + + " print a\n"+ + "\n"+ + " def foo(self):\n" + + " pass # Left changes\n"+ + "\n"+ + "\n"; + @NonNls private static final String CENTER_TEXT = "class MyClass\n" + + " value = 123\n" + + "\n" + + " def bar(self, a)\n" + + " print a\n"+ + "\n"+ + " def foo(self):\n" + + " pass\n" + + "\n" + + "\n" + + " def removed_from_left(self):\n" + + " bar('PyCharmRulezzz')"; + @NonNls private static final String RIGHT_TEXT = "class MyClass\n" + + " value = -123\n" + + "\n" + + " def bar(self, a)\n" + + " print a\n"+ + "\n"+ + " def foo(self):\n" + + " pass # Right changes\n" + + "\n" + + "\n" + + " def removed_from_left(self):\n" + + " bar('PyCharmRulezzz')"; +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PyModuleDependenciesConfigurable.form b/python/ide/src/com/jetbrains/python/configuration/PyModuleDependenciesConfigurable.form new file mode 100644 index 000000000000..d5b0c6bb33c2 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyModuleDependenciesConfigurable.form @@ -0,0 +1,29 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ide/src/com/jetbrains/python/configuration/PyModuleDependenciesConfigurable.java b/python/ide/src/com/jetbrains/python/configuration/PyModuleDependenciesConfigurable.java new file mode 100644 index 000000000000..56045c23266b --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PyModuleDependenciesConfigurable.java @@ -0,0 +1,118 @@ +package com.jetbrains.python.configuration; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.UnnamedConfigurable; +import com.intellij.openapi.roots.ModifiableRootModel; +import com.intellij.openapi.roots.ModuleOrderEntry; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.OrderEntry; +import com.intellij.ui.CheckBoxList; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.util.Function; +import com.intellij.util.ui.EditableListModelDecorator; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author yole + */ +public class PyModuleDependenciesConfigurable implements UnnamedConfigurable { + private final Module myModule; + private List myInitialDependencies; + private JPanel myMainPanel; + private JPanel myListHolderPanel; + private CheckBoxList myDependenciesList; + + public PyModuleDependenciesConfigurable(Module module) { + myModule = module; + myDependenciesList = new CheckBoxList(); + resetModel(); + ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myDependenciesList, + new EditableListModelDecorator((DefaultListModel) myDependenciesList.getModel())); + decorator.disableRemoveAction(); + myListHolderPanel.add(decorator.createPanel(), BorderLayout.CENTER); + } + + private void resetModel() { + List possibleDependencies = new ArrayList(); + myInitialDependencies = Arrays.asList(ModuleRootManager.getInstance(myModule).getDependencies()); + possibleDependencies.addAll(myInitialDependencies); + for (Module otherModule : ModuleManager.getInstance(myModule.getProject()).getModules()) { + if (!possibleDependencies.contains(otherModule) && otherModule != myModule) { + possibleDependencies.add(otherModule); + } + } + myDependenciesList.setItems(possibleDependencies, new Function() { + @Override + public String fun(Module module) { + return module.getName(); + } + }); + myDependenciesList.setBorder(null); + for (Module dependency : myInitialDependencies) { + myDependenciesList.setItemSelected(dependency, true); + } + } + + @Nullable + @Override + public JComponent createComponent() { + return myMainPanel; + } + + @Override + public boolean isModified() { + return collectDependencies().equals(myInitialDependencies); + } + + private List collectDependencies() { + List result = new ArrayList(); + for (int i = 0; i < myDependenciesList.getItemsCount(); i++) { + Module module = (Module)myDependenciesList.getItemAt(i); + if (myDependenciesList.isItemSelected(module)) { + result.add(module); + } + } + return result; + } + + @Override + public void apply() throws ConfigurationException { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + ModifiableRootModel model = ModuleRootManager.getInstance(myModule).getModifiableModel(); + List entries = new ArrayList(); + for (OrderEntry entry : model.getOrderEntries()) { + if (entry instanceof ModuleOrderEntry) { + entries.add((ModuleOrderEntry) entry); + } + } + for (ModuleOrderEntry entry : entries) { + model.removeOrderEntry(entry); + } + for (Module module : collectDependencies()) { + model.addModuleOrderEntry(module); + } + model.commit(); + } + }); + } + + @Override + public void reset() { + resetModel(); + } + + @Override + public void disposeUIResources() { + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PythonContentEntriesConfigurable.java b/python/ide/src/com/jetbrains/python/configuration/PythonContentEntriesConfigurable.java new file mode 100644 index 000000000000..f7a3e2f03a8a --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PythonContentEntriesConfigurable.java @@ -0,0 +1,42 @@ +package com.jetbrains.python.configuration; + +import com.intellij.application.options.ModuleAwareProjectConfigurable; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.NonDefaultProjectConfigurable; +import com.intellij.openapi.options.OptionalConfigurable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ui.configuration.PlatformContentEntriesConfigurable; +import com.intellij.util.ArrayUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.jps.model.java.JavaSourceRootType; + +/** + * @author yole + */ +public class PythonContentEntriesConfigurable extends ModuleAwareProjectConfigurable implements NonDefaultProjectConfigurable, OptionalConfigurable { + private final Project myProject; + + public PythonContentEntriesConfigurable(final Project project) { + super(project, "Project Structure", "reference.settingsdialog.project.structure"); + myProject = project; + } + + @Nullable + private static Module getSingleModule(Project project) { + return ArrayUtil.getFirstElement(ModuleManager.getInstance(project).getModules()); + } + + @Override + public boolean needDisplay() { + return getSingleModule(myProject) != null; + } + + @NotNull + @Override + protected Configurable createModuleConfigurable(Module module) { + return new PlatformContentEntriesConfigurable(module, JavaSourceRootType.SOURCE); + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PythonPathEditor.java b/python/ide/src/com/jetbrains/python/configuration/PythonPathEditor.java new file mode 100644 index 000000000000..97d1c9703138 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PythonPathEditor.java @@ -0,0 +1,316 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.configuration; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.projectRoots.SdkAdditionalData; +import com.intellij.openapi.projectRoots.SdkModificator; +import com.intellij.openapi.projectRoots.ui.SdkPathEditor; +import com.intellij.openapi.roots.OrderRootType; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.JarFileSystem; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.AnActionButton; +import com.intellij.ui.ListCellRendererWrapper; +import com.intellij.ui.ListUtil; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.components.JBList; +import com.intellij.util.ArrayUtil; +import com.jetbrains.python.sdk.PythonSdkAdditionalData; +import com.jetbrains.python.sdk.PythonSdkType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; +import java.util.Set; + +/** + * @author traff + */ +public class PythonPathEditor extends SdkPathEditor { + private final PathListModel myPathListModel; + + public PythonPathEditor(final String displayName, + final OrderRootType orderRootType, + final FileChooserDescriptor descriptor) { + super(displayName, orderRootType, descriptor); + myPathListModel = new PathListModel(orderRootType, getListModel()); + } + + public void reset(@Nullable SdkModificator modificator) { + if (modificator != null) { + List list = Lists.newArrayList(modificator.getRoots(getOrderRootType())); + resetPath(myPathListModel.reset(list, modificator)); + } + else { + setEnabled(false); + } + } + + public void reload(@Nullable SdkModificator sdkModificator) { + if (sdkModificator != null) { + List list = Lists.newArrayList(sdkModificator.getRoots(getOrderRootType())); + resetPath(myPathListModel.reload(list)); + setModified(true); + } + else { + setEnabled(false); + } + } + + @Override + public void apply(SdkModificator sdkModificator) { + sdkModificator.removeRoots(getOrderRootType()); + // add all items + for (int i = 0; i < myPathListModel.getRowCount(); i++) { + VirtualFile path = myPathListModel.getValueAt(i); + if (!myPathListModel.isExcluded(path)) { + sdkModificator.addRoot(path, getOrderRootType()); + } + } + setModified(false); + myPathListModel.apply(sdkModificator); + } + + @Override + protected VirtualFile[] adjustAddedFileSet(Component component, VirtualFile[] files) { + for (int i = 0, filesLength = files.length; i < filesLength; i++) { + if (files[i].getFileType() == FileTypes.ARCHIVE) { + files[i] = JarFileSystem.getInstance().getJarRootForLocalFile(files[i]); + } + } + if (myPathListModel.add(Lists.newArrayList(files))) { + setModified(true); + } + return files; + } + + @Override + protected void doRemoveItems(int[] idxs, JList list) { + List> removed = Lists.newArrayList(); + for (int i : idxs) { + removed.add(Pair.create((VirtualFile)getListModel().get(i), i)); + } + ListUtil.removeIndices(list, myPathListModel.remove(removed)); + list.updateUI(); + setModified(true); + } + + @Override + protected ListCellRenderer createListCellRenderer(JBList list) { + return new PythonPathListCellRenderer(super.createListCellRenderer(list), myPathListModel); + } + + @Override + protected void addToolbarButtons(ToolbarDecorator toolbarDecorator) { + AnActionButton reloadButton = new AnActionButton("Reload List of Paths", AllIcons.Actions.Refresh) { + @Override + public void actionPerformed(AnActionEvent e) { + onReloadButtonClicked(); + } + }; + toolbarDecorator.addExtraAction(reloadButton); + } + + protected void onReloadButtonClicked() { + } + + private static class PathListModel { + private Set myAdded = Sets.newHashSet(); + private Set myExcluded = Sets.newHashSet(); + private Set myFoundFiles = Sets.newHashSet(); + private List myFilteredOut = Lists.newArrayList(); + private final DefaultListModel myListModel; + private final OrderRootType myOrderRootType; + private Set myUserAddedToRemove = Sets.newHashSet(); + + public PathListModel(OrderRootType orderRootType, DefaultListModel listModel) { + myOrderRootType = orderRootType; + myListModel = listModel; + } + + private int getRowCount() { + return myListModel.getSize(); + } + + private VirtualFile getValueAt(int row) { + return (VirtualFile)myListModel.get(row); + } + + public boolean add(List files) { + for (VirtualFile file : files) { + if (!myFoundFiles.contains(file)) { + if (!myExcluded.remove(file)) { //if it was excluded we only delete exclusion mark + myAdded.add(file); + myUserAddedToRemove.remove(file); + } + else { + myFoundFiles.add(file); + return true; + } + } else { + myExcluded.remove(file); + } + } + return false; + } + + public int[] remove(List> files) { + List toRemove = Lists.newArrayList(); + for (Pair e : files) { + if (myAdded.contains(e.first)) { + toRemove.add(e.second); + myAdded.remove(e.first); + myUserAddedToRemove.add(e.first); + } + else if (myExcluded.contains(e.first)) { + myExcluded.remove(e.first); + } + else { + myExcluded.add(e.first); + } + } + return ArrayUtil.toIntArray(toRemove); + } + + public void apply(SdkModificator sdkModificator) { + sdkModificator.setSdkAdditionalData(collectSdkAdditionalData(sdkModificator)); + addFilteredOutRoots(sdkModificator); + } + + private void addFilteredOutRoots(SdkModificator sdkModificator) { + for (VirtualFile file : myFilteredOut) { + sdkModificator.addRoot(file, myOrderRootType); + } + } + + private SdkAdditionalData collectSdkAdditionalData(SdkModificator sdkModificator) { + PythonSdkAdditionalData data = (PythonSdkAdditionalData)sdkModificator.getSdkAdditionalData(); + if (data == null) { + data = new PythonSdkAdditionalData(null); + } + data.setAddedPathsFromVirtualFiles(myAdded); + data.setExcludedPathsFromVirtualFiles(myExcluded); + return data; + } + + public void setAdded(Set added) { + myAdded = Sets.newHashSet(added); + } + + public void setExcluded(Set excluded) { + myExcluded = Sets.newHashSet(excluded); + } + + public String getPresentationSuffix(VirtualFile file) { + if (myAdded.contains(file)) { + return "(added by user)"; + } + if (myExcluded.contains(file)) { + return "(removed by user)"; + } + return ""; + } + + public List reload(List list) { + myFoundFiles.clear(); + myFoundFiles.addAll(list); + List result = filterOutStubs(list, myFilteredOut); + result.removeAll(myUserAddedToRemove); + result.addAll(myAdded); + + return result; + } + + public List reset(List list, SdkModificator modificator) { + myFilteredOut.clear(); + List result = filterOutStubs(list, myFilteredOut); + + myFoundFiles.clear(); + myFoundFiles.addAll(list); + myUserAddedToRemove.clear(); + + if (modificator.getSdkAdditionalData() instanceof PythonSdkAdditionalData) { + PythonSdkAdditionalData data = (PythonSdkAdditionalData)modificator.getSdkAdditionalData(); + setAdded(data.getAddedPathFiles()); + setExcluded(data.getExcludedPathFiles()); + result.addAll(myExcluded); + } + else if (modificator.getSdkAdditionalData() == null) { + myAdded.clear(); + myExcluded.clear(); + } + return result; + } + + private static List filterOutStubs(List list, List filteredOut) { + List result = Lists.newArrayList(); + filteredOut.clear(); + for (VirtualFile file : list) { + if (!isStubPath(file)) { + result.add(file); + } + else { + filteredOut.add(file); + } + } + return result; + } + + private static boolean isStubPath(@NotNull VirtualFile file) { + final String path = PythonSdkType.getSkeletonsRootPath(PathManager.getSystemPath()); + final VirtualFile skeletonRoot = LocalFileSystem.getInstance().findFileByPath(path); + if (skeletonRoot != null) { + return file.getPath().startsWith(skeletonRoot.getPath()); + } + else { + return false; + } + } + + public boolean isExcluded(VirtualFile path) { + return myExcluded.contains(path); + } + } + + + private static class PythonPathListCellRenderer extends ListCellRendererWrapper { + private final PathListModel model; + + public PythonPathListCellRenderer(final ListCellRenderer listCellRenderer, PathListModel model) { + super(); + this.model = model; + } + + @Override + public void customize(JList list, VirtualFile value, int index, boolean selected, boolean hasFocus) { + String suffix = model.getPresentationSuffix(value); + if (suffix.length() > 0) { + suffix = " " + suffix; + } + setText(value != null ? value.getPresentableUrl() + suffix : ""); + } + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/PythonSdkConfigurable.form b/python/ide/src/com/jetbrains/python/configuration/PythonSdkConfigurable.form new file mode 100644 index 000000000000..b1a81e8c2a8a --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PythonSdkConfigurable.form @@ -0,0 +1,32 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ide/src/com/jetbrains/python/configuration/PythonSdkConfigurable.java b/python/ide/src/com/jetbrains/python/configuration/PythonSdkConfigurable.java new file mode 100644 index 000000000000..3365ac8bbf24 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/PythonSdkConfigurable.java @@ -0,0 +1,571 @@ +package com.jetbrains.python.configuration; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.intellij.CommonBundle; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.*; +import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl; +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.OrderRootType; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.Splitter; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.remotesdk.RemoteCredentials; +import com.intellij.ui.*; +import com.intellij.ui.components.JBList; +import com.intellij.ui.components.JBTabbedPane; +import com.intellij.util.Consumer; +import com.intellij.util.NullableConsumer; +import com.intellij.util.NullableFunction; +import com.intellij.util.containers.FactoryMap; +import com.intellij.webcore.packaging.PackagesNotificationPanel; +import com.jetbrains.python.packaging.ui.PyInstalledPackagesPanel; +import com.jetbrains.python.packaging.ui.PyPackageManagementService; +import com.jetbrains.python.remote.PythonRemoteInterpreterManager; +import com.jetbrains.python.sdk.*; +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; +import icons.PythonIcons; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.util.*; +import java.util.List; + +public class PythonSdkConfigurable implements Configurable, Configurable.NoScroll { + private JPanel myPanel; + private JList mySdkList; + private JPanel mySplitterHolder; + private PackagesNotificationPanel myNotificationsArea; + private JPanel myNotificationsPlaceholder; + private PythonPathEditor myPathEditor; + private boolean mySdkListChanged = false; + private final PyConfigurableInterpreterList myInterpreterList; + private final ProjectSdksModel myProjectSdksModel; + private final PyInstalledPackagesPanel myPackagesPanel; + + private Map myModificators = new FactoryMap() { + @Override + protected SdkModificator create(Sdk sdk) { + return sdk.getSdkModificator(); + } + }; + private Set myModifiedModificators = new HashSet(); + private Sdk myPreviousSelection; + private boolean myFirstReset; + private final Project myProject; + + private boolean myNewProject = false; + private boolean myShowOtherProjectVirtualenvs = false; + + public void setNewProject(final boolean newProject) { + myNewProject = newProject; + } + + public PythonSdkConfigurable(Project project) { + myProject = project; + myInterpreterList = PyConfigurableInterpreterList.getInstance(myProject); + myProjectSdksModel = myInterpreterList.getModel(); + myFirstReset = true; + + + mySdkList = new JBList(); + mySdkList.setCellRenderer(new PySdkListCellRenderer("", myModificators)); + mySdkList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + ToolbarDecorator decorator = ToolbarDecorator.createDecorator(mySdkList).disableUpDownActions() + .setAddAction(new AnActionButtonRunnable() { + @Override + public void run(AnActionButton button) { + addSdk(button); + } + }) + .setEditAction(new AnActionButtonRunnable() { + @Override + public void run(AnActionButton button) { + editSdk(); + } + }) + .setRemoveAction(new AnActionButtonRunnable() { + @Override + public void run(AnActionButton button) { + removeSdk(); + } + }) + .addExtraAction(new CreateVirtualEnvButton()) + .addExtraAction(new ToggleVirtualEnvFilterButton()); + + + final Splitter splitter = new Splitter(true); + /* + final JScrollPane sdkListPane = ScrollPaneFactory.createScrollPane(mySdkList, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + sdkListPane.setPreferredSize(new Dimension(10, 10)); + */ + splitter.setFirstComponent(decorator.createPanel()); + + myPathEditor = + new PythonPathEditor("Classes", OrderRootType.CLASSES, FileChooserDescriptorFactory.createAllButJarContentsDescriptor()) { + @Override + protected void onReloadButtonClicked() { + reloadSdk(); + } + }; + + myNotificationsArea = new PackagesNotificationPanel(project); + myNotificationsPlaceholder.add(myNotificationsArea.getComponent(), BorderLayout.CENTER); + + final JBTabbedPane tabbedPane = new JBTabbedPane(SwingConstants.TOP); + myPackagesPanel = new PyInstalledPackagesPanel(project, myNotificationsArea); + tabbedPane.addTab("Packages", myPackagesPanel); + + JPanel panel1 = new JPanel(new GridBagLayout()); + Insets anInsets1 = new Insets(2, 2, 2, 2); + JScrollPane scrollPane1 = ScrollPaneFactory.createScrollPane(myPathEditor.createComponent(), + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane1.setPreferredSize(new Dimension(500, 500)); + panel1.add(scrollPane1, new GridBagConstraints(0, 0, 1, 8, 1.0, 1.0, + GridBagConstraints.CENTER, + GridBagConstraints.BOTH, + anInsets1, 0, 0)); + + tabbedPane.addTab("Paths", panel1); + + splitter.setSecondComponent(tabbedPane); + mySplitterHolder.add(splitter, BorderLayout.CENTER); + + addListeners(); + } + + private void addListeners() { + myProjectSdksModel.addListener(new SdkModel.Listener() { + @Override + public void sdkAdded(Sdk sdk) { + } + + @Override + public void beforeSdkRemove(Sdk sdk) { + } + + @Override + public void sdkChanged(Sdk sdk, String previousName) { + refreshSdkList(); + } + + @Override + public void sdkHomeSelected(Sdk sdk, String newSdkHome) { + } + }); + myPackagesPanel.addPathChangedListener(new Consumer() { + @Override + public void consume(Sdk sdk) { + updateSdkPaths(sdk); + } + }); + + myNotificationsArea.addLinkHandler(PyInstalledPackagesPanel.CREATE_VENV, new Runnable() { + @Override + public void run() { + createVirtualEnv(getSelectedSdk()); + } + }); + mySdkList.addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent event) { + updateUI(getSelectedSdk()); + } + }); + } + + private void updateUI(final Sdk selectedSdk) { + if (myPreviousSelection != null) { + saveSdkPaths(myPreviousSelection); + } + myProjectSdksModel.setProjectSdk(selectedSdk); + updateSdkPaths(selectedSdk); + myPreviousSelection = selectedSdk; + myPackagesPanel.updatePackages(selectedSdk == null ? null : new PyPackageManagementService(myProject, selectedSdk)); + + if (selectedSdk != null) { + myPackagesPanel.updateNotifications(selectedSdk); + } + } + + private void createVirtualEnv(Sdk sdk) { + CreateVirtualEnvDialog.VirtualEnvCallback callback = new CreateVirtualEnvDialog.VirtualEnvCallback() { + @Override + public void virtualEnvCreated(Sdk sdk, boolean associateWithProject, boolean setAsProjectInterpreter) { + PythonSdkType.setupSdkPaths(sdk, myProject, null); + if (associateWithProject) { + SdkAdditionalData additionalData = sdk.getSdkAdditionalData(); + if (additionalData == null) { + additionalData = new PythonSdkAdditionalData(PythonSdkFlavor.getFlavor(sdk.getHomePath())); + ((ProjectJdkImpl)sdk).setSdkAdditionalData(additionalData); + } + if (myNewProject) { + ((PythonSdkAdditionalData)additionalData).associateWithNewProject(); + } + else { + ((PythonSdkAdditionalData)additionalData).associateWithProject(myProject); + } + } + addCreatedSdk(sdk, true, setAsProjectInterpreter); + } + }; + final List allSdks = PyConfigurableInterpreterList.getInstance(myProject).getAllPythonSdks(); + final CreateVirtualEnvDialog dialog = new CreateVirtualEnvDialog(myProject, myNewProject, allSdks, sdk); + dialog.show(); + if (dialog.isOK()) { + dialog.createVirtualEnv(allSdks, callback); + } + } + + public String getDisplayName() { + return "Python Interpreters"; + } + + public String getHelpTopic() { + return "python_interpreter"; + } + + public JComponent createComponent() { + return myPanel; + } + + public boolean isModified() { + return mySdkListChanged || + myProjectSdksModel.isModified() || + myPathEditor.isModified() || + !myModifiedModificators.isEmpty(); + } + + @Nullable + private String getSelectedSdkName() { + final Sdk selectedSdk = (Sdk)mySdkList.getSelectedValue(); + return selectedSdk == null ? null : selectedSdk.getName(); + } + + public void apply() throws ConfigurationException { + if (myPreviousSelection != null) { + saveSdkPaths(myPreviousSelection); + } + for (SdkModificator modificator : myModifiedModificators) { + modificator.commitChanges(); + } + myModificators.clear(); + myModifiedModificators.clear(); + myProjectSdksModel.apply(); + mySdkListChanged = false; + } + + /** + * Returns the stable copy of the SDK currently selected in the SDK table. + * + * @return the selected SDK, or null if there's no selection + */ + @Nullable + public Sdk getRealSelectedSdk() { + return ProjectJdkTable.getInstance().findJdk(getSelectedSdkName()); + } + + @Nullable + public Sdk getSelectedSdk() { + return (Sdk)mySdkList.getSelectedValue(); + } + + public void reset() { + clearModificators(); + if (myFirstReset) { + myFirstReset = false; + } + else { + myProjectSdksModel.reset(null); + } + refreshSdkList(); + final Sdk selectedSdk = getRealSelectedSdk(); + if (selectedSdk != null) { + myPackagesPanel.updateNotifications(selectedSdk); + } + } + + private void clearModificators() { + myModificators.clear(); + myModifiedModificators.clear(); + myPreviousSelection = null; + } + + private void refreshSdkList() { + final List pythonSdks = myInterpreterList.getAllPythonSdks(); + Sdk projectSdk = myProjectSdksModel.getProjectSdk(); + if (!myShowOtherProjectVirtualenvs) { + VirtualEnvProjectFilter.removeNotMatching(myProject, pythonSdks); + } + Collections.sort(pythonSdks, new PreferredSdkComparator()); + mySdkList.setModel(new CollectionListModel(pythonSdks)); + + mySdkListChanged = false; + if (projectSdk == null) projectSdk = getSdk(); + if (projectSdk != null) { + projectSdk = myProjectSdksModel.findSdk(projectSdk.getName()); + mySdkList.clearSelection(); + mySdkList.setSelectedValue(projectSdk, true); + mySdkList.updateUI(); + } + } + + @Nullable + private Sdk getSdk() { + final Module[] modules = ModuleManager.getInstance(myProject).getModules(); + if (modules.length > 0) { + final Module module = modules[0]; + final ModuleRootManager rootManager = ModuleRootManager.getInstance(module); + return rootManager.getSdk(); + } + return ProjectRootManager.getInstance(myProject).getProjectSdk(); + } + + private void addSdk(AnActionButton button) { + InterpreterPathChooser + .show(myProject, myProjectSdksModel.getSdks(), button.getPreferredPopupPoint(), false, new NullableConsumer() { + @Override + public void consume(Sdk sdk) { + addCreatedSdk(sdk, false, false); + } + }); + } + + private void addCreatedSdk(@Nullable final Sdk sdk, boolean newVirtualEnv, boolean makeActive) { + if (sdk != null) { + boolean isVirtualEnv = PythonSdkType.isVirtualEnv(sdk); + boolean askSetAsProjectInterpreter = !myProject.isDefault() && !myNewProject; + if (askSetAsProjectInterpreter && isVirtualEnv && !newVirtualEnv) { + AddVEnvOptionsDialog dialog = new AddVEnvOptionsDialog(myPanel); + dialog.show(); + if (dialog.getExitCode() != DialogWrapper.OK_EXIT_CODE) { + return; + } + SdkModificator modificator = myModificators.get(sdk); + setSdkAssociated(modificator, !dialog.makeAvailableToAll()); + myModifiedModificators.add(modificator); + makeActive = dialog.useForThisProject(); + } + myProjectSdksModel.addSdk(sdk); + refreshSdkList(); + mySdkList.setSelectedValue(sdk, true); + mySdkListChanged = true; + if (askSetAsProjectInterpreter && !isVirtualEnv && !PythonSdkType.isInvalid(sdk) && !PythonSdkType.isIncompleteRemote(sdk)) { + //TODO: make native mac dialog work + makeActive = Messages.showIdeaMessageDialog(myProject, "Do you want to set this interpreter as Project Interpreter?", + "Project Interpreter", + new String[]{CommonBundle.getYesButtonText(), CommonBundle.getNoButtonText()}, 0, null, + null + ) == Messages.YES; + } + if (makeActive) { + SdkConfigurationUtil.setDirectoryProjectSdk(myProject, sdk); + myProjectSdksModel.setProjectSdk(sdk); + myInterpreterList.setSelectedSdk(sdk); + } + } + } + + private void editSdk() { + final Sdk currentSdk = getSelectedSdk(); + if (currentSdk != null) { + if (currentSdk.getSdkAdditionalData() instanceof RemoteCredentials) { + editRemoteSdk(currentSdk); + } + else { + editSdk(currentSdk); + } + updateUI(currentSdk); + } + } + + private void editRemoteSdk(Sdk currentSdk) { + PythonRemoteInterpreterManager remoteInterpreterManager = PythonRemoteInterpreterManager.getInstance(); + if (remoteInterpreterManager != null) { + final SdkModificator modificator = myModificators.get(currentSdk); + Set existingSdks = Sets.newHashSet(myProjectSdksModel.getSdks()); + existingSdks.remove(currentSdk); + if (remoteInterpreterManager.editSdk(myProject, modificator, existingSdks)) { + myModifiedModificators.add(modificator); + } + } + } + + private void editSdk(final Sdk currentSdk) { + final SdkModificator modificator = myModificators.get(currentSdk); + final EditSdkDialog dialog = new EditSdkDialog(myProject, modificator, new NullableFunction() { + @Override + public String fun(String s) { + if (isDuplicateSdkName(s, currentSdk)) { + return "Please specify a unique name for the interpreter"; + } + return null; + } + }); + dialog.show(); + if (dialog.isOK()) { + final boolean pathChanged = !Comparing.equal(currentSdk.getHomePath(), dialog.getHomePath()); + if (!currentSdk.getName().equals(dialog.getName()) || pathChanged || dialog.isAssociateChanged()) { + myModifiedModificators.add(modificator); + modificator.setName(dialog.getName()); + modificator.setHomePath(dialog.getHomePath()); + + if (dialog.isAssociateChanged()) { + setSdkAssociated(modificator, dialog.associateWithProject()); + } + if (pathChanged) { + reloadSdk(currentSdk); + } + } + } + } + + private void setSdkAssociated(SdkModificator modificator, boolean isAssociated) { + PythonSdkAdditionalData additionalData = (PythonSdkAdditionalData)modificator.getSdkAdditionalData(); + if (additionalData == null) { + additionalData = new PythonSdkAdditionalData(PythonSdkFlavor.getFlavor(modificator.getHomePath())); + modificator.setSdkAdditionalData(additionalData); + } + if (isAssociated) { + additionalData.associateWithProject(myProject); + } + else { + additionalData.setAssociatedProjectPath(null); + } + } + + private boolean isDuplicateSdkName(String s, Sdk sdk) { + for (Sdk existingSdk : myProjectSdksModel.getSdks()) { + if (existingSdk == sdk) { + continue; + } + String existingName; + if (myModificators.containsKey(existingSdk)) { + existingName = myModificators.get(existingSdk).getName(); + } + else { + existingName = existingSdk.getName(); + } + if (existingName.equals(s)) { + return true; + } + } + return false; + } + + private void removeSdk() { + final Sdk current_sdk = getSelectedSdk(); + if (current_sdk != null) { + myProjectSdksModel.removeSdk(current_sdk); + if (myModificators.containsKey(current_sdk)) { + SdkModificator modificator = myModificators.get(current_sdk); + myModifiedModificators.remove(modificator); + myModificators.remove(current_sdk); + } + refreshSdkList(); + mySdkListChanged = true; + // TODO select initially selected SDK + if (mySdkList.getSelectedIndex() < 0) { + mySdkList.setSelectedIndex(0); + } + } + } + + private void reloadSdk() { + final Sdk currentSdk = getSelectedSdk(); + if (currentSdk != null) { + myModifiedModificators.add(myModificators.get(currentSdk)); + reloadSdk(currentSdk); + } + } + + private void reloadSdk(Sdk currentSdk) { + PythonSdkType.setupSdkPaths(myProject, null, currentSdk, myModificators.get(currentSdk)); // or must it be a RunWriteAction? + reloadSdkPaths(currentSdk); + } + + public void disposeUIResources() { + myInterpreterList.disposeModel(); + clearModificators(); + myFirstReset = true; + } + + private void saveSdkPaths(Sdk selection) { + SdkModificator modificator = myModificators.get(selection); + if (myPathEditor.isModified()) { + myPathEditor.apply(modificator); + myModifiedModificators.add(modificator); + } + } + + private void reloadSdkPaths(Sdk selection) { + List rootPaths = Lists.newArrayList(); + if (selection != null) { + Collections.addAll(rootPaths, selection.getRootProvider().getFiles(OrderRootType.CLASSES)); + myPathEditor.reload(myModificators.get(selection)); + } + else { + myPathEditor.reload(null); + } + } + + private void updateSdkPaths(final Sdk selection) { + final List rootPaths = Lists.newArrayList(); + if (selection != null) { + Collections.addAll(rootPaths, selection.getRootProvider().getFiles(OrderRootType.CLASSES)); + myPathEditor.reset(myModificators.get(selection)); + } + else { + myPathEditor.reset(null); + } + } + + private class CreateVirtualEnvButton extends AnActionButton implements DumbAware { + public CreateVirtualEnvButton() { + super("Create Virtual Environment", PythonIcons.Python.Virtualenv); + } + + @Override + public void actionPerformed(AnActionEvent e) { + Sdk selectedSdk = getSelectedSdk(); + createVirtualEnv(selectedSdk); + } + } + + private class ToggleVirtualEnvFilterButton extends ToggleActionButton implements DumbAware { + public ToggleVirtualEnvFilterButton() { + super("Show virtual environments associated with other projects", AllIcons.General.Filter); + } + + @Override + public boolean isSelected(AnActionEvent e) { + return myShowOtherProjectVirtualenvs; + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + myShowOtherProjectVirtualenvs = state; + refreshSdkList(); + } + } +} diff --git a/python/ide/src/com/jetbrains/python/configuration/VirtualEnvProjectFilter.java b/python/ide/src/com/jetbrains/python/configuration/VirtualEnvProjectFilter.java new file mode 100644 index 000000000000..50b2b7436a3f --- /dev/null +++ b/python/ide/src/com/jetbrains/python/configuration/VirtualEnvProjectFilter.java @@ -0,0 +1,50 @@ +package com.jetbrains.python.configuration; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.util.io.FileUtil; +import com.jetbrains.python.sdk.PythonSdkAdditionalData; +import com.jetbrains.python.sdk.PythonSdkType; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** +* @author yole +*/ +public class VirtualEnvProjectFilter implements Predicate { + private final String myBasePath; + + public VirtualEnvProjectFilter(@Nullable final String basePath) { + myBasePath = basePath; + } + + @Override + public boolean apply(@Nullable final Sdk input) { + if (input != null && PythonSdkType.isVirtualEnv(input)) { + PythonSdkAdditionalData data = (PythonSdkAdditionalData) input.getSdkAdditionalData(); + if (data != null) { + final String path = data.getAssociatedProjectPath(); + if (path != null && (myBasePath == null || !path.equals(myBasePath))) { + return true; + } + } + } + return false; + } + + public static void removeNotMatching(Project project, List sdks) { + if (project != null) { + final String basePath = project.getBasePath(); + if (basePath != null) { + Iterables.removeIf(sdks, new VirtualEnvProjectFilter(FileUtil.toSystemIndependentName(basePath))); + } + } + } + + public static void removeAllAssociated(List sdks) { + Iterables.removeIf(sdks, new VirtualEnvProjectFilter(null)); + } +} diff --git a/python/ide/src/com/jetbrains/python/newProject/PythonNewDirectoryProjectAction.java b/python/ide/src/com/jetbrains/python/newProject/PythonNewDirectoryProjectAction.java new file mode 100644 index 000000000000..1919be622ea3 --- /dev/null +++ b/python/ide/src/com/jetbrains/python/newProject/PythonNewDirectoryProjectAction.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.newProject; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkAdditionalData; +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.DirectoryProjectGenerator; +import com.intellij.platform.NewDirectoryProjectAction; +import com.jetbrains.python.sdk.PythonSdkAdditionalData; +import com.jetbrains.python.sdk.PythonSdkType; + +import java.util.List; + +/** + * User : catherine + */ +public class PythonNewDirectoryProjectAction extends NewDirectoryProjectAction { + private Sdk mySdk; + private boolean myInstallFramework; + + public void actionPerformed(final AnActionEvent e) { + Project project = e.getData(PlatformDataKeys.PROJECT); + if (project == null) { + project = ProjectManager.getInstance().getDefaultProject(); + } + PythonNewDirectoryProjectDialog dlg = new PythonNewDirectoryProjectDialog(project); + dlg.show(); + if (dlg.getExitCode() != DialogWrapper.OK_EXIT_CODE) return; + mySdk = dlg.getSdk(); + myInstallFramework = dlg.installFramework(); + Project newProject = generateProject(project, dlg); + if (newProject != null) { + SdkConfigurationUtil.setDirectoryProjectSdk(newProject, mySdk); + final List sdks = PythonSdkType.getAllSdks(); + for (Sdk sdk : sdks) { + final SdkAdditionalData additionalData = sdk.getSdkAdditionalData(); + if (additionalData instanceof PythonSdkAdditionalData) { + ((PythonSdkAdditionalData) additionalData).reassociateWithCreatedProject(newProject); + } + } + } + } + + @Override + protected Object showSettings(DirectoryProjectGenerator generator, VirtualFile baseDir) + throws ProcessCanceledException { + Object settings = super.showSettings(generator, baseDir); + if (settings instanceof PyNewProjectSettings) { + ((PyNewProjectSettings)settings).setSdk(mySdk); + ((PyNewProjectSettings)settings).setInstallFramework(myInstallFramework); + } + return settings; + } +} diff --git a/python/ide/src/com/jetbrains/python/newProject/PythonNewDirectoryProjectDialog.java b/python/ide/src/com/jetbrains/python/newProject/PythonNewDirectoryProjectDialog.java new file mode 100644 index 000000000000..1b959844722e --- /dev/null +++ b/python/ide/src/com/jetbrains/python/newProject/PythonNewDirectoryProjectDialog.java @@ -0,0 +1,292 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.newProject; + +import com.intellij.facet.ui.ValidationResult; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.DirectoryProjectGenerator; +import com.intellij.platform.NewDirectoryProjectDialog; +import com.intellij.remotesdk.RemoteSdkData; +import com.intellij.ui.ComboboxWithBrowseButton; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.ui.components.JBLabel; +import com.jetbrains.python.PythonSdkChooserCombo; +import com.jetbrains.python.configuration.VirtualEnvProjectFilter; +import com.jetbrains.python.packaging.PyExternalProcessException; +import com.jetbrains.python.packaging.PyPackage; +import com.jetbrains.python.packaging.PyPackageManager; +import com.jetbrains.python.packaging.PyPackageManagerImpl; +import com.jetbrains.python.remote.PythonRemoteInterpreterManager; +import com.jetbrains.python.remote.RemoteProjectSettings; +import com.jetbrains.python.sdk.PreferredSdkComparator; +import com.jetbrains.python.sdk.PythonSdkType; +import com.jetbrains.python.sdk.flavors.JythonSdkFlavor; +import com.jetbrains.python.sdk.flavors.PyPySdkFlavor; +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Collections; +import java.util.List; + +/** + * User : catherine + */ +public class PythonNewDirectoryProjectDialog extends NewDirectoryProjectDialog { + private ComboboxWithBrowseButton mySdkCombo; + private final JCheckBox myFrameworkCheckbox; + private boolean myInstallFrameworkChanged; + private final Project myProject; + + protected PythonNewDirectoryProjectDialog(Project project) { + super(project); + myProject = project; + + final List sdks = PythonSdkType.getAllSdks(); + VirtualEnvProjectFilter.removeAllAssociated(sdks); + Collections.sort(sdks, PreferredSdkComparator.INSTANCE); + final Sdk preferred = sdks.isEmpty() ? null : sdks.iterator().next(); + mySdkCombo = new PythonSdkChooserCombo(project, sdks, new Condition() { + @Override + public boolean value(Sdk sdk) { + return sdk == preferred; + } + }); + final JLabel label = new JBLabel("Interpreter:", SwingConstants.LEFT) { + @Override + public Dimension getMinimumSize() { + return new JLabel("Project name:").getPreferredSize(); + } + + @Override + public Dimension getPreferredSize() { + return getMinimumSize(); + } + }; + label.setLabelFor(mySdkCombo); + final JPanel placeholder = getPlaceHolder(); + final GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridx = 0; + constraints.gridy = 0; + constraints.anchor = GridBagConstraints.WEST; + constraints.insets = new Insets(0, 0, 0, 10); + placeholder.add(label, constraints); + + constraints.gridx = 1; + constraints.gridy = 0; + constraints.fill = GridBagConstraints.BOTH; + constraints.weightx = 1.0; + constraints.insets = new Insets(0, 0, 0, 0); + + placeholder.add(mySdkCombo, constraints); + + final ActionListener listener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + checkValid(); + } + }; + + myFrameworkCheckbox = new JBCheckBox("Install "); + constraints.gridx = 0; + constraints.gridy = 1; + constraints.gridwidth = 2; + constraints.weightx = 0.0; + placeholder.add(myFrameworkCheckbox, constraints); + myFrameworkCheckbox.setVisible(false); + + myFrameworkCheckbox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + myInstallFrameworkChanged = true; + checkValid(); + } + }); + + mySdkCombo.addActionListener(listener); + mySdkCombo.getComboBox().addActionListener(listener); + myProjectTypeComboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + selectCompatiblePython(); + checkValid(); + } + }); + + mySdkCombo.getComboBox().addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + checkValid(); + } + }); + + final Dimension checkBoxSize = myFrameworkCheckbox.getPreferredSize(); + myRootPane.setPreferredSize(new Dimension(myRootPane.getPreferredSize().width, + myRootPane.getPreferredSize().height + checkBoxSize.height)); + + checkValid(); + } + + @Override + protected Object getEmptyProjectGenerator() { + return new DirectoryProjectGenerator() { + + @NotNull + @Nls + @Override + public String getName() { + return "Empty project"; + } + + @Nullable + @Override + public Object showGenerationSettings(VirtualFile baseDir) throws ProcessCanceledException { + if (PythonSdkType.isRemote(getSdk())) { + PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance(); + assert manager != null; + return manager.showRemoteProjectSettingsDialog(baseDir, (RemoteSdkData)getSdk().getSdkAdditionalData()); + } + else { + return null; + } + } + + @Override + public void generateProject(@NotNull Project project, + @NotNull VirtualFile baseDir, + Object settings, + @NotNull Module module) { + if (settings instanceof RemoteProjectSettings) { + PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance(); + assert manager != null; + manager.createDeployment(project, baseDir, (RemoteProjectSettings)settings, + (RemoteSdkData)getSdk().getSdkAdditionalData()); + } + } + + @NotNull + @Override + public ValidationResult validate(@NotNull String baseDirPath) { + if (PythonSdkType.isRemote(getSdk())) { + if (PythonRemoteInterpreterManager.getInstance() == null) { + return new ValidationResult(PythonRemoteInterpreterManager.WEB_DEPLOYMENT_PLUGIN_IS_DISABLED); + } + } + return ValidationResult.OK; + } + }; + } + + @Override + public void checkValid() { + super.checkValid(); + Sdk sdk = getSdk(); + if (isOKActionEnabled()) { + setOKActionEnabled(true); + setErrorText(null); + myFrameworkCheckbox.setVisible(false); + + DirectoryProjectGenerator generator = getProjectGenerator(); + final boolean isPy3k = sdk != null && PythonSdkType.getLanguageLevelForSdk(sdk).isPy3K(); + if (sdk != null && PythonSdkType.isRemote(sdk) && !acceptsRemoteSdk(generator)) { + setOKActionEnabled(false); + setErrorText("Please choose a local interpreter"); + } + else if (generator instanceof PyFrameworkProjectGenerator) { + PyFrameworkProjectGenerator frameworkProjectGenerator = (PyFrameworkProjectGenerator)generator; + String frameworkName = frameworkProjectGenerator.getFrameworkTitle(); + if (sdk != null && !isFrameworkInstalled(sdk)) { + final PyPackageManagerImpl packageManager = (PyPackageManagerImpl)PyPackageManager.getInstance(sdk); + final boolean onlyWithCache = + PythonSdkFlavor.getFlavor(sdk) instanceof JythonSdkFlavor || PythonSdkFlavor.getFlavor(sdk) instanceof PyPySdkFlavor; + try { + if (onlyWithCache && packageManager.cacheIsNotNull() || !onlyWithCache) { + final PyPackage pip = packageManager.findPackage("pip"); + myFrameworkCheckbox.setText("Install " + frameworkName); + myFrameworkCheckbox.setMnemonic(frameworkName.charAt(0)); + myFrameworkCheckbox.setVisible(pip != null); + if (!myInstallFrameworkChanged) { + myFrameworkCheckbox.setSelected(pip != null); + } + } + } + catch (PyExternalProcessException e) { + myFrameworkCheckbox.setVisible(false); + } + if (!myFrameworkCheckbox.isSelected()) { + setErrorText("No " + frameworkName + " support installed in selected interpreter"); + setOKActionEnabled(false); + } + } + if (isPy3k && !((PyFrameworkProjectGenerator)generator).supportsPython3()) { + setErrorText(frameworkName + " is not supported for the selected interpreter"); + setOKActionEnabled(false); + } + } + if (sdk == null) { + setOKActionEnabled(false); + setErrorText("No Python interpreter selected"); + } + } + } + + private void selectCompatiblePython() { + DirectoryProjectGenerator generator = getProjectGenerator(); + if (generator instanceof PyFrameworkProjectGenerator && !((PyFrameworkProjectGenerator)generator).supportsPython3()) { + Sdk sdk = getSdk(); + if (sdk != null && PythonSdkType.getLanguageLevelForSdk(sdk).isPy3K()) { + Sdk python2Sdk = PythonSdkType.findPython2Sdk(null); + if (python2Sdk != null) { + mySdkCombo.getComboBox().setSelectedItem(python2Sdk); + mySdkCombo.getComboBox().repaint(); + } + } + } + } + + private boolean isFrameworkInstalled(Sdk sdk) { + PyFrameworkProjectGenerator projectGenerator = (PyFrameworkProjectGenerator)getProjectGenerator(); + + return projectGenerator != null && projectGenerator.isFrameworkInstalled(myProject, sdk); + } + + private static boolean acceptsRemoteSdk(DirectoryProjectGenerator generator) { + if (generator instanceof PyFrameworkProjectGenerator) { + return ((PyFrameworkProjectGenerator)generator).acceptsRemoteSdk(); + } + return true; + } + + public boolean installFramework() { + return myFrameworkCheckbox.isSelected() && myFrameworkCheckbox.isVisible(); + } + + public Sdk getSdk() { + return (Sdk)mySdkCombo.getComboBox().getSelectedItem(); + } +} diff --git a/python/python-ide.iml b/python/python-ide.iml index 0a9c7f9fc149..b1c3c129ab68 100644 --- a/python/python-ide.iml +++ b/python/python-ide.iml @@ -22,6 +22,7 @@ +
diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index 75fe8c3eb215..0d6326a88198 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -423,6 +423,7 @@ +