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