Use myPackagesCache only from a syncrhonized context
[idea/community.git] / python / ide / src / com / jetbrains / python / newProject / actions / AbstractProjectSettingsStep.java
1 package com.jetbrains.python.newProject.actions;
2
3 import com.intellij.facet.ui.ValidationResult;
4 import com.intellij.icons.AllIcons;
5 import com.intellij.ide.impl.ProjectUtil;
6 import com.intellij.ide.util.projectWizard.WebProjectTemplate;
7 import com.intellij.openapi.actionSystem.AnAction;
8 import com.intellij.openapi.actionSystem.AnActionEvent;
9 import com.intellij.openapi.actionSystem.Presentation;
10 import com.intellij.openapi.actionSystem.impl.ActionButtonWithText;
11 import com.intellij.openapi.extensions.Extensions;
12 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
13 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
14 import com.intellij.openapi.project.DumbAware;
15 import com.intellij.openapi.project.Project;
16 import com.intellij.openapi.project.ProjectManager;
17 import com.intellij.openapi.projectRoots.Sdk;
18 import com.intellij.openapi.ui.MessageType;
19 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
20 import com.intellij.openapi.ui.ValidationInfo;
21 import com.intellij.openapi.util.Condition;
22 import com.intellij.openapi.util.io.FileUtil;
23 import com.intellij.openapi.wm.impl.welcomeScreen.AbstractActionWithPanel;
24 import com.intellij.platform.DirectoryProjectGenerator;
25 import com.intellij.platform.WebProjectGenerator;
26 import com.intellij.ui.DocumentAdapter;
27 import com.intellij.ui.JBColor;
28 import com.intellij.ui.components.JBScrollPane;
29 import com.intellij.util.NullableConsumer;
30 import com.intellij.util.ui.UIUtil;
31 import com.jetbrains.python.PythonSdkChooserCombo;
32 import com.jetbrains.python.configuration.PyConfigurableInterpreterList;
33 import com.jetbrains.python.configuration.VirtualEnvProjectFilter;
34 import com.jetbrains.python.newProject.PyFrameworkProjectGenerator;
35 import com.jetbrains.python.newProject.PythonProjectGenerator;
36 import com.jetbrains.python.packaging.PyPackageManager;
37 import com.jetbrains.python.sdk.PySdkUtil;
38 import com.jetbrains.python.sdk.PythonSdkType;
39 import icons.PythonIcons;
40 import org.jetbrains.annotations.Nullable;
41
42 import javax.swing.*;
43 import javax.swing.border.Border;
44 import javax.swing.border.LineBorder;
45 import javax.swing.event.DocumentEvent;
46 import java.awt.*;
47 import java.awt.event.ActionEvent;
48 import java.awt.event.ActionListener;
49 import java.awt.event.FocusEvent;
50 import java.awt.event.MouseEvent;
51 import java.beans.PropertyChangeEvent;
52 import java.beans.PropertyChangeListener;
53 import java.io.File;
54 import java.util.List;
55
56 abstract public class AbstractProjectSettingsStep extends AbstractActionWithPanel implements DumbAware {
57   protected final DirectoryProjectGenerator myProjectGenerator;
58   private final NullableConsumer<AbstractProjectSettingsStep> myCallback;
59   private final boolean myIsWelcomeScreen;
60   private PythonSdkChooserCombo mySdkCombo;
61   private boolean myInstallFramework;
62   private TextFieldWithBrowseButton myLocationField;
63   protected final File myProjectDirectory;
64   private Button myCreateButton;
65   private JLabel myErrorLabel;
66   private AnAction myCreateAction;
67   private Sdk mySdk;
68
69   public AbstractProjectSettingsStep(DirectoryProjectGenerator projectGenerator,
70                                      NullableConsumer<AbstractProjectSettingsStep> callback,
71                                      boolean isWelcomeScreen) {
72     super();
73     myProjectGenerator = projectGenerator;
74     myCallback = callback;
75     myIsWelcomeScreen = isWelcomeScreen;
76     myProjectDirectory = FileUtil.findSequentNonexistentFile(new File(ProjectUtil.getBaseDir()), "untitled", "");
77     if (myProjectGenerator instanceof WebProjectTemplate) {
78       ((WebProjectTemplate)myProjectGenerator).getPeer().addSettingsStateListener(new WebProjectGenerator.SettingsStateListener() {
79         @Override
80         public void stateChanged(boolean validSettings) {
81           checkValid();
82         }
83       });
84     }
85     else if (myProjectGenerator instanceof PythonProjectGenerator) {
86       ((PythonProjectGenerator)myProjectGenerator).addSettingsStateListener(new PythonProjectGenerator.SettingsListener() {
87         @Override
88         public void stateChanged() {
89           checkValid();
90         }
91       });
92     }
93
94     myCreateAction = new AnAction("Create", "Create Project", getIcon()) {
95       @Override
96       public void actionPerformed(AnActionEvent e) {
97         boolean isValid = checkValid();
98         if (isValid && myCallback != null)
99           myCallback.consume(AbstractProjectSettingsStep.this);
100       }
101     };
102   }
103
104   @Override
105   public void actionPerformed(AnActionEvent e) {
106   }
107
108   @Override
109   public JPanel createPanel() {
110     final JPanel basePanel = createBasePanel();
111     final JPanel mainPanel = new JPanel(new BorderLayout());
112
113     final JPanel scrollPanel = new JPanel(new BorderLayout());
114
115     final DirectoryProjectGenerator[] generators = Extensions.getExtensions(DirectoryProjectGenerator.EP_NAME);
116     final int height = generators.length == 0 && !myIsWelcomeScreen ? 150 : 400;
117     mainPanel.setPreferredSize(new Dimension(mainPanel.getPreferredSize().width, height));
118     myErrorLabel = new JLabel("");
119     myErrorLabel.setForeground(JBColor.RED);
120     myCreateButton = new Button(myCreateAction, myCreateAction.getTemplatePresentation());
121
122     scrollPanel.add(basePanel, BorderLayout.NORTH);
123     final JPanel advancedSettings = createAdvancedSettings();
124     if (advancedSettings != null) {
125       scrollPanel.add(advancedSettings, BorderLayout.CENTER);
126     }
127     final JBScrollPane scrollPane = new JBScrollPane(scrollPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
128                                                                                                       ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
129     scrollPane.setBorder(null);
130     mainPanel.add(scrollPane, BorderLayout.CENTER);
131
132     final JPanel bottomPanel = new JPanel(new BorderLayout());
133
134     bottomPanel.add(myErrorLabel, BorderLayout.NORTH);
135     bottomPanel.add(myCreateButton, BorderLayout.EAST);
136     mainPanel.add(bottomPanel, BorderLayout.SOUTH);
137     return mainPanel;
138   }
139
140   protected Icon getIcon() {
141     return myProjectGenerator.getLogo();
142   }
143
144   private JPanel createBasePanel() {
145     final JPanel panel = new JPanel(new GridBagLayout());
146     final GridBagConstraints c = new GridBagConstraints();
147     c.fill = GridBagConstraints.HORIZONTAL;
148     c.anchor = GridBagConstraints.NORTHWEST;
149     c.weightx = 0;
150     c.insets = new Insets(2, 2, 2, 2);
151     myLocationField = new TextFieldWithBrowseButton();
152     myLocationField.setText(myProjectDirectory.toString());
153
154     final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor();
155     myLocationField.addBrowseFolderListener("Select base directory", "Select base directory for the Project",
156                                             null, descriptor);
157     myLocationField.getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
158       @Override
159       protected void textChanged(DocumentEvent e) {
160         if (myProjectGenerator instanceof PythonProjectGenerator) {
161           String path = myLocationField.getText().trim();
162           if (path.endsWith(File.separator)) {
163             path = path.substring(0, path.length() - File.separator.length());
164           }
165           int ind = path.lastIndexOf(File.separator);
166           if (ind != -1) {
167             String projectName = path.substring(ind + 1, path.length());
168             ((PythonProjectGenerator)myProjectGenerator).locationChanged(projectName);
169           }
170         }
171       }
172     });
173     final JLabel locationLabel = new JLabel("Location:");
174     c.gridx = 0;
175     c.gridy = 0;
176     panel.add(locationLabel, c);
177
178     c.gridx = 1;
179     c.gridy = 0;
180     c.weightx = 1.;
181     panel.add(myLocationField, c);
182
183     final JLabel interpreterLabel = new JLabel("Interpreter:", SwingConstants.LEFT) {
184       @Override
185       public Dimension getMinimumSize() {
186         return new JLabel("Project name:").getPreferredSize();
187       }
188
189       @Override
190       public Dimension getPreferredSize() {
191         return getMinimumSize();
192       }
193     };
194     c.gridx = 0;
195     c.gridy = 1;
196     c.weightx = 0;
197     panel.add(interpreterLabel, c);
198
199     final Project project = ProjectManager.getInstance().getDefaultProject();
200     final List<Sdk> sdks = PyConfigurableInterpreterList.getInstance(project).getAllPythonSdks();
201     VirtualEnvProjectFilter.removeAllAssociated(sdks);
202     Sdk compatibleSdk = sdks.isEmpty() ? null : sdks.iterator().next();
203     DirectoryProjectGenerator generator = getProjectGenerator();
204     if (generator instanceof PyFrameworkProjectGenerator && !((PyFrameworkProjectGenerator)generator).supportsPython3()) {
205       if (compatibleSdk != null && PythonSdkType.getLanguageLevelForSdk(compatibleSdk).isPy3K()) {
206         Sdk python2Sdk = PythonSdkType.findPython2Sdk(sdks);
207         if (python2Sdk != null) {
208           compatibleSdk = python2Sdk;
209
210         }
211       }
212     }
213
214     final Sdk preferred = compatibleSdk;
215     mySdkCombo = new PythonSdkChooserCombo(project, sdks, new Condition<Sdk>() {
216       @Override
217       public boolean value(Sdk sdk) {
218         return sdk == preferred;
219       }
220     });
221     mySdkCombo.setButtonIcon(PythonIcons.Python.InterpreterGear);
222
223     c.gridx = 1;
224     c.gridy = 1;
225     c.weightx = 1.;
226     panel.add(mySdkCombo, c);
227     final JPanel basePanelExtension = extendBasePanel();
228     if (basePanelExtension != null) {
229       c.gridwidth = 2;
230       c.gridy = 2;
231       c.gridx = 0;
232       panel.add(basePanelExtension, c);
233     }
234     registerValidators();
235     return panel;
236   }
237
238   @Nullable
239   protected JPanel extendBasePanel() {
240     if (myProjectGenerator instanceof PythonProjectGenerator)
241       return ((PythonProjectGenerator)myProjectGenerator).extendBasePanel();
242     return null;
243   }
244
245   protected void registerValidators() {
246     myLocationField.getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
247       @Override
248       protected void textChanged(DocumentEvent e) {
249         checkValid();
250       }
251     });
252     final ActionListener listener = new ActionListener() {
253       @Override
254       public void actionPerformed(ActionEvent e) {
255         checkValid();
256       }
257     };
258     mySdkCombo.getComboBox().addPropertyChangeListener(new PropertyChangeListener() {
259       @Override
260       public void propertyChange(PropertyChangeEvent event) {
261         checkValid();
262       }
263     });
264     myLocationField.getTextField().addActionListener(listener);
265     mySdkCombo.getComboBox().addActionListener(listener);
266     mySdkCombo.addActionListener(listener);
267   }
268
269   public boolean checkValid() {
270     if (myLocationField == null) return true;
271     final String projectName = myLocationField.getText();
272     setErrorText(null);
273     myInstallFramework = false;
274
275     if (projectName.trim().isEmpty()) {
276       setErrorText("Project name can't be empty");
277       return false;
278     }
279     if (myLocationField.getText().indexOf('$') >= 0) {
280       setErrorText("Project directory name must not contain the $ character");
281       return false;
282     }
283     if (myProjectGenerator != null) {
284       final String baseDirPath = myLocationField.getTextField().getText();
285       ValidationResult validationResult = myProjectGenerator.validate(baseDirPath);
286       if (!validationResult.isOk()) {
287         setErrorText(validationResult.getErrorMessage());
288         return false;
289       }
290       if (myProjectGenerator instanceof PythonProjectGenerator) {
291         final ValidationResult warningResult = ((PythonProjectGenerator)myProjectGenerator).warningValidation(getSdk());
292         if (!warningResult.isOk()) {
293           setWarningText(warningResult.getErrorMessage());
294         }
295       }
296       if (myProjectGenerator instanceof WebProjectTemplate) {
297         final WebProjectGenerator.GeneratorPeer peer = ((WebProjectTemplate)myProjectGenerator).getPeer();
298         final ValidationInfo validationInfo = peer.validate();
299         if (validationInfo != null && !peer.isBackgroundJobRunning()) {
300           setErrorText(validationInfo.message);
301           return false;
302         }
303       }
304     }
305
306     final Sdk sdk = getSdk();
307
308     final boolean isPy3k = sdk != null && PythonSdkType.getLanguageLevelForSdk(sdk).isPy3K();
309     if (sdk != null && PythonSdkType.isRemote(sdk) && !acceptsRemoteSdk(myProjectGenerator)) {
310       setErrorText("Please choose a local interpreter");
311       return false;
312     }
313     else if (myProjectGenerator instanceof PyFrameworkProjectGenerator) {
314       PyFrameworkProjectGenerator frameworkProjectGenerator = (PyFrameworkProjectGenerator)myProjectGenerator;
315       String frameworkName = frameworkProjectGenerator.getFrameworkTitle();
316       if (sdk != null && !isFrameworkInstalled(sdk)) {
317         String warningText = frameworkName + " will be installed on selected interpreter";
318         myInstallFramework = true;
319         final PyPackageManager packageManager = PyPackageManager.getInstance(sdk);
320         if (!packageManager.hasManagement(PySdkUtil.isRemote(sdk))) {
321           warningText = "Python packaging tools and " + warningText;
322         }
323         setWarningText(warningText);
324       }
325       if (isPy3k && !((PyFrameworkProjectGenerator)myProjectGenerator).supportsPython3()) {
326         setErrorText(frameworkName + " is not supported for the selected interpreter");
327         return false;
328       }
329     }
330     if (sdk == null) {
331       setErrorText("No Python interpreter selected");
332       return false;
333     }
334     return true;
335   }
336
337   public void setErrorText(@Nullable String text) {
338     myErrorLabel.setText(text);
339     myErrorLabel.setForeground(MessageType.ERROR.getTitleForeground());
340     myErrorLabel.setIcon(text == null ? null : AllIcons.Actions.Lightning);
341     myCreateButton.setEnabled(text == null);
342   }
343
344   public void setWarningText(@Nullable String text) {
345     myErrorLabel.setText("Note: " + text + "  ");
346     myErrorLabel.setForeground(MessageType.WARNING.getTitleForeground());
347   }
348
349   private static boolean acceptsRemoteSdk(DirectoryProjectGenerator generator) {
350     if (generator instanceof PyFrameworkProjectGenerator) {
351       return ((PyFrameworkProjectGenerator)generator).acceptsRemoteSdk();
352     }
353     return true;
354   }
355
356   private boolean isFrameworkInstalled(Sdk sdk) {
357     PyFrameworkProjectGenerator projectGenerator = (PyFrameworkProjectGenerator)getProjectGenerator();
358     return projectGenerator != null && projectGenerator.isFrameworkInstalled(sdk);
359   }
360
361   @Nullable
362   protected JPanel createAdvancedSettings() {
363     return null;
364   }
365
366   public DirectoryProjectGenerator getProjectGenerator() {
367     return myProjectGenerator;
368   }
369
370   private static class Button extends ActionButtonWithText {
371     private final Border myBorder;
372
373     public Button(AnAction action, Presentation presentation) {
374       super(action, presentation, "NewProject", new Dimension(70, 50));
375       final Border border = new LineBorder(JBColor.border(), 1, true);
376       myBorder = UIUtil.isUnderDarcula() ? UIUtil.getButtonBorder() : border;
377       setBorder(myBorder);
378     }
379
380     @Override
381     protected int iconTextSpace() {
382       return 8;
383     }
384
385     @Override
386     public boolean isFocusable() {
387       return true;
388     }
389
390     @Override
391     protected void processFocusEvent(FocusEvent e) {
392       super.processFocusEvent(e);
393       if (e.getID() == FocusEvent.FOCUS_GAINED) {
394         processMouseEvent(new MouseEvent(this, MouseEvent.MOUSE_ENTERED, System.currentTimeMillis(), 0, 0, 0, 0, false));
395
396       }
397       else if (e.getID() == FocusEvent.FOCUS_LOST) {
398         processMouseEvent(new MouseEvent(this, MouseEvent.MOUSE_EXITED, System.currentTimeMillis(), 0, 0, 0, 0, false));
399       }
400     }
401
402     @Override
403     public Insets getInsets() {
404       return new Insets(5,10,5,5);
405     }
406
407     @Override
408     protected int horizontalTextAlignment() {
409       return SwingConstants.LEFT;
410     }
411
412     protected void processMouseEvent(MouseEvent e) {
413       super.processMouseEvent(e);
414       if (e.getID() == MouseEvent.MOUSE_ENTERED) {
415         setBorder(null);
416       }
417       else if (e.getID() == MouseEvent.MOUSE_EXITED) {
418         setBorder(myBorder);
419       }
420     }
421   }
422
423   public Sdk getSdk() {
424     if (mySdk != null) return mySdk;
425     return (Sdk)mySdkCombo.getComboBox().getSelectedItem();
426   }
427
428   public void setSdk(final Sdk sdk) {
429     mySdk = sdk;
430   }
431
432   public String getProjectLocation() {
433     return myLocationField.getText();
434   }
435
436   public boolean installFramework() {
437     return myInstallFramework;
438   }
439
440 }