From 3cec4c138f0bc8d82998affd5fcc9e04420dd7e4 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Mon, 1 Jul 2019 18:50:53 +0200 Subject: [PATCH] Form to UI DSL converter: generate property bindings, bunch of other fixes GitOrigin-RevId: 911ba4f93f72b404ca2cc3adc1f77c520ff0a0cc --- .../src/formConversion/ConvertFormDialog.kt | 33 ++ .../formConversion/ConvertFormToDslAction.kt | 290 ++++++++++++------ 2 files changed, 229 insertions(+), 94 deletions(-) create mode 100644 plugins/devkit/devkit-core/src/formConversion/ConvertFormDialog.kt diff --git a/plugins/devkit/devkit-core/src/formConversion/ConvertFormDialog.kt b/plugins/devkit/devkit-core/src/formConversion/ConvertFormDialog.kt new file mode 100644 index 000000000000..d8548fc893f0 --- /dev/null +++ b/plugins/devkit/devkit-core/src/formConversion/ConvertFormDialog.kt @@ -0,0 +1,33 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.formConversion + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.EditorTextFieldWithBrowseButton +import com.intellij.ui.layout.* +import javax.swing.JComponent + +/** + * @author yole + */ +class ConvertFormDialog(val project: Project, var className: String) : DialogWrapper(project) { + init { + init() + title = "Convert Form to UI DSL" + } + + var boundInstanceType: String = "" + + override fun createCenterPanel(): JComponent? { + return panel { + row("Target class name:") { + textField(::className, columns = 40).focused() + } + row("Bound instance type:") { + EditorTextFieldWithBrowseButton(project, true)() + .withBinding(EditorTextFieldWithBrowseButton::getText, EditorTextFieldWithBrowseButton::setText, + ::boundInstanceType.toBinding()) + } + } + } +} diff --git a/plugins/devkit/devkit-core/src/formConversion/ConvertFormToDslAction.kt b/plugins/devkit/devkit-core/src/formConversion/ConvertFormToDslAction.kt index 99e8925d9497..940fb35c962e 100644 --- a/plugins/devkit/devkit-core/src/formConversion/ConvertFormToDslAction.kt +++ b/plugins/devkit/devkit-core/src/formConversion/ConvertFormToDslAction.kt @@ -9,12 +9,12 @@ import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.fileTypes.FileTypeRegistry import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiFileFactory -import com.intellij.psi.PsiJavaFile +import com.intellij.psi.* import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.codeStyle.NameUtil +import com.intellij.psi.search.ProjectScope import com.intellij.psi.search.PsiShortNamesCache import com.intellij.psi.util.PsiTreeUtil import com.intellij.uiDesigner.PsiPropertiesProvider @@ -22,6 +22,11 @@ import com.intellij.uiDesigner.binding.FormClassIndex import com.intellij.uiDesigner.compiler.Utils.getRootContainer import com.intellij.uiDesigner.core.GridLayoutManager import com.intellij.uiDesigner.lw.* +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UastVisibility +import org.jetbrains.uast.toUElement +import java.util.* +import kotlin.collections.LinkedHashSet /** * @author yole @@ -43,29 +48,44 @@ class ConvertFormToDslAction : AnAction() { return } + val dialog = ConvertFormDialog(project, "${psiClass.name}Ui") + if (!dialog.showAndGet()) return + + val boundInstanceUClass = findBoundInstanceUClass(project, dialog.boundInstanceType) + val rootContainer = getRootContainer(formFile.text, PsiPropertiesProvider(module)) - val form = convertRootContainer(module, rootContainer) + val form = convertRootContainer(module, rootContainer, boundInstanceUClass) + + val imports = LinkedHashSet(form.imports) + boundInstanceUClass?.qualifiedName?.let { + imports.add(it) + } val formText = buildString { append("val panel = panel {\n") form.root.render(this) append("}\n") } - val uiName = "${psiClass.name}Ui" + val uiName = dialog.className val ktFileType = FileTypeRegistry.getInstance().getFileTypeByExtension("kt") val ktFileText = buildString { if (psiFile.packageName.isNotEmpty()) { append("package ${psiFile.packageName}\n\n") } append("import com.intellij.ui.layout.*\n") - for (usedImport in form.imports) { + for (usedImport in imports) { append("import $usedImport\n") } append("\n") - append("class $uiName {") + append("class $uiName") + if (boundInstanceUClass != null) { + append("(val model: ${dialog.boundInstanceType.substringAfterLast('.')})") + } + append(" {") - for (binding in form.bindings) { - append("lateinit var ${binding.name}: ${binding.type.substringAfterLast('.')}\n") + for (binding in form.componentProperties) { + val typeParameters = buildTypeParametersString(module, binding.type) + append("lateinit var ${binding.name}: ${binding.type.substringAfterLast('.')}$typeParameters\n") } append(formText) @@ -79,8 +99,16 @@ class ConvertFormToDslAction : AnAction() { } } - private fun convertRootContainer(module: Module, rootContainer: LwRootContainer): UiForm { - val call = convertContainer(rootContainer) + private fun findBoundInstanceUClass(project: Project, boundInstanceType: String): UClass? { + val psiClass = JavaPsiFacade.getInstance(project).findClass(boundInstanceType, ProjectScope.getAllScope(project)) + if (psiClass == null) return null + return psiClass.navigationElement.toUElement(UClass::class.java) + } + + private fun convertRootContainer(module: Module, + rootContainer: LwRootContainer, + boundInstanceUClass: UClass?): UiForm { + val call = FormToDslConverter(module, boundInstanceUClass).convertContainer(rootContainer) for (buttonGroup in rootContainer.buttonGroups) { call.checkConvertButtonGroup(buttonGroup.componentIds) } @@ -105,6 +133,23 @@ class FormCall( this.args.addAll(args.toList()) } + fun addArgIfPresent(arg: Array?): FormCall { + if (arg != null) { + Collections.addAll(args, *arg) + } + return this + } + + fun addArgOrDefault(arg: Array?, vararg default: String): FormCall { + if (arg != null) { + Collections.addAll(args, *arg) + } + else { + Collections.addAll(args, *default) + } + return this + } + fun render(builder: StringBuilder) { if (callee == "row" && args.isEmpty() && contents.all { it.callee == "row" }) { for (content in contents) { @@ -131,14 +176,14 @@ class FormCall( } } -data class Binding(val name: String, val type: String) +data class ComponentProperty(val name: String, val type: String) class UiForm(module: Module, val root: FormCall) { private val _imports = sortedSetOf() - private val _bindings = mutableListOf() + private val _componentProperties = mutableListOf() val imports: Collection get() { return _imports } - val bindings: Collection get() { return _bindings } + val componentProperties: Collection get() { return _componentProperties } init { collectUsedImportsAndBindings(module, root) @@ -160,7 +205,7 @@ class UiForm(module: Module, val root: FormCall) { } formCall.bindingType?.let { bindingType -> _imports.add(bindingType) - formCall.binding?.let { bindingName -> _bindings.add(Binding(bindingName, bindingType))} + formCall.binding?.let { bindingName -> _componentProperties.add(ComponentProperty(bindingName, bindingType))} } for (content in formCall.contents) { @@ -169,104 +214,161 @@ class UiForm(module: Module, val root: FormCall) { } } +internal class PropertyBinding(val type: PsiType?, val bindingCallParameters: Array) -private fun convertContainer(container: LwContainer): FormCall { - val row: FormCall +class FormToDslConverter(private val module: Module, private val boundInstanceUClass: UClass?) { + fun convertContainer(container: LwContainer): FormCall { + val row: FormCall - val borderTitle = container.borderTitle - if (borderTitle != null) { - row = FormCall("titledRow", origin = container) - row.args.add(convertStringDescriptor(borderTitle)) - } - else { - row = FormCall("row", origin = container) + val borderTitle = container.borderTitle + if (borderTitle != null) { + row = FormCall("titledRow", origin = container) + row.args.add(convertStringDescriptor(borderTitle)) + } + else { + row = FormCall("row", origin = container) + } + + val layoutManager = container.layout + if (layoutManager is GridLayoutManager) { + for (rowIndex in 0 until layoutManager.rowCount) { + row.appendGridRow(container, layoutManager, rowIndex) + } + } + else { + for (index in 0 until container.componentCount) { + row.contents.add(convertComponentOrContainer(container.getComponent(index))) + } + } + return row } - val layoutManager = container.layout - if (layoutManager is GridLayoutManager) { - for (rowIndex in 0 until layoutManager.rowCount) { - row.appendGridRow(container, layoutManager, rowIndex) + private fun FormCall.appendGridRow(container: LwContainer, layoutManager: GridLayoutManager, rowIndex: Int) { + val allComponents = container.collectComponentsInRow(rowIndex, layoutManager.columnCount) + val components = allComponents.filter { it !is LwHSpacer && it !is LwVSpacer } + if (components.isEmpty()) return + + val row = FormCall("row", origin = container) + contents.add(row) + if (components.first().componentClassName == "javax.swing.JLabel") { + row.args.add(convertComponentText(components.first())) + for (component in components.drop(1)) { + row.contents.add(convertComponentOrContainer(component)) + } + } + else { + for (component in components) { + row.contents.add(convertComponentOrContainer((component))) + } } } - else { - for (index in 0 until container.componentCount) { - row.contents.add(convertComponentOrContainer(container.getComponent(index))) + + private fun LwContainer.collectComponentsInRow(row: Int, columnCount: Int): List { + val result = arrayOfNulls(columnCount) + for (i in 0 until componentCount) { + val component = getComponent(i) + + val constraints = component.constraints + if (constraints.row == row) { + result[constraints.column] = component + } + } + return result.toList().filterNotNull() + } + + private fun convertComponentOrContainer(component: IComponent): FormCall { + if (component is LwContainer) { + return convertContainer(component) + } + else { + return convertComponent(component).also { it.origin = component } } } - return row -} -fun FormCall.appendGridRow(container: LwContainer, layoutManager: GridLayoutManager, rowIndex: Int) { - val allComponents = container.collectComponentsInRow(rowIndex, layoutManager.columnCount) - val components = allComponents.filter { it !is LwHSpacer && it !is LwVSpacer } - if (components.isEmpty()) return + private fun convertComponent(component: IComponent): FormCall { + val propertyBinding = convertBinding(component.binding) + return when (component.componentClassName) { + "javax.swing.JCheckBox", + "com.intellij.ui.components.JBCheckBox" -> + FormCall("checkBox", convertComponentText(component)) + .addArgIfPresent(propertyBinding?.bindingCallParameters) - val row = FormCall("row", origin = container) - contents.add(row) - if (components.first().componentClassName == "javax.swing.JLabel") { - row.args.add(convertComponentText(components.first())) - for (component in components.drop(1)) { - row.contents.add(convertComponentOrContainer(component)) + "javax.swing.JTextField" -> { + val methodName = if (propertyBinding?.type?.canonicalText == "int") "intTextField" else "textField" + FormCall(methodName) + .addArgOrDefault(propertyBinding?.bindingCallParameters, "{ \"\" }", "{}") + } + + "javax.swing.JRadioButton", + "com.intellij.ui.components.JBRadioButton"-> + FormCall("radioButton", convertComponentText(component)) + + "javax.swing.JButton" -> + FormCall("button", convertComponentText(component), "actionListener = { TODO() }") + + "javax.swing.JLabel", + "com.intellij.ui.components.JBLabel" -> + FormCall("label", convertComponentText(component)) + + else -> { + val typeParameters = buildTypeParametersString(module, component.componentClassName) + + val classShortName = component.componentClassName.substringAfterLast('.') + FormCall("$classShortName$typeParameters()", + binding = component.binding, + bindingType = component.componentClassName) + } } } - else { - for (component in components) { - row.contents.add(convertComponentOrContainer((component))) - } - } -} -private fun LwContainer.collectComponentsInRow(row: Int, columnCount: Int): List { - val result = arrayOfNulls(columnCount) - for (i in 0 until componentCount) { - val component = getComponent(i) - val constraints = component.constraints - if (constraints.row == row) { - result[constraints.column] = component + private fun convertBinding(binding: String?): PropertyBinding? { + if (binding == null || boundInstanceUClass == null) return null + val field = boundInstanceUClass.fields.find { it.matchesBinding(binding) } + if (field != null && !field.isStatic && field.visibility != UastVisibility.PRIVATE) { + return PropertyBinding(field.type, arrayOf("model::${field.name}")) } - } - return result.toList().filterNotNull() -} -private fun convertComponentOrContainer(component: IComponent): FormCall { - if (component is LwContainer) { - return convertContainer(component) + val getter = boundInstanceUClass.methods.find { + !it.name.startsWith("set") && it.matchesBinding(binding) + } + val setter = boundInstanceUClass.methods.find { + it.name.startsWith("set") && it.matchesBinding(binding) + } + if (getter != null && setter != null) { + return PropertyBinding(getter.returnType, arrayOf("model::${getter.name}", "model::${setter.name}")) + } + + return null } - else { - return convertComponent(component).also { it.origin = component } + + private fun PsiNamedElement.matchesBinding(binding: String): Boolean { + val bindingWords = NameUtil.nameToWordsLowerCase(binding.removePrefix("my")) + val elementWords = NameUtil.nameToWordsLowerCase(name?.removePrefix("my") ?: "") + if (bindingWords.size == 1 && elementWords.size == 1) { + return bindingWords[0] == elementWords[0] + } + return bindingWords.count { it in elementWords } > 1 + } + + private fun convertComponentText(component: IComponent) = + convertStringDescriptor(component.getPropertyValue("text") as StringDescriptor) + + private fun convertStringDescriptor(text: StringDescriptor): String { + text.value?.let { + return "\"${StringUtil.escapeQuotes(it)}\"" + } + return "${text.bundleName.substringAfterLast('/')}.message(\"${text.key}\")" } } -private fun convertComponent(component: IComponent): FormCall { - return when (component.componentClassName) { - "javax.swing.JCheckBox" -> - FormCall("checkBox", convertComponentText(component)) - - "javax.swing.JTextField" -> - FormCall("textField", "{ \"\" }", "{}") - - "javax.swing.JRadioButton" -> - FormCall("radioButton", convertComponentText(component)) - - "javax.swing.JButton" -> - FormCall("button", convertComponentText(component), "actionListener = {}") - - else -> { - FormCall("${component.componentClassName.substringAfterLast('.')}()", - binding = component.binding, - bindingType = component.componentClassName) - } +private fun buildTypeParametersString(module: Module, className: String): String { + val javaPsiFacade = JavaPsiFacade.getInstance(module.project) + val componentClass = javaPsiFacade.findClass(className, module.moduleWithDependenciesScope) + return if (componentClass != null && componentClass.typeParameters.isNotEmpty()) { + Array(componentClass.typeParameters.size) { "Any" }.joinToString(prefix = "<", postfix = ">", separator = ", ") } -} - -private fun convertComponentText(component: IComponent) = - convertStringDescriptor(component.getPropertyValue("text") as StringDescriptor) - -private fun convertStringDescriptor(text: StringDescriptor): String { - text.value?.let { - return "\"${StringUtil.escapeQuotes(it)}\"" - } - return "${text.bundleName.substringAfterLast('/')}.message(\"${text.key}\")" + else + "" } private fun IComponent.getPropertyValue(name: String): Any? {