Move: Implement "Move nested class to another class" UI

#KT-9027 Fixed
This commit is contained in:
Alexey Sedunov
2016-01-20 16:20:10 +03:00
parent e943508d87
commit 64f6c62d4f
4 changed files with 523 additions and 10 deletions
@@ -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<KtClassOrObject>(),
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<out PsiElement>, 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<out PsiElement>): 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<out PsiElement>, targetContainer: PsiElement?, callback: MoveCallback?) {
@@ -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()
}
}
}
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesDialog">
<grid id="1a398" binding="mainPanel" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="10" y="10" width="472" height="414"/>
</constraints>
<properties/>
<border type="none">
<color color="-8355712"/>
</border>
<children>
<grid id="64650" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="5" left="5" bottom="5" right="5"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="c6d13" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="50ecd"/>
<text value="Move members from:"/>
</properties>
</component>
<component id="50ecd" class="javax.swing.JTextField" binding="originalClassField">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<editable value="false"/>
</properties>
</component>
</children>
</grid>
<vspacer id="e0fb3">
<constraints>
<grid row="2" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<grid id="e4ac9" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="393db" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="To (fully qualified name):"/>
</properties>
</component>
<grid id="1f1bc" binding="targetClassChooserPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</children>
</grid>
<grid id="77b3c" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="3" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="fd527" class="javax.swing.JCheckBox" binding="openInEditorCheckBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="true"/>
<text value="Open moved members in editor"/>
</properties>
</component>
<hspacer id="cdd1d">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
</children>
</grid>
<grid id="a6a2e" binding="membersInfoPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</children>
</grid>
<buttonGroups>
<group name="buttonGroup1">
<member id="765d3"/>
<member id="58e61"/>
</group>
</buttonGroups>
</form>
@@ -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<KtNamedDeclaration, KotlinMemberInfo> {
}
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<KtClassOrObject> 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<KtClassOrObject> elementsToMove) {
List<KotlinMemberInfo> memberInfos = CollectionsKt.mapNotNull(
originalClass.getDeclarations(),
new Function1<KtDeclaration, KotlinMemberInfo>() {
@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<KtNamedDeclaration, KotlinMemberInfo>(memberInfos));
selectionPanel.getTable().setMemberInfoModel(memberInfoModel);
selectionPanel.getTable().addMemberInfoChangeListener(memberInfoModel);
membersInfoPanel.add(selectionPanel, BorderLayout.CENTER);
}
private List<KtClassOrObject> getSelectedElementsToMove() {
return CollectionsKt.map(
memberTable.getSelectedMemberInfos(),
new Function1<KotlinMemberInfo, KtClassOrObject>() {
@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<KtClassOrObject> 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();
}
}