android UI xml bridge initial
- add frontend.android module - add xml ids to kotlin extension properties converter - add converter basic functional test
This commit is contained in:
committed by
Yan Zhulanow
parent
c8c1ce521b
commit
1e8769b5f7
Generated
+1
@@ -18,6 +18,7 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/core/descriptors/descriptors.iml" filepath="$PROJECT_DIR$/core/descriptors/descriptors.iml" group="core" />
|
<module fileurl="file://$PROJECT_DIR$/core/descriptors/descriptors.iml" filepath="$PROJECT_DIR$/core/descriptors/descriptors.iml" group="core" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/eval4j/eval4j.iml" filepath="$PROJECT_DIR$/eval4j/eval4j.iml" group="ide" />
|
<module fileurl="file://$PROJECT_DIR$/eval4j/eval4j.iml" filepath="$PROJECT_DIR$/eval4j/eval4j.iml" group="ide" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/compiler/frontend/frontend.iml" filepath="$PROJECT_DIR$/compiler/frontend/frontend.iml" group="compiler" />
|
<module fileurl="file://$PROJECT_DIR$/compiler/frontend/frontend.iml" filepath="$PROJECT_DIR$/compiler/frontend/frontend.iml" group="compiler" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/compiler/frontend.android/frontend.android.iml" filepath="$PROJECT_DIR$/compiler/frontend.android/frontend.android.iml" group="compiler" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/compiler/frontend.java/frontend.java.iml" filepath="$PROJECT_DIR$/compiler/frontend.java/frontend.java.iml" group="compiler/java" />
|
<module fileurl="file://$PROJECT_DIR$/compiler/frontend.java/frontend.java.iml" filepath="$PROJECT_DIR$/compiler/frontend.java/frontend.java.iml" group="compiler/java" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/generators/generators.iml" filepath="$PROJECT_DIR$/generators/generators.iml" group="infrastructure" />
|
<module fileurl="file://$PROJECT_DIR$/generators/generators.iml" filepath="$PROJECT_DIR$/generators/generators.iml" group="infrastructure" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/grammar/grammar.iml" filepath="$PROJECT_DIR$/grammar/grammar.iml" group="compiler" />
|
<module fileurl="file://$PROJECT_DIR$/grammar/grammar.iml" filepath="$PROJECT_DIR$/grammar/grammar.iml" group="compiler" />
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="kotlin-runtime" level="project" />
|
||||||
|
<orderEntry type="library" name="intellij-core" level="project" />
|
||||||
|
<orderEntry type="module" module-name="frontend" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package org.jetbrains.jet.lang.resolve.android
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
import org.jetbrains.jet.lang.psi.JetFile
|
||||||
|
import java.util.ArrayList
|
||||||
|
import javax.xml.parsers.SAXParserFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import org.jetbrains.jet.lang.psi.JetPsiFactory
|
||||||
|
|
||||||
|
class AndroidUIXmlParser(val project: Project?, val searchPaths: Collection<File>) {
|
||||||
|
|
||||||
|
val ids: MutableCollection<AndroidID> = ArrayList()
|
||||||
|
val kw = KotlinStringWriter()
|
||||||
|
val androidImports = arrayListOf("android.app.Activity",
|
||||||
|
"android.view.View")
|
||||||
|
|
||||||
|
public fun parse(): String {
|
||||||
|
doParse()
|
||||||
|
return produceKotlinSignatures().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun parseToPsi(): JetFile {
|
||||||
|
return JetPsiFactory.createFile(project, parse())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAndroidUIXml(file: File): Boolean {
|
||||||
|
return file.extension == "xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchForUIXml(paths: Collection<File>?): Collection<File> {
|
||||||
|
if (paths == null) return ArrayList(0)
|
||||||
|
val res = ArrayList<File>()
|
||||||
|
for (path in paths) {
|
||||||
|
if (path.isFile() && isAndroidUIXml(path)) {
|
||||||
|
res.add(path)
|
||||||
|
} else if (path.isDirectory()) {
|
||||||
|
res.addAll(searchForUIXml(path.listFiles()?.toArrayList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeImports() {
|
||||||
|
for (elem in androidImports)
|
||||||
|
kw.writeImport(elem)
|
||||||
|
kw.writeEmptyLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doParse() {
|
||||||
|
val xmlStreams = searchForUIXml(searchPaths).map { FileInputStream(it) }
|
||||||
|
val factory = SAXParserFactory.newInstance()
|
||||||
|
factory?.setNamespaceAware(true)
|
||||||
|
val parser = factory!!.newSAXParser() // TODO: annotate this
|
||||||
|
val handler = AndroidXmlHandler({ id -> ids.add(AndroidID(id)) })
|
||||||
|
writeImports()
|
||||||
|
for (xmlStream in xmlStreams) {
|
||||||
|
parser.parse(xmlStream, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun produceKotlinSignatures(): StringBuffer {
|
||||||
|
for (id in ids) {
|
||||||
|
val body = arrayListOf("return findViewById(R.id.${id.toString()})!!")
|
||||||
|
kw.writeImmutableExtensionProperty(receiver = "Activity",
|
||||||
|
name = id.toString(),
|
||||||
|
retType = "View",
|
||||||
|
getterBody = body )
|
||||||
|
}
|
||||||
|
return kw.output()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.jetbrains.jet.lang.resolve.android
|
||||||
|
|
||||||
|
trait AndroidResource
|
||||||
|
|
||||||
|
class AndroidID(val rawID: String): AndroidResource {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other is AndroidID && this.rawID == other.rawID
|
||||||
|
}
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return rawID.hashCode()
|
||||||
|
}
|
||||||
|
override fun toString(): String {
|
||||||
|
return rawID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package org.jetbrains.jet.lang.resolve.android
|
||||||
|
|
||||||
|
import org.xml.sax.helpers.DefaultHandler
|
||||||
|
import org.xml.sax.Attributes
|
||||||
|
import java.util.HashMap
|
||||||
|
|
||||||
|
class AndroidXmlHandler(val idCallback: (String)-> Unit): DefaultHandler() {
|
||||||
|
|
||||||
|
override fun startDocument() {
|
||||||
|
super<DefaultHandler>.startDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endDocument() {
|
||||||
|
super<DefaultHandler>.endDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
|
||||||
|
val hashMap = attributes.toMap()
|
||||||
|
val s = hashMap["id"]
|
||||||
|
val idPrefix = "@+id/"
|
||||||
|
if (s != null && s.startsWith(idPrefix)) idCallback(s.replace(idPrefix, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endElement(uri: String?, localName: String, qName: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun Attributes.toMap(): HashMap<String, String> {
|
||||||
|
val res = HashMap<String, String>()
|
||||||
|
for (index in 0..getLength()-1) {
|
||||||
|
val attrName = getLocalName(index)!!
|
||||||
|
val attrVal = getValue(index)!!
|
||||||
|
res[attrName] = attrVal
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package org.jetbrains.jet.lang.resolve.android
|
||||||
|
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
|
||||||
|
open class Context(val buffer: StringBuffer = StringBuffer(), var indentDepth: Int = 0) {
|
||||||
|
open class InvalidIndent(num: Int) : RuntimeException("Indentation level < 0: $num")
|
||||||
|
|
||||||
|
val indentUnit = " "
|
||||||
|
protected var currentIndent: String = indentUnit.repeat(indentDepth)
|
||||||
|
val children = ArrayList<Context>()
|
||||||
|
|
||||||
|
public fun incIndent() {
|
||||||
|
indentDepth++
|
||||||
|
currentIndent += indentUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun decIndent() {
|
||||||
|
indentDepth--
|
||||||
|
if (indentDepth < 0)
|
||||||
|
throw InvalidIndent(indentDepth)
|
||||||
|
currentIndent = currentIndent.substring(0, currentIndent.length - indentUnit.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
public open fun write(what: String) {
|
||||||
|
writeNoIndent(currentIndent)
|
||||||
|
writeNoIndent(what)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun writeNoIndent(what: String) {
|
||||||
|
buffer.append(what)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun writeln(what: String) {
|
||||||
|
write(what)
|
||||||
|
newLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun newLine() {
|
||||||
|
writeNoIndent("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public fun trim(num: Int) {
|
||||||
|
buffer.delete(buffer.length - num, buffer.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun fork(newBuffer: StringBuffer = StringBuffer(),
|
||||||
|
newIndentDepth: Int = indentDepth): Context {
|
||||||
|
val child = Context(newBuffer, newIndentDepth)
|
||||||
|
children.add(child)
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun adopt<T : Context>(c: T, inheritIndent: Boolean = true): T {
|
||||||
|
children.add(c)
|
||||||
|
if (inheritIndent) c.currentIndent = currentIndent
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun absorbChildren(noIndent: Boolean = true) {
|
||||||
|
for (child in children) {
|
||||||
|
child.absorbChildren()
|
||||||
|
if (noIndent)
|
||||||
|
writeNoIndent(child.toString())
|
||||||
|
else
|
||||||
|
write(child.toString())
|
||||||
|
}
|
||||||
|
children.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun toString(): String {
|
||||||
|
return buffer.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.jetbrains.jet.lang.resolve.android
|
||||||
|
|
||||||
|
trait KotlinWriter
|
||||||
|
|
||||||
|
class KotlinStringWriter : KotlinWriter {
|
||||||
|
|
||||||
|
val ctx = Context()
|
||||||
|
|
||||||
|
fun writeFunction(name: String,
|
||||||
|
args: Collection<String>?,
|
||||||
|
retType: String,
|
||||||
|
body: Collection<String>) {
|
||||||
|
val returnTerm = if (retType == "" || retType == "Unit") "" else ": $retType"
|
||||||
|
val argStr = if (args != null) args.join(", ") else ""
|
||||||
|
ctx.writeln("fun $name($argStr)$returnTerm {")
|
||||||
|
ctx.incIndent()
|
||||||
|
for (stmt in body)
|
||||||
|
ctx.writeln(stmt)
|
||||||
|
ctx.decIndent()
|
||||||
|
ctx.writeln("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeExtensionFunction(receiver: String,
|
||||||
|
name: String,
|
||||||
|
args: Collection<String>?,
|
||||||
|
retType: String,
|
||||||
|
body: Collection<String>) {
|
||||||
|
writeFunction("$receiver.$name", args, retType, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeImmutableProperty(name: String,
|
||||||
|
retType: String,
|
||||||
|
getterBody: Collection<String>) {
|
||||||
|
ctx.writeln("val $name: $retType")
|
||||||
|
ctx.incIndent()
|
||||||
|
ctx.write("get() ")
|
||||||
|
if (getterBody.size > 1) {
|
||||||
|
ctx.writeNoIndent("{\n")
|
||||||
|
ctx.incIndent()
|
||||||
|
for (stmt in getterBody) {
|
||||||
|
ctx.writeln(stmt)
|
||||||
|
}
|
||||||
|
ctx.decIndent()
|
||||||
|
ctx.writeln("}")
|
||||||
|
} else {
|
||||||
|
ctx.writeNoIndent("=")
|
||||||
|
ctx.writeNoIndent(getterBody.join("").replace("return", ""))
|
||||||
|
ctx.newLine()
|
||||||
|
}
|
||||||
|
ctx.decIndent()
|
||||||
|
ctx.newLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeImmutableExtensionProperty(receiver: String,
|
||||||
|
name: String,
|
||||||
|
retType: String,
|
||||||
|
getterBody: Collection<String>) {
|
||||||
|
writeImmutableProperty("$receiver.$name", retType, getterBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeImport(what: String) {
|
||||||
|
ctx.writeln("import $what")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeEmptyLine() {
|
||||||
|
ctx.newLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun output() = ctx.buffer
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import android.app.Activity
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
val Activity.item_detail_container: View
|
||||||
|
get() = findViewById(R.id.item_detail_container)!!
|
||||||
|
|
||||||
|
val Activity.textView1: View
|
||||||
|
get() = findViewById(R.id.textView1)!!
|
||||||
|
|
||||||
|
val Activity.password: View
|
||||||
|
get() = findViewById(R.id.password)!!
|
||||||
|
|
||||||
|
val Activity.textView2: View
|
||||||
|
get() = findViewById(R.id.textView2)!!
|
||||||
|
|
||||||
|
val Activity.passwordConfirmation: View
|
||||||
|
get() = findViewById(R.id.passwordConfirmation)!!
|
||||||
|
|
||||||
|
val Activity.login: View
|
||||||
|
get() = findViewById(R.id.login)!!
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/item_detail_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ItemDetailActivity"
|
||||||
|
tools:ignore="MergeRootFrame" >
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Enter your password" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textPassword" >
|
||||||
|
|
||||||
|
<requestFocus />
|
||||||
|
</EditText>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Repeat your password" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/passwordConfirmation"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/login"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Sign in" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -21,5 +21,6 @@
|
|||||||
<orderEntry type="module" module-name="js.tests" />
|
<orderEntry type="module" module-name="js.tests" />
|
||||||
<orderEntry type="module" module-name="preloader" />
|
<orderEntry type="module" module-name="preloader" />
|
||||||
<orderEntry type="module" module-name="util" />
|
<orderEntry type="module" module-name="util" />
|
||||||
|
<orderEntry type="module" module-name="frontend.android" scope="TEST" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package org.jetbrains.jet.codegen;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.jet.JetTestCaseBuilder;
|
||||||
|
import org.jetbrains.jet.lang.resolve.android.AndroidUIXmlParser;
|
||||||
|
import org.jetbrains.jet.test.TestCaseWithTmpdir;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
public class AndroidXmlTest extends TestCaseWithTmpdir {
|
||||||
|
@Override
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
super.setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static String getTestDataPath() {
|
||||||
|
return JetTestCaseBuilder.getTestDataPathBase() + "/android";
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"ResultOfMethodCallIgnored", "IOResourceOpenedButNotSafelyClosed"})
|
||||||
|
protected static String loadOrCreate(File file, String data) throws IOException {
|
||||||
|
try {
|
||||||
|
return new Scanner(file, "UTF-8" ).useDelimiter("\\A").next();
|
||||||
|
} catch (IOException e) {
|
||||||
|
file.createNewFile();
|
||||||
|
FileWriter fileWriter = new FileWriter(file);
|
||||||
|
fileWriter.write(data);
|
||||||
|
fileWriter.close();
|
||||||
|
fail("Empty expected data, creating from actual");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testConverterOneFile() throws Exception {
|
||||||
|
ArrayList<File> paths = new ArrayList<File>();
|
||||||
|
paths.add(new File(getTestDataPath() + "/layout.xml"));
|
||||||
|
AndroidUIXmlParser parser = new AndroidUIXmlParser(null, paths);
|
||||||
|
|
||||||
|
String actual = parser.parse();
|
||||||
|
String expected = loadOrCreate(new File(getTestDataPath() + "/layout.kt"), actual);
|
||||||
|
|
||||||
|
assertEquals(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user