diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt index 96118a542ec..e34ced28a3c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt @@ -32,8 +32,10 @@ import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackages import com.intellij.refactoring.util.CommonRefactoringUtil import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.refactoring.canRefactor -import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesToUpperLevelDialog +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinSelectNestedClassRefactoringDialog +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesDialog import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog +import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext @@ -96,13 +98,23 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { } is KtClassOrObject -> { - if (elementsToSearch.size > 1) return false - - val outerClass = container.parent as KtClassOrObject - val newContainer = targetContainer - ?: outerClass.containingClassOrObject - ?: outerClass.containingFile.let { it.containingDirectory ?: it } - MoveKotlinNestedClassesToUpperLevelDialog(project, elementsToSearch.first() as KtClassOrObject, newContainer).show() + if (elementsToSearch.size > 1) { + // todo: allow moving multiple classes to upper level + if (targetContainer !is KtClassOrObject) { + val message = RefactoringBundle.getCannotRefactorMessage("Moving multiple nested classes to top-level is not supported") + CommonRefactoringUtil.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) + return true + } + @Suppress("UNCHECKED_CAST") + MoveKotlinNestedClassesDialog(project, + elementsToSearch.filterIsInstance(), + container, + targetContainer, + callback).show() + return true + } + KotlinSelectNestedClassRefactoringDialog.chooseNestedClassRefactoring(elementsToSearch.first() as KtClassOrObject, + targetContainer) } else -> throw AssertionError("Unexpected container: ${container.getElementTextWithContext()}") @@ -113,7 +125,11 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { private fun canMove(elements: Array, targetContainer: PsiElement?, editorMode: Boolean): Boolean { if (!super.canMove(elements, targetContainer)) return false - if (getUniqueContainer(elements) == null) return false + val container = getUniqueContainer(elements) ?: return false + + if (container is KtClassOrObject && targetContainer != null && targetContainer !is KtClassOrObject && elements.size > 1) { + return false + } return elements.all { e -> if (e is KtClass || (e is KtObjectDeclaration && !e.isObjectLiteral()) || e is KtNamedFunction || e is KtProperty) { @@ -128,7 +144,12 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { } override fun isValidTarget(psiElement: PsiElement?, sources: Array): Boolean { - return psiElement is PsiPackage || (psiElement is PsiDirectory && psiElement.getPackage() != null) || psiElement is KtFile + return psiElement is PsiPackage + || (psiElement is PsiDirectory && psiElement.getPackage() != null) + || psiElement is KtFile + || (psiElement is KtClassOrObject + && !(psiElement.hasModifier(KtTokens.ANNOTATION_KEYWORD)) + && !sources.any { it.parent is KtFile }) } override fun doMove(project: Project, elements: Array, targetContainer: PsiElement?, callback: MoveCallback?) { diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinSelectNestedClassRefactoringDialog.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinSelectNestedClassRefactoringDialog.kt new file mode 100644 index 00000000000..8bd3d2d6036 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinSelectNestedClassRefactoringDialog.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui + +import com.intellij.BundleBase +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.psi.PsiElement +import com.intellij.refactoring.RefactoringBundle +import com.intellij.refactoring.util.RadioUpDownListener +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import java.awt.BorderLayout +import javax.swing.* + +internal class KotlinSelectNestedClassRefactoringDialog private constructor ( + private val project: Project, + private val nestedClass: KtClassOrObject, + private val targetContainer: PsiElement? +) : DialogWrapper(project, true) { + private val moveToUpperLevelButton = JRadioButton() + private val moveMembersButton = JRadioButton() + + init { + title = RefactoringBundle.message("select.refactoring.title") + init() + } + + override fun createNorthPanel() = JLabel(RefactoringBundle.message("what.would.you.like.to.do")) + + override fun getPreferredFocusedComponent() = moveToUpperLevelButton + + override fun getDimensionServiceKey(): String { + return "#org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.ui.KotlinSelectInnerOrMembersRefactoringDialog" + } + + override fun createCenterPanel(): JComponent? { + moveToUpperLevelButton.text = BundleBase.replaceMnemonicAmpersand("Move &nested class ${nestedClass.name} to upper level") + moveToUpperLevelButton.isSelected = true + + moveMembersButton.text = BundleBase.replaceMnemonicAmpersand("&Move nested class ${nestedClass.name} to another class") + + ButtonGroup().apply { + add(moveToUpperLevelButton) + add(moveMembersButton) + } + + RadioUpDownListener(moveToUpperLevelButton, moveMembersButton) + + return JPanel(BorderLayout()).apply { + val box = Box.createVerticalBox().apply { + add(Box.createVerticalStrut(5)) + add(moveToUpperLevelButton) + add(moveMembersButton) + } + add(box, BorderLayout.CENTER) + } + } + + fun getNextDialog(): DialogWrapper? { + return when { + moveToUpperLevelButton.isSelected -> MoveKotlinNestedClassesToUpperLevelDialog(nestedClass, targetContainer) + moveMembersButton.isSelected -> MoveKotlinNestedClassesDialog(nestedClass, targetContainer) + else -> null + } + } + + companion object { + private fun MoveKotlinNestedClassesToUpperLevelDialog( + nestedClass: KtClassOrObject, + targetContainer: PsiElement? + ): MoveKotlinNestedClassesToUpperLevelDialog { + val outerClass = nestedClass.containingClassOrObject!! + val newTarget = targetContainer + ?: outerClass.containingClassOrObject + ?: outerClass.containingFile.let { it.containingDirectory ?: it } + return MoveKotlinNestedClassesToUpperLevelDialog(nestedClass.project, nestedClass, newTarget) + } + + private fun MoveKotlinNestedClassesDialog( + nestedClass: KtClassOrObject, + targetContainer: PsiElement? + ): MoveKotlinNestedClassesDialog { + return MoveKotlinNestedClassesDialog(nestedClass.project, + listOf(nestedClass), + nestedClass.containingClassOrObject!!, + targetContainer as? KtClassOrObject ?: nestedClass.containingClassOrObject!!, + null) + } + + fun chooseNestedClassRefactoring(nestedClass: KtClassOrObject, targetContainer: PsiElement?) { + val project = nestedClass.project + val dialog = when { + targetContainer != null && targetContainer !is KtClassOrObject || + nestedClass is KtClass && nestedClass.isInner() -> { + MoveKotlinNestedClassesToUpperLevelDialog(nestedClass, targetContainer) + } + else -> { + val selectionDialog = KotlinSelectNestedClassRefactoringDialog(project, nestedClass, targetContainer) + selectionDialog.show() + if (selectionDialog.exitCode != OK_EXIT_CODE) return + selectionDialog.getNextDialog() ?: return + } + } + dialog.show() + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.form b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.form new file mode 100644 index 00000000000..3ff6a397818 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.form @@ -0,0 +1,113 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.java new file mode 100644 index 00000000000..59397bdf84f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.java @@ -0,0 +1,256 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui; + +import com.intellij.ide.util.ClassFilter; +import com.intellij.ide.util.TreeClassChooser; +import com.intellij.ide.util.TreeJavaClassChooserDialog; +import com.intellij.openapi.editor.event.DocumentAdapter; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.psi.JavaCodeFragment; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.classMembers.AbstractMemberInfoModel; +import com.intellij.refactoring.classMembers.MemberInfoChange; +import com.intellij.refactoring.move.MoveCallback; +import com.intellij.refactoring.move.MoveHandler; +import com.intellij.refactoring.ui.RefactoringDialog; +import com.intellij.ui.ReferenceEditorComboWithBrowseButton; +import kotlin.collections.CollectionsKt; +import kotlin.jvm.functions.Function1; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.kotlin.asJava.KtLightClassForExplicitDeclaration; +import org.jetbrains.kotlin.asJava.LightClassUtil; +import org.jetbrains.kotlin.asJava.LightClassUtilsKt; +import org.jetbrains.kotlin.idea.projectView.KtClassOrObjectTreeNode; +import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo; +import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionPanel; +import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionTable; +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*; +import org.jetbrains.kotlin.psi.*; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +public class MoveKotlinNestedClassesDialog extends RefactoringDialog { + private static final String RECENTS_KEY = MoveKotlinNestedClassesDialog.class.getName() + ".RECENTS_KEY"; + + private static class MemberInfoModelImpl extends AbstractMemberInfoModel { + + } + + private JPanel mainPanel; + private JTextField originalClassField; + private JPanel membersInfoPanel; + private ReferenceEditorComboWithBrowseButton targetClassChooser; + private JCheckBox openInEditorCheckBox; + private JPanel targetClassChooserPanel; + private KotlinMemberSelectionTable memberTable; + + private final KtClassOrObject originalClass; + private KtClassOrObject targetClass; + private final MoveCallback moveCallback; + + public MoveKotlinNestedClassesDialog( + @NotNull Project project, + @NotNull List elementsToMove, + @NotNull KtClassOrObject originalClass, + @NotNull KtClassOrObject targetClass, + @Nullable MoveCallback moveCallback + ) { + super(project, true); + + this.originalClass = originalClass; + this.targetClass = targetClass; + this.moveCallback = moveCallback; + + init(); + + setTitle(MoveHandler.REFACTORING_NAME); + + initClassChooser(targetClass); + + initMemberInfo(elementsToMove); + + validateButtons(); + } + + private void initClassChooser(@NotNull KtClassOrObject initialTargetClass) { + //noinspection ConstantConditions + originalClassField.setText(originalClass.getFqName().asString()); + + //noinspection ConstantConditions + targetClassChooser = new ReferenceEditorComboWithBrowseButton( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + TreeClassChooser chooser = new TreeJavaClassChooserDialog( + RefactoringBundle.message("choose.destination.class"), + myProject, + GlobalSearchScope.projectScope(myProject), + new ClassFilter() { + @Override + public boolean isAccepted(PsiClass aClass) { + if (!(aClass instanceof KtLightClassForExplicitDeclaration)) return false; + KtClassOrObject classOrObject = ((KtLightClassForExplicitDeclaration) aClass).getOrigin(); + + if (classOrObject.isTopLevel()) return true; + + if (classOrObject instanceof KtObjectDeclaration) { + return !((KtObjectDeclaration) classOrObject).isObjectLiteral(); + } + + if (classOrObject instanceof KtClass) { + return !((KtClass) classOrObject).isInner(); + } + + return false; + } + }, + null, + null, + true + ) { + @Nullable + @Override + protected PsiClass getSelectedFromTreeUserObject(DefaultMutableTreeNode node) { + PsiClass psiClass = super.getSelectedFromTreeUserObject(node); + if (psiClass != null) return psiClass; + + Object userObject = node.getUserObject(); + if (!(userObject instanceof KtClassOrObjectTreeNode)) return null; + return LightClassUtilsKt.toLightClass(((KtClassOrObjectTreeNode) userObject).getValue()); + } + }; + chooser.selectDirectory((targetClass != null ? targetClass : originalClass).getContainingFile().getContainingDirectory()); + chooser.showDialog(); + + PsiClass aClass = chooser.getSelected(); + if (aClass instanceof KtLightClassForExplicitDeclaration) { + targetClass = ((KtLightClassForExplicitDeclaration) aClass).getOrigin(); + targetClassChooser.setText(aClass.getQualifiedName()); + } + } + }, + initialTargetClass.getFqName().asString(), + myProject, + true, + JavaCodeFragment.VisibilityChecker.PROJECT_SCOPE_VISIBLE, + RECENTS_KEY); + targetClassChooser.getChildComponent().getDocument().addDocumentListener( + new DocumentAdapter() { + @Override + public void documentChanged(DocumentEvent e) { + PsiClass aClass = JavaPsiFacade + .getInstance(myProject) + .findClass(targetClassChooser.getText(), GlobalSearchScope.projectScope(myProject)); + targetClass = aClass instanceof KtLightClassForExplicitDeclaration + ? ((KtLightClassForExplicitDeclaration) aClass).getOrigin() + : null; + validateButtons(); + } + } + ); + targetClassChooserPanel.add(targetClassChooser); + } + + private void initMemberInfo(@NotNull final List elementsToMove) { + List memberInfos = CollectionsKt.mapNotNull( + originalClass.getDeclarations(), + new Function1() { + @Override + public KotlinMemberInfo invoke(KtDeclaration declaration) { + if (!(declaration instanceof KtClassOrObject)) return null; + + KotlinMemberInfo memberInfo = new KotlinMemberInfo((KtClassOrObject) declaration, false); + memberInfo.setChecked(elementsToMove.contains(declaration)); + return memberInfo; + } + } + ); + KotlinMemberSelectionPanel selectionPanel = new KotlinMemberSelectionPanel(getTitle(), memberInfos, null); + memberTable = selectionPanel.getTable(); + MemberInfoModelImpl memberInfoModel = new MemberInfoModelImpl(); + memberInfoModel.memberInfoChanged(new MemberInfoChange(memberInfos)); + selectionPanel.getTable().setMemberInfoModel(memberInfoModel); + selectionPanel.getTable().addMemberInfoChangeListener(memberInfoModel); + membersInfoPanel.add(selectionPanel, BorderLayout.CENTER); + } + + private List getSelectedElementsToMove() { + return CollectionsKt.map( + memberTable.getSelectedMemberInfos(), + new Function1() { + @Override + public KtClassOrObject invoke(KotlinMemberInfo info) { + return (KtClassOrObject) info.getMember(); + } + } + ); + } + + @Override + protected JComponent createCenterPanel() { + return mainPanel; + } + + @Override + protected String getDimensionServiceKey() { + return "#" + getClass().getName(); + } + + @Override + protected void canRun() throws ConfigurationException { + if (targetClass == null) throw new ConfigurationException("No destination class specified"); + + if (originalClass == targetClass) { + throw new ConfigurationException(RefactoringBundle.message("source.and.destination.classes.should.be.different")); + } + + for (KtClassOrObject classOrObject : getSelectedElementsToMove()) { + if (PsiTreeUtil.isAncestor(classOrObject, targetClass, false)) { + throw new ConfigurationException("Cannot move nested class " + classOrObject.getName() + " to itself"); + } + } + } + + @Override + protected void doAction() { + List elementsToMove = getSelectedElementsToMove(); + KotlinMoveTarget target = new KotlinMoveTargetForExistingElement(targetClass); + MoveDeclarationsDelegate.NestedClass delegate = new MoveDeclarationsDelegate.NestedClass(); + MoveDeclarationsDescriptor descriptor = new MoveDeclarationsDescriptor( + elementsToMove, target, delegate, false, false, true, false, moveCallback, openInEditorCheckBox.isSelected() + ); + invokeRefactoring(new MoveKotlinDeclarationsProcessor(myProject, descriptor, Mover.Default.INSTANCE)); + } + + @Override + public JComponent getPreferredFocusedComponent() { + return targetClassChooser.getChildComponent(); + } +}