97b83c6e16996b60c2167931b6b6fd7542e6be6d
[idea/community.git] / python / src / com / jetbrains / python / refactoring / changeSignature / PyChangeSignatureDialog.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.refactoring.changeSignature;
17
18 import com.intellij.lang.LanguageNamesValidation;
19 import com.intellij.lang.refactoring.NamesValidator;
20 import com.intellij.openapi.editor.Document;
21 import com.intellij.openapi.editor.event.DocumentAdapter;
22 import com.intellij.openapi.editor.event.DocumentEvent;
23 import com.intellij.openapi.fileTypes.LanguageFileType;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.ValidationInfo;
26 import com.intellij.openapi.ui.VerticalFlowLayout;
27 import com.intellij.openapi.util.Pair;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.psi.PsiCodeFragment;
30 import com.intellij.psi.PsiDocumentManager;
31 import com.intellij.psi.util.PsiTreeUtil;
32 import com.intellij.refactoring.BaseRefactoringProcessor;
33 import com.intellij.refactoring.changeSignature.CallerChooserBase;
34 import com.intellij.refactoring.changeSignature.ChangeSignatureDialogBase;
35 import com.intellij.refactoring.changeSignature.ParameterTableModelItemBase;
36 import com.intellij.refactoring.ui.ComboBoxVisibilityPanel;
37 import com.intellij.refactoring.ui.VisibilityPanelBase;
38 import com.intellij.ui.EditorTextField;
39 import com.intellij.ui.components.JBLabel;
40 import com.intellij.ui.treeStructure.Tree;
41 import com.intellij.util.Consumer;
42 import com.intellij.util.IJSwingUtilities;
43 import com.intellij.util.containers.HashSet;
44 import com.intellij.util.ui.UIUtil;
45 import com.intellij.util.ui.table.JBListTable;
46 import com.intellij.util.ui.table.JBTableRow;
47 import com.intellij.util.ui.table.JBTableRowEditor;
48 import com.jetbrains.python.PyBundle;
49 import com.jetbrains.python.PyNames;
50 import com.jetbrains.python.PythonFileType;
51 import com.jetbrains.python.PythonLanguage;
52 import com.jetbrains.python.psi.LanguageLevel;
53 import com.jetbrains.python.psi.PyFunction;
54 import com.jetbrains.python.psi.PyParameterList;
55 import com.jetbrains.python.refactoring.introduce.IntroduceValidator;
56 import org.jetbrains.annotations.NonNls;
57 import org.jetbrains.annotations.Nullable;
58
59 import javax.swing.*;
60 import java.awt.*;
61 import java.awt.event.ItemEvent;
62 import java.awt.event.ItemListener;
63 import java.util.ArrayList;
64 import java.util.List;
65 import java.util.Set;
66
67 /**
68  * User : ktisha
69  */
70
71 public class PyChangeSignatureDialog extends ChangeSignatureDialogBase<PyParameterInfo, PyFunction, String, PyMethodDescriptor, PyParameterTableModelItem, PyParameterTableModel> {
72
73   public PyChangeSignatureDialog(Project project,
74                                  PyMethodDescriptor method) {
75     super(project, method, false, method.getMethod().getContext());
76   }
77
78   @Override
79   protected LanguageFileType getFileType() {
80     return PythonFileType.INSTANCE;
81   }
82
83   @Override
84   protected PyParameterTableModel createParametersInfoModel(PyMethodDescriptor method) {
85     final PyParameterList parameterList = PsiTreeUtil.getChildOfType(method.getMethod(), PyParameterList.class);
86     return new PyParameterTableModel(parameterList, myDefaultValueContext, myProject);
87   }
88
89   @Override
90   protected BaseRefactoringProcessor createRefactoringProcessor() {
91     final List<PyParameterInfo> parameters = getParameters();
92     return new PyChangeSignatureProcessor(myProject, myMethod.getMethod(), getMethodName(),
93                                           parameters.toArray(new PyParameterInfo[parameters.size()]));
94   }
95
96   @Nullable
97   @Override
98   protected PsiCodeFragment createReturnTypeCodeFragment() {
99     return null;
100   }
101
102   @Nullable
103   @Override
104   protected CallerChooserBase<PyFunction> createCallerChooser(String title, Tree treeToReuse, Consumer<Set<PyFunction>> callback) {
105     return null;
106   }
107
108   public boolean isNameValid(final String name, final Project project) {
109     final NamesValidator validator = LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance());
110     return (name != null) &&
111            (validator.isIdentifier(name, project)) &&
112            !(validator.isKeyword(name, project));
113   }
114
115   @Nullable
116   @Override
117   protected String validateAndCommitData() {
118     final String functionName = myNameField.getText().trim();
119     if (!functionName.equals(myMethod.getName())) {
120       final boolean defined = IntroduceValidator.isDefinedInScope(functionName, myMethod.getMethod());
121       if (defined) {
122         return PyBundle.message("refactoring.change.signature.dialog.validation.name.defined");
123       }
124       if (!isNameValid(functionName, myProject)) {
125         return PyBundle.message("refactoring.change.signature.dialog.validation.function.name");
126       }
127     }
128     final List<PyParameterTableModelItem> parameters = myParametersTableModel.getItems();
129     Set<String> parameterNames = new HashSet<String>();
130     boolean hadPositionalContainer = false;
131     boolean hadKeywordContainer = false;
132     boolean hadDefaultValue = false;
133     boolean hadSingleStar = false;
134     boolean hadParamsAfterSingleStar = false;
135     LanguageLevel languageLevel = LanguageLevel.forElement(myMethod.getMethod());
136
137     int parametersLength = parameters.size();
138
139     for (int index = 0; index != parametersLength; ++index) {
140       PyParameterTableModelItem info = parameters.get(index);
141       final PyParameterInfo parameter = info.parameter;
142       final String name = parameter.getName();
143       final String nameWithoutStars = StringUtil.trimLeading(name, '*').trim();
144       if (parameterNames.contains(nameWithoutStars)) {
145         return PyBundle.message("ANN.duplicate.param.name");
146       }
147       parameterNames.add(nameWithoutStars);
148
149       if (name.equals("*")) {
150         hadSingleStar = true;
151         if (index == parametersLength-1) {
152           return PyBundle.message("ANN.named.arguments.after.star");
153         }
154       }
155       else if (name.startsWith("*") && !name.startsWith("**")) {
156         if (hadKeywordContainer) {
157           return PyBundle.message("ANN.starred.param.after.kwparam");
158         }
159         if (hadSingleStar || hadPositionalContainer) {
160           return PyBundle.message("refactoring.change.signature.dialog.validation.multiple.star");
161         }
162         if (!isNameValid(name.substring(1), myProject)) {
163           return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.name");
164         }
165         hadPositionalContainer = true;
166       }
167       else if (name.startsWith("**")) {
168         if (hadSingleStar && !hadParamsAfterSingleStar) {
169           return PyBundle.message("ANN.named.arguments.after.star");
170         }
171         if (hadKeywordContainer) {
172           return PyBundle.message("refactoring.change.signature.dialog.validation.multiple.double.star");
173         }
174         if (!isNameValid(name.substring(2), myProject)) {
175           return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.name");
176         }
177         hadKeywordContainer = true;
178       }
179       else {
180         if (!isNameValid(name, myProject)) {
181           return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.name");
182         }
183         if (hadSingleStar) {
184           hadParamsAfterSingleStar = true;
185         }
186         if (hadPositionalContainer && !languageLevel.isPy3K()) {
187           return PyBundle.message("ANN.regular.param.after.vararg");
188         }
189         else if (hadKeywordContainer) {
190           return PyBundle.message("ANN.regular.param.after.keyword");
191         }
192         final String defaultValue = info.getDefaultValue();
193         if (defaultValue != null && !StringUtil.isEmptyOrSpaces(defaultValue) && parameter.getDefaultInSignature()) {
194           hadDefaultValue = true;
195         }
196         else {
197           if (hadDefaultValue && !hadSingleStar && (!languageLevel.isPy3K() || !hadPositionalContainer)) {
198             return PyBundle.message("ANN.non.default.param.after.default");
199           }
200         }
201       }
202       if (parameter.getOldIndex() < 0 && !parameter.getName().startsWith("*")) {
203         if (StringUtil.isEmpty(info.defaultValueCodeFragment.getText()))
204           return PyBundle.message("refactoring.change.signature.dialog.validation.default.missing");
205         if (StringUtil.isEmptyOrSpaces(parameter.getName()))
206           return PyBundle.message("refactoring.change.signature.dialog.validation.parameter.missing");
207       }
208     }
209
210
211     return null;
212   }
213
214   @Override
215   protected ValidationInfo doValidate() {
216     final String message = validateAndCommitData();
217     SwingUtilities.invokeLater(new Runnable() {
218       public void run() {
219         getRefactorAction().setEnabled(message == null);
220         getPreviewAction().setEnabled(message == null);
221       }
222     });
223     if (message != null) return new ValidationInfo(message);
224     return super.doValidate();
225   }
226
227   @Override
228   public JComponent getPreferredFocusedComponent() {
229     return myNameField;
230   }
231
232   @Override
233   protected String calculateSignature() {
234     @NonNls StringBuilder builder = new StringBuilder();
235     builder.append(getMethodName());
236     builder.append("(");
237     final List<PyParameterTableModelItem> parameters = myParametersTableModel.getItems();
238     for (int i = 0; i != parameters.size(); ++i) {
239       PyParameterTableModelItem parameterInfo = parameters.get(i);
240       builder.append(parameterInfo.parameter.getName());
241       final String defaultValue = parameterInfo.defaultValueCodeFragment.getText();
242       if (!defaultValue.isEmpty() && parameterInfo.isDefaultInSignature()) {
243         builder.append(" = " + defaultValue);
244       }
245       if (i != parameters.size()-1)
246         builder.append(", ");
247     }
248     builder.append(")");
249     return builder.toString();
250   }
251
252   @Override
253   protected VisibilityPanelBase<String> createVisibilityControl() {
254     return new ComboBoxVisibilityPanel<String>(new String[0]);
255   }
256
257   @Override
258   protected JComponent getRowPresentation(ParameterTableModelItemBase<PyParameterInfo> item, boolean selected, final boolean focused) {
259     String text = item.parameter.getName();
260     final String defaultCallValue = item.defaultValueCodeFragment.getText();
261     PyParameterTableModelItem pyItem = (PyParameterTableModelItem)item;
262     final String defaultValue = pyItem.isDefaultInSignature()? pyItem.defaultValueCodeFragment.getText() : "";
263
264     if (StringUtil.isNotEmpty(defaultValue)) {
265       text += " = " + defaultValue;
266     }
267
268     String tail = "";
269     if (StringUtil.isNotEmpty(defaultCallValue)) {
270       tail += " default value = " + defaultCallValue;
271     }
272     if (!StringUtil.isEmpty(tail)) {
273       text += " //" + tail;
274     }
275     return JBListTable.createEditorTextFieldPresentation(getProject(), getFileType(), " " + text, selected, focused);
276   }
277
278   @Override
279   protected boolean isListTableViewSupported() {
280     return true;
281   }
282
283   @Override
284   protected JBTableRowEditor getTableEditor(final JTable t, final ParameterTableModelItemBase<PyParameterInfo> item) {
285     return new JBTableRowEditor() {
286       private EditorTextField myNameEditor;
287       private EditorTextField myDefaultValueEditor;
288       private JCheckBox myDefaultInSignature;
289
290       @Override
291       public void prepareEditor(JTable table, int row) {
292         setLayout(new GridLayout(1, 3));
293         final JPanel parameterPanel = createParameterPanel();
294         add(parameterPanel);
295         final JPanel defaultValuePanel = createDefaultValuePanel();
296         add(defaultValuePanel);
297         final JPanel defaultValueCheckBox = createDefaultValueCheckBox();
298         add(defaultValueCheckBox);
299
300         final String nameText = myNameEditor.getText();
301         myDefaultValueEditor.setEnabled(!nameText.startsWith("*")
302                                         && !PyNames.CANONICAL_SELF.equals(nameText));
303         myDefaultInSignature.setEnabled(!nameText.startsWith("*")
304                                         && !PyNames.CANONICAL_SELF.equals(nameText));
305       }
306
307       private JPanel createDefaultValueCheckBox() {
308         final JPanel defaultValuePanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 4, 2, true, false));
309
310         final JBLabel inSignatureLabel = new JBLabel(PyBundle.message("refactoring.change.signature.dialog.default.value.checkbox"),
311                                                      UIUtil.ComponentStyle.SMALL);
312         IJSwingUtilities.adjustComponentsOnMac(inSignatureLabel,
313                                                myDefaultInSignature);
314         defaultValuePanel.add(inSignatureLabel, BorderLayout.WEST);
315         myDefaultInSignature = new JCheckBox();
316         myDefaultInSignature.setSelected(
317           ((PyParameterTableModelItem)item).isDefaultInSignature());
318         myDefaultInSignature.addItemListener(new ItemListener() {
319           @Override
320           public void itemStateChanged(ItemEvent event) {
321             ((PyParameterTableModelItem)item)
322               .setDefaultInSignature(myDefaultInSignature.isSelected());
323           }
324         });
325         myDefaultInSignature.addChangeListener(mySignatureUpdater);
326         myDefaultInSignature.setEnabled(item.parameter.getOldIndex() == -1);
327         defaultValuePanel.add(myDefaultInSignature, BorderLayout.EAST);
328         return defaultValuePanel;
329       }
330
331       private JPanel createDefaultValuePanel() {
332         final JPanel defaultValuePanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 4, 2, true, false));
333         final Document doc = PsiDocumentManager.getInstance(getProject()).getDocument(item.defaultValueCodeFragment);
334         myDefaultValueEditor = new EditorTextField(doc, getProject(), getFileType());
335         final JBLabel defaultValueLabel = new JBLabel(PyBundle.message("refactoring.change.signature.dialog.default.value.label"),
336                                                       UIUtil.ComponentStyle.SMALL);
337         IJSwingUtilities.adjustComponentsOnMac(defaultValueLabel, myDefaultValueEditor);
338         defaultValuePanel.add(defaultValueLabel);
339         defaultValuePanel.add(myDefaultValueEditor);
340         myDefaultValueEditor.setPreferredWidth(t.getWidth() / 2);
341         myDefaultValueEditor.addDocumentListener(mySignatureUpdater);
342         return defaultValuePanel;
343       }
344
345       private JPanel createParameterPanel() {
346         final JPanel namePanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 4, 2, true, false));
347         myNameEditor = new EditorTextField(item.parameter.getName(), getProject(), getFileType());
348         final JBLabel nameLabel = new JBLabel(PyBundle.message("refactoring.change.signature.dialog.name.label"),
349                                               UIUtil.ComponentStyle.SMALL);
350         IJSwingUtilities.adjustComponentsOnMac(nameLabel, myNameEditor);
351         namePanel.add(nameLabel);
352         namePanel.add(myNameEditor);
353         myNameEditor.setPreferredWidth(t.getWidth() / 2);
354         myNameEditor.addDocumentListener(new DocumentAdapter() {
355           @Override
356           public void documentChanged(DocumentEvent event) {
357             fireDocumentChanged(event, 0);
358             myDefaultValueEditor.setEnabled(!myNameEditor.getText().startsWith("*"));
359             myDefaultInSignature.setEnabled(!myNameEditor.getText().startsWith("*"));
360           }
361         });
362
363         myNameEditor.addDocumentListener(mySignatureUpdater);
364         return namePanel;
365       }
366
367       @Override
368       public JBTableRow getValue() {
369         return new JBTableRow() {
370           @Override
371           public Object getValueAt(int column) {
372             switch (column) {
373               case 0: return myNameEditor.getText().trim();
374               case 1: return new Pair<PsiCodeFragment, Boolean>(item.defaultValueCodeFragment,
375                                                                 ((PyParameterTableModelItem)item).isDefaultInSignature());
376             }
377             return null;
378           }
379         };
380       }
381
382       @Override
383       public JComponent getPreferredFocusedComponent() {
384         return myNameEditor.getFocusTarget();
385       }
386
387       @Override
388       public JComponent[] getFocusableComponents() {
389         final List<JComponent> focusable = new ArrayList<JComponent>();
390         focusable.add(myNameEditor.getFocusTarget());
391         if (myDefaultValueEditor != null) {
392           focusable.add(myDefaultValueEditor.getFocusTarget());
393         }
394         return focusable.toArray(new JComponent[focusable.size()]);
395       }
396     };
397   }
398
399   @Override
400   protected boolean mayPropagateParameters() {
401     return false;
402   }
403
404   @Override
405   protected boolean postponeValidation() {
406     return false;
407   }
408 }