PY-17265 Extracted PyBaseMoveDialog to later create new dialog for the refactoring
[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.project.Project;
20 import com.intellij.openapi.ui.DialogWrapperPeer;
21 import com.intellij.psi.PsiNamedElement;
22 import com.intellij.refactoring.classMembers.MemberInfoChange;
23 import com.intellij.refactoring.classMembers.MemberInfoModel;
24 import com.intellij.refactoring.ui.AbstractMemberSelectionTable;
25 import com.intellij.ui.HideableDecorator;
26 import com.intellij.ui.RowIcon;
27 import com.intellij.ui.components.JBScrollPane;
28 import com.intellij.util.Function;
29 import com.intellij.util.containers.ContainerUtil;
30 import com.jetbrains.python.PyBundle;
31 import com.jetbrains.python.psi.PyClass;
32 import com.jetbrains.python.psi.PyElement;
33 import com.jetbrains.python.psi.PyFile;
34 import com.jetbrains.python.psi.PyFunction;
35 import com.jetbrains.python.psi.impl.PyPsiUtils;
36 import org.jetbrains.annotations.NonNls;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39 import org.jetbrains.annotations.TestOnly;
40
41 import javax.swing.*;
42 import javax.swing.event.TableModelEvent;
43 import javax.swing.event.TableModelListener;
44 import java.awt.*;
45 import java.util.Collection;
46 import java.util.Comparator;
47 import java.util.List;
48
49 /**
50  * @author Mikhail Golubev
51  */
52 public class PyMoveModuleMembersDialog extends PyBaseMoveDialog {
53   @NonNls private final static String BULK_MOVE_TABLE_VISIBLE = "python.move.module.members.dialog.show.table";
54
55   /**
56    * Instance to be injected to mimic this class in tests
57    */
58   private static PyMoveModuleMembersDialog ourInstanceToReplace = null;
59
60   private final TopLevelSymbolsSelectionTable myMemberSelectionTable;
61   private final PyModuleMemberInfoModel myModuleMemberModel;
62   private final boolean mySeveralElementsSelected;
63
64   /**
65    * Either creates new dialog or return singleton instance initialized with {@link #setInstanceToReplace)}.
66    * Singleton dialog is intended to be used in tests.
67    *
68    * @param project dialog project
69    * @param elements elements to move
70    * @param source
71    *@param destination destination where elements have to be moved  @return dialog
72    */
73   public static PyMoveModuleMembersDialog getInstance(@NotNull Project project,
74                                                       @NotNull List<PsiNamedElement> elements,
75                                                       @NotNull String source, 
76                                                       @NotNull String destination) {
77     return ourInstanceToReplace != null ? ourInstanceToReplace : new PyMoveModuleMembersDialog(project, elements, source, destination);
78   }
79
80   /**
81    * Injects instance to be used in tests
82    *
83    * @param instanceToReplace instance to be used in tests
84    */
85   @TestOnly
86   public static void setInstanceToReplace(@NotNull final PyMoveModuleMembersDialog instanceToReplace) {
87     ourInstanceToReplace = instanceToReplace;
88   }
89
90   /**
91    * @param project dialog project
92    * @param elements elements to move
93    * @param source
94    * @param destination destination where elements have to be moved
95    */
96   protected PyMoveModuleMembersDialog(@NotNull Project project,
97                                       @NotNull List<PsiNamedElement> elements,
98                                       @NotNull String source, 
99                                       @NotNull String destination) {
100     super(project, source, destination);
101
102     assert !elements.isEmpty();
103     final PsiNamedElement firstElement = elements.get(0);
104     setTitle(PyBundle.message("refactoring.move.module.members.dialog.title"));
105
106     final PyFile pyFile = (PyFile)firstElement.getContainingFile();
107     myModuleMemberModel = new PyModuleMemberInfoModel(pyFile);
108
109     final List<PyModuleMemberInfo> symbolsInfos = collectModuleMemberInfos(myModuleMemberModel.myPyFile);
110     for (PyModuleMemberInfo info : symbolsInfos) {
111       //noinspection SuspiciousMethodCalls
112       info.setChecked(elements.contains(info.getMember()));
113     }
114     myModuleMemberModel.memberInfoChanged(new MemberInfoChange<>(symbolsInfos));
115     myMemberSelectionTable = new TopLevelSymbolsSelectionTable(symbolsInfos, myModuleMemberModel);
116     myMemberSelectionTable.addMemberInfoChangeListener(myModuleMemberModel);
117     myMemberSelectionTable.getModel().addTableModelListener(new TableModelListener() {
118       @Override
119       public void tableChanged(TableModelEvent e) {
120         validateButtons();
121       }
122     });
123     mySeveralElementsSelected = elements.size() > 1;
124     final boolean tableIsVisible = mySeveralElementsSelected || PropertiesComponent.getInstance().getBoolean(BULK_MOVE_TABLE_VISIBLE);
125     final String description;
126     if (!tableIsVisible && elements.size() == 1) {
127       if (firstElement instanceof PyFunction) {
128         description =  PyBundle.message("refactoring.move.module.members.dialog.description.function.$0", firstElement.getName());
129       }
130       else if (firstElement instanceof PyClass) {
131         description = PyBundle.message("refactoring.move.module.members.dialog.description.class.$0", firstElement.getName());
132       }
133       else {
134         description = PyBundle.message("refactoring.move.module.members.dialog.description.variable.$0", firstElement.getName());
135       }
136     }
137     else {
138       description = PyBundle.message("refactoring.move.module.members.dialog.description.selection");
139     }
140     myDescription.setText(description);
141     final HideableDecorator decorator = new HideableDecorator(myExtraPanel, PyBundle.message("refactoring.move.module.members.dialog.table.title"), true) {
142       @Override
143       protected void on() {
144         super.on();
145         myDescription.setText(PyBundle.message("refactoring.move.module.members.dialog.description.selection"));
146         PropertiesComponent.getInstance().setValue(BULK_MOVE_TABLE_VISIBLE, true);
147       }
148
149       @Override
150       protected void off() {
151         super.off();
152         PropertiesComponent.getInstance().setValue(BULK_MOVE_TABLE_VISIBLE, false);
153       }
154     };
155     decorator.setOn(tableIsVisible);
156     decorator.setContentComponent(new JBScrollPane(myMemberSelectionTable) {
157       @Override
158       public Dimension getMinimumSize() {
159         // Prevent growth of the dialog after several expand/collapse actions
160         return new Dimension((int)super.getMinimumSize().getWidth(), 0);
161       }
162     });
163
164     init();
165   }
166
167   @Override
168   protected void doWhenFirstShown() {
169     super.doWhenFirstShown();
170     enlargeDialogHeightIfNecessary();
171   }
172   
173   private void enlargeDialogHeightIfNecessary() {
174     if (mySeveralElementsSelected && !PropertiesComponent.getInstance(getProject()).getBoolean(BULK_MOVE_TABLE_VISIBLE)) {
175       final DialogWrapperPeer peer = getPeer();
176       final Dimension realSize = peer.getSize();
177       final double preferredHeight = peer.getPreferredSize().getHeight();
178       if (realSize.getHeight() < preferredHeight) {
179         peer.setSize((int)realSize.getWidth(), (int)preferredHeight);
180       }
181     }
182   }
183
184   @Nullable
185   @Override
186   protected String getDimensionServiceKey() {
187     return "#com.jetbrains.python.refactoring.move.PyMoveModuleMembersDialog";
188   }
189
190   @Override
191   protected String getHelpId() {
192     return "python.reference.moveModuleMembers";
193   }
194
195   @Override
196   protected boolean areButtonsValid() {
197     return !myMemberSelectionTable.getSelectedMemberInfos().isEmpty();
198   }
199
200   /**
201    * @return selected elements in the same order as they are declared in the original file
202    */
203   @NotNull
204   public List<PyElement> getSelectedTopLevelSymbols() {
205     final Collection<PyModuleMemberInfo> selectedMembers = myMemberSelectionTable.getSelectedMemberInfos();
206     final List<PyElement> selectedElements = ContainerUtil.map(selectedMembers, info -> info.getMember());
207     return ContainerUtil.sorted(selectedElements, (e1, e2) -> PyPsiUtils.isBefore(e1, e2) ? -1 : 1);
208   }
209
210   @NotNull
211   private static List<PyModuleMemberInfo> collectModuleMemberInfos(@NotNull PyFile pyFile) {
212     final List<PyElement> moduleMembers = PyMoveModuleMembersHelper.getTopLevelModuleMembers(pyFile);
213     return ContainerUtil.mapNotNull(moduleMembers, element -> new PyModuleMemberInfo(element));
214   }
215
216   static class TopLevelSymbolsSelectionTable extends AbstractMemberSelectionTable<PyElement, PyModuleMemberInfo> {
217     public TopLevelSymbolsSelectionTable(Collection<PyModuleMemberInfo> memberInfos,
218                                          @Nullable MemberInfoModel<PyElement, PyModuleMemberInfo> memberInfoModel) {
219       super(memberInfos, memberInfoModel, null);
220     }
221
222     @Nullable
223     @Override
224     protected Object getAbstractColumnValue(PyModuleMemberInfo memberInfo) {
225       return null;
226     }
227
228     @Override
229     protected boolean isAbstractColumnEditable(int rowIndex) {
230       return false;
231     }
232
233     @Override
234     protected void setVisibilityIcon(PyModuleMemberInfo memberInfo, RowIcon icon) {
235
236     }
237
238     @Override
239     protected Icon getOverrideIcon(PyModuleMemberInfo memberInfo) {
240       return null;
241     }
242   }
243 }