2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.jetbrains.python.refactoring.move;
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;
53 import javax.swing.event.TableModelEvent;
54 import javax.swing.event.TableModelListener;
57 import java.util.Collection;
58 import java.util.Comparator;
59 import java.util.List;
62 * @author Mikhail Golubev
64 public class PyMoveModuleMembersDialog extends RefactoringDialog {
65 @NonNls private final static String BULK_MOVE_TABLE_VISIBLE = "python.move.module.members.dialog.show.table";
68 * Instance to be injected to mimic this class in tests
70 private static PyMoveModuleMembersDialog ourInstanceToReplace = null;
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;
82 * Either creates new dialog or return singleton instance initialized with {@link #setInstanceToReplace)}.
83 * Singleton dialog is intended to be used in tests.
85 * @param project dialog project
86 * @param elements elements to move
87 * @param destination destination where elements have to be moved
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);
97 * Injects instance to be used in tests
99 * @param instanceToReplace instance to be used in tests
102 public static void setInstanceToReplace(@NotNull final PyMoveModuleMembersDialog instanceToReplace) {
103 ourInstanceToReplace = instanceToReplace;
107 * @param project dialog project
108 * @param elements elements to move
109 * @param destination destination where elements have to be moved
111 protected PyMoveModuleMembersDialog(@NotNull Project project, @NotNull final List<PsiNamedElement> elements, @Nullable String destination) {
112 super(project, true);
114 assert !elements.isEmpty();
115 final PsiNamedElement firstElement = elements.get(0);
116 setTitle(PyBundle.message("refactoring.move.module.members.dialog.title"));
118 final String sourceFilePath = getContainingFileName(firstElement);
119 mySourcePathField.setText(sourceFilePath);
120 if (destination == null) {
121 destination = sourceFilePath;
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"),
131 TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
133 final PyFile pyFile = (PyFile)firstElement.getContainingFile();
134 myModuleMemberModel = new PyModuleMemberInfoModel(pyFile);
136 final List<PyModuleMemberInfo> symbolsInfos = collectModuleMemberInfos(myModuleMemberModel.myPyFile);
137 for (PyModuleMemberInfo info : symbolsInfos) {
138 //noinspection SuspiciousMethodCalls
139 info.setChecked(elements.contains(info.getMember()));
141 myModuleMemberModel.memberInfoChanged(new MemberInfoChange<>(symbolsInfos));
142 myMemberSelectionTable = new TopLevelSymbolsSelectionTable(symbolsInfos, myModuleMemberModel);
143 myMemberSelectionTable.addMemberInfoChangeListener(myModuleMemberModel);
144 myMemberSelectionTable.getModel().addTableModelListener(new TableModelListener() {
146 public void tableChanged(TableModelEvent e) {
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());
157 else if (firstElement instanceof PyClass) {
158 description = PyBundle.message("refactoring.move.module.members.dialog.description.class.$0", firstElement.getName());
161 description = PyBundle.message("refactoring.move.module.members.dialog.description.variable.$0", firstElement.getName());
165 description = PyBundle.message("refactoring.move.module.members.dialog.description.selection");
167 myDescription.setText(description);
168 final HideableDecorator decorator = new HideableDecorator(myTablePanel, PyBundle.message("refactoring.move.module.members.dialog.table.title"), true) {
170 protected void on() {
172 myDescription.setText(PyBundle.message("refactoring.move.module.members.dialog.description.selection"));
173 PropertiesComponent.getInstance().setValue(BULK_MOVE_TABLE_VISIBLE, true);
177 protected void off() {
179 PropertiesComponent.getInstance().setValue(BULK_MOVE_TABLE_VISIBLE, false);
182 decorator.setOn(tableIsVisible);
183 decorator.setContentComponent(new JBScrollPane(myMemberSelectionTable) {
185 public Dimension getMinimumSize() {
186 // Prevent growth of the dialog after several expand/collapse actions
187 return new Dimension((int)super.getMinimumSize().getWidth(), 0);
191 UiNotifyConnector.doWhenFirstShown(myCenterPanel, () -> {
192 enlargeDialogHeightIfNecessary();
193 preselectLastPathComponent(myBrowseFieldWithButton.getTextField());
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);
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);
221 protected String getDimensionServiceKey() {
222 return "#com.jetbrains.python.refactoring.move.PyMoveModuleMembersDialog";
227 protected JComponent createCenterPanel() {
228 return myCenterPanel;
232 protected void doAction() {
237 protected String getHelpId() {
238 return "python.reference.moveModuleMembers";
242 public JComponent getPreferredFocusedComponent() {
243 return myBrowseFieldWithButton.getTextField();
247 protected boolean areButtonsValid() {
248 return !myMemberSelectionTable.getSelectedMemberInfos().isEmpty();
252 public String getTargetPath() {
253 return myBrowseFieldWithButton.getText();
257 * @return selected elements in the same order as they are declared in the original file
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);
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));
273 private static String getContainingFileName(@NotNull PsiElement element) {
274 final VirtualFile file = element.getContainingFile().getVirtualFile();
276 return FileUtil.toSystemDependentName(file.getPath());
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);
291 protected Object getAbstractColumnValue(PyModuleMemberInfo memberInfo) {
296 protected boolean isAbstractColumnEditable(int rowIndex) {
301 protected void setVisibilityIcon(PyModuleMemberInfo memberInfo, RowIcon icon) {
306 protected Icon getOverrideIcon(PyModuleMemberInfo memberInfo) {