a458adc26d3f1567c7cfc8efd850cf8880ae2fb0
[idea/community.git] / python / src / com / jetbrains / python / refactoring / move / PyMoveModuleMembersDialog.java
1 /*
2  * Copyright 2000-2015 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.move;
17
18 import com.intellij.ide.util.PropertiesComponent;
19 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
20 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.roots.ProjectRootManager;
23 import com.intellij.openapi.ui.DialogWrapperPeer;
24 import com.intellij.openapi.ui.TextComponentAccessor;
25 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.vfs.VirtualFile;
28 import com.intellij.psi.PsiElement;
29 import com.intellij.psi.PsiNamedElement;
30 import com.intellij.refactoring.classMembers.MemberInfoChange;
31 import com.intellij.refactoring.classMembers.MemberInfoModel;
32 import com.intellij.refactoring.ui.AbstractMemberSelectionTable;
33 import com.intellij.refactoring.ui.RefactoringDialog;
34 import com.intellij.ui.HideableDecorator;
35 import com.intellij.ui.RowIcon;
36 import com.intellij.ui.components.JBLabel;
37 import com.intellij.ui.components.JBScrollPane;
38 import com.intellij.util.Function;
39 import com.intellij.util.containers.ContainerUtil;
40 import com.intellij.util.ui.update.UiNotifyConnector;
41 import com.jetbrains.python.PyBundle;
42 import com.jetbrains.python.psi.PyClass;
43 import com.jetbrains.python.psi.PyElement;
44 import com.jetbrains.python.psi.PyFile;
45 import com.jetbrains.python.psi.PyFunction;
46 import com.jetbrains.python.psi.impl.PyPsiUtils;
47 import org.jetbrains.annotations.NonNls;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50 import org.jetbrains.annotations.TestOnly;
51
52 import javax.swing.*;
53 import javax.swing.event.TableModelEvent;
54 import javax.swing.event.TableModelListener;
55 import java.awt.*;
56 import java.io.File;
57 import java.util.Collection;
58 import java.util.Comparator;
59 import java.util.List;
60
61 /**
62  * @author Mikhail Golubev
63  */
64 public class PyMoveModuleMembersDialog extends RefactoringDialog {
65   @NonNls private final static String BULK_MOVE_TABLE_VISIBLE = "python.move.module.members.dialog.show.table";
66
67   /**
68    * Instance to be injected to mimic this class in tests
69    */
70   private static PyMoveModuleMembersDialog ourInstanceToReplace = null;
71
72   private final TopLevelSymbolsSelectionTable myMemberSelectionTable;
73   private final PyModuleMemberInfoModel myModuleMemberModel;
74   private final boolean mySeveralElementsSelected;
75   private JPanel myCenterPanel;
76   private JPanel myTablePanel;
77   private TextFieldWithBrowseButton myBrowseFieldWithButton;
78   private JBLabel myDescription;
79   private JTextField mySourcePathField;
80
81   /**
82    * Either creates new dialog or return singleton instance initialized with {@link #setInstanceToReplace)}.
83    * Singleton dialog is intended to be used in tests.
84    *
85    * @param project dialog project
86    * @param elements elements to move
87    * @param destination destination where elements have to be moved
88    * @return dialog
89    */
90   public static PyMoveModuleMembersDialog getInstance(@NotNull final Project project,
91                                                        @NotNull final List<PsiNamedElement> elements,
92                                                        @Nullable final String destination) {
93     return ourInstanceToReplace != null ? ourInstanceToReplace : new PyMoveModuleMembersDialog(project, elements, destination);
94   }
95
96   /**
97    * Injects instance to be used in tests
98    *
99    * @param instanceToReplace instance to be used in tests
100    */
101   @TestOnly
102   public static void setInstanceToReplace(@NotNull final PyMoveModuleMembersDialog instanceToReplace) {
103     ourInstanceToReplace = instanceToReplace;
104   }
105
106   /**
107    * @param project dialog project
108    * @param elements elements to move
109    * @param destination destination where elements have to be moved
110    */
111   protected PyMoveModuleMembersDialog(@NotNull Project project, @NotNull final List<PsiNamedElement> elements, @Nullable String destination) {
112     super(project, true);
113
114     assert !elements.isEmpty();
115     final PsiNamedElement firstElement = elements.get(0);
116     setTitle(PyBundle.message("refactoring.move.module.members.dialog.title"));
117
118     final String sourceFilePath = getContainingFileName(firstElement);
119     mySourcePathField.setText(sourceFilePath);
120     if (destination == null) {
121       destination = sourceFilePath;
122     }
123     myBrowseFieldWithButton.setText(destination);
124     final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor();
125     descriptor.setRoots(ProjectRootManager.getInstance(project).getContentRoots());
126     descriptor.withTreeRootVisible(true);
127     myBrowseFieldWithButton.addBrowseFolderListener(PyBundle.message("refactoring.move.module.members.dialog.choose.destination.file.title"),
128                                                     null,
129                                                     project,
130                                                     descriptor,
131                                                     TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
132
133     final PyFile pyFile = (PyFile)firstElement.getContainingFile();
134     myModuleMemberModel = new PyModuleMemberInfoModel(pyFile);
135
136     final List<PyModuleMemberInfo> symbolsInfos = collectModuleMemberInfos(myModuleMemberModel.myPyFile);
137     for (PyModuleMemberInfo info : symbolsInfos) {
138       //noinspection SuspiciousMethodCalls
139       info.setChecked(elements.contains(info.getMember()));
140     }
141     myModuleMemberModel.memberInfoChanged(new MemberInfoChange<>(symbolsInfos));
142     myMemberSelectionTable = new TopLevelSymbolsSelectionTable(symbolsInfos, myModuleMemberModel);
143     myMemberSelectionTable.addMemberInfoChangeListener(myModuleMemberModel);
144     myMemberSelectionTable.getModel().addTableModelListener(new TableModelListener() {
145       @Override
146       public void tableChanged(TableModelEvent e) {
147         validateButtons();
148       }
149     });
150     mySeveralElementsSelected = elements.size() > 1;
151     final boolean tableIsVisible = mySeveralElementsSelected || PropertiesComponent.getInstance().getBoolean(BULK_MOVE_TABLE_VISIBLE);
152     final String description;
153     if (!tableIsVisible && elements.size() == 1) {
154       if (firstElement instanceof PyFunction) {
155         description =  PyBundle.message("refactoring.move.module.members.dialog.description.function.$0", firstElement.getName());
156       }
157       else if (firstElement instanceof PyClass) {
158         description = PyBundle.message("refactoring.move.module.members.dialog.description.class.$0", firstElement.getName());
159       }
160       else {
161         description = PyBundle.message("refactoring.move.module.members.dialog.description.variable.$0", firstElement.getName());
162       }
163     }
164     else {
165       description = PyBundle.message("refactoring.move.module.members.dialog.description.selection");
166     }
167     myDescription.setText(description);
168     final HideableDecorator decorator = new HideableDecorator(myTablePanel, PyBundle.message("refactoring.move.module.members.dialog.table.title"), true) {
169       @Override
170       protected void on() {
171         super.on();
172         myDescription.setText(PyBundle.message("refactoring.move.module.members.dialog.description.selection"));
173         PropertiesComponent.getInstance().setValue(BULK_MOVE_TABLE_VISIBLE, true);
174       }
175
176       @Override
177       protected void off() {
178         super.off();
179         PropertiesComponent.getInstance().setValue(BULK_MOVE_TABLE_VISIBLE, false);
180       }
181     };
182     decorator.setOn(tableIsVisible);
183     decorator.setContentComponent(new JBScrollPane(myMemberSelectionTable) {
184       @Override
185       public Dimension getMinimumSize() {
186         // Prevent growth of the dialog after several expand/collapse actions
187         return new Dimension((int)super.getMinimumSize().getWidth(), 0);
188       }
189     });
190
191     UiNotifyConnector.doWhenFirstShown(myCenterPanel, () -> {
192       enlargeDialogHeightIfNecessary();
193       preselectLastPathComponent(myBrowseFieldWithButton.getTextField());
194     });
195     init();
196   }
197
198   private void enlargeDialogHeightIfNecessary() {
199     if (mySeveralElementsSelected && !PropertiesComponent.getInstance(getProject()).getBoolean(BULK_MOVE_TABLE_VISIBLE)) {
200       final DialogWrapperPeer peer = getPeer();
201       final Dimension realSize = peer.getSize();
202       final double preferredHeight = peer.getPreferredSize().getHeight();
203       if (realSize.getHeight() < preferredHeight) {
204         peer.setSize((int)realSize.getWidth(), (int)preferredHeight);
205       }
206     }
207   }
208
209   private static void preselectLastPathComponent(@NotNull JTextField field) {
210     final String text = field.getText();
211     final int start = text.lastIndexOf(File.separatorChar);
212     final int lastDotIndex = text.lastIndexOf('.');
213     final int end = lastDotIndex < 0 ? text.length() : lastDotIndex;
214     if (start + 1 < end) {
215       field.select(start + 1, end);
216     }
217   }
218
219   @Nullable
220   @Override
221   protected String getDimensionServiceKey() {
222     return "#com.jetbrains.python.refactoring.move.PyMoveModuleMembersDialog";
223   }
224
225   @Nullable
226   @Override
227   protected JComponent createCenterPanel() {
228     return myCenterPanel;
229   }
230
231   @Override
232   protected void doAction() {
233     close(OK_EXIT_CODE);
234   }
235
236   @Override
237   protected String getHelpId() {
238     return "python.reference.moveModuleMembers";
239   }
240
241   @Override
242   public JComponent getPreferredFocusedComponent() {
243     return myBrowseFieldWithButton.getTextField();
244   }
245
246   @Override
247   protected boolean areButtonsValid() {
248     return !myMemberSelectionTable.getSelectedMemberInfos().isEmpty();
249   }
250
251   @NotNull
252   public String getTargetPath() {
253     return myBrowseFieldWithButton.getText();
254   }
255
256   /**
257    * @return selected elements in the same order as they are declared in the original file
258    */
259   @NotNull
260   public List<PyElement> getSelectedTopLevelSymbols() {
261     final Collection<PyModuleMemberInfo> selectedMembers = myMemberSelectionTable.getSelectedMemberInfos();
262     final List<PyElement> selectedElements = ContainerUtil.map(selectedMembers, info -> info.getMember());
263     return ContainerUtil.sorted(selectedElements, (e1, e2) -> PyPsiUtils.isBefore(e1, e2) ? -1 : 1);
264   }
265
266   @NotNull
267   private static List<PyModuleMemberInfo> collectModuleMemberInfos(@NotNull PyFile pyFile) {
268     final List<PyElement> moduleMembers = PyMoveModuleMembersHelper.getTopLevelModuleMembers(pyFile);
269     return ContainerUtil.mapNotNull(moduleMembers, element -> new PyModuleMemberInfo(element));
270   }
271
272   @NotNull
273   private static String getContainingFileName(@NotNull PsiElement element) {
274     final VirtualFile file = element.getContainingFile().getVirtualFile();
275     if (file != null) {
276       return FileUtil.toSystemDependentName(file.getPath());
277     }
278     else {
279       return "";
280     }
281   }
282
283   static class TopLevelSymbolsSelectionTable extends AbstractMemberSelectionTable<PyElement, PyModuleMemberInfo> {
284     public TopLevelSymbolsSelectionTable(Collection<PyModuleMemberInfo> memberInfos,
285                                          @Nullable MemberInfoModel<PyElement, PyModuleMemberInfo> memberInfoModel) {
286       super(memberInfos, memberInfoModel, null);
287     }
288
289     @Nullable
290     @Override
291     protected Object getAbstractColumnValue(PyModuleMemberInfo memberInfo) {
292       return null;
293     }
294
295     @Override
296     protected boolean isAbstractColumnEditable(int rowIndex) {
297       return false;
298     }
299
300     @Override
301     protected void setVisibilityIcon(PyModuleMemberInfo memberInfo, RowIcon icon) {
302
303     }
304
305     @Override
306     protected Icon getOverrideIcon(PyModuleMemberInfo memberInfo) {
307       return null;
308     }
309   }
310 }