feat: use Kotlin Symbol Processor to generate IViewAdapterIndex implementation

This commit is contained in:
Akash Yadav
2023-01-01 08:33:24 +05:30
parent 2bdf2e33f7
commit 34e09dc0d4
19 changed files with 329 additions and 321 deletions
+7 -1
View File
@@ -5,15 +5,18 @@
<profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.google.auto.value/auto-value/1.10/e97ada61b651d7dff21931c2cc1b158e870cb417/auto-value-1.10.jar" />
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.google.auto.value/auto-value/1.10.1/e0184c26c3a20257c6fb46eaea16fdd3d7b6d357/auto-value-1.10.1.jar" />
</processorPath>
<module name="AndroidIDE.subprojects.google-java-format.main" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel target="1.8">
<module name="AndroidIDE.annotation-processors" target="11" />
<module name="AndroidIDE.annotation-processors-java" target="11" />
<module name="AndroidIDE.annotation-processors-java.main" target="11" />
<module name="AndroidIDE.annotation-processors-java.test" target="11" />
<module name="AndroidIDE.annotation-processors.main" target="11" />
<module name="AndroidIDE.annotation-processors.test" target="11" />
<module name="AndroidIDE.build-tools.aaptcompiler" target="11" />
<module name="AndroidIDE.build-tools.aaptcompiler.test" target="11" />
<module name="AndroidIDE.build-tools.builder-api" target="11" />
@@ -92,6 +95,9 @@
<module name="AndroidIDE.gradle-tooling" target="1.8" />
<module name="AndroidIDE.gradle-tooling.plugin" target="1.8" />
<module name="AndroidIDE.javac" target="1.8" />
<module name="AndroidIDE.ksp-processor" target="11" />
<module name="AndroidIDE.ksp-processor.main" target="11" />
<module name="AndroidIDE.ksp-processor.test" target="11" />
<module name="AndroidIDE.sdk-info" target="11" />
<module name="AndroidIDE.treesitter.test" target="1.7" />
</bytecodeTargetLevel>
@@ -15,21 +15,23 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
@Suppress("JavaPluginLanguageLevel")
plugins {
id("java-library")
id("kotlin")
kotlin("jvm")
}
group = "com.itsaky.androidide.annotations"
version = Versions.versionName
dependencies {
implementation(libs.common.jkotlin)
implementation(libs.common.javapoet)
implementation(libs.google.auto.service)
implementation(kotlin("stdlib"))
implementation(projects.annotations)
implementation(projects.shared)
kapt(libs.google.auto.service)
kapt(projects.subprojects.frameworkStubs)
implementation(libs.common.javapoet)
implementation(libs.common.ksp)
}
sourceSets.main {
java.srcDirs("src/main/kotlin")
}
@@ -0,0 +1,35 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.annotations.ksp
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.itsaky.androidide.annotations.ksp.inflater.ViewAdapterSymbolProcessor
/**
* AndroidIDE [SymbolProcessorProvider] implementation.
*
* @author Akash Yadav
*/
class IDESymbolProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return ViewAdapterSymbolProcessor(environment.codeGenerator, environment.logger)
}
}
@@ -15,8 +15,16 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.annotations.processor
package com.itsaky.androidide.annotations.ksp.inflater
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.itsaky.androidide.annotations.inflater.ViewAdapter
import com.itsaky.androidide.annotations.uidesigner.IncludeInDesigner
import com.squareup.javapoet.ClassName
@@ -26,37 +34,34 @@ import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import com.squareup.javapoet.WildcardTypeName
import java.io.File
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.util.Collections
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier.FINAL
import javax.lang.model.element.Modifier.PRIVATE
import javax.lang.model.element.Modifier.PUBLIC
import javax.lang.model.element.Modifier.STATIC
import javax.lang.model.element.TypeElement
import javax.lang.model.type.MirroredTypeException
import javax.tools.Diagnostic.Kind.ERROR
/**
* Generates the ViewAdapterIndex class.
*
* @author Akash Yadav
*/
class ViewAdapterIndexGenerator {
class ViewAdapterIndexGenerator(private val logger: KSPLogger) {
private val indexClassBuilder =
TypeSpec.classBuilder(INDEX_CLASS_NAME).addModifiers(PUBLIC, FINAL).addSuperinterface(viewAdapterInterface)
TypeSpec.classBuilder(INDEX_CLASS_NAME)
.addModifiers(PUBLIC, FINAL)
.addSuperinterface(viewAdapterInterface)
private val indexAddStatements = CodeBlock.builder()
fun init(env: ProcessingEnvironment) {
fun init() {
val adapterType =
ParameterizedTypeName.get(
ClassName.get(ADAPTER_BASE_CLASS_PCK, ADAPTER_BASE_CLASS_NAME),
WildcardTypeName.subtypeOf(ClassName.get(env.viewElement()))
WildcardTypeName.subtypeOf(viewClass)
)
val mapOfStringToViewAdapter =
ParameterizedTypeName.get(
@@ -77,7 +82,7 @@ class ViewAdapterIndexGenerator {
)
)
addPrivateConstructor(indexClassBuilder)
// addPrivateConstructor(indexClassBuilder)
val indexImpl = ClassName.get(INDEX_PACKAGE_NAME, INDEX_CLASS_NAME)
indexClassBuilder.addField(
@@ -95,7 +100,7 @@ class ViewAdapterIndexGenerator {
.returns(adapterType)
.addStatement("var adapter = \$L.get(\$L)", INDEX_MAP_FIELD, "view")
.beginControlFlow("if (adapter == null)")
.addStatement("adapter = \$L.get(\$S)", INDEX_MAP_FIELD, env.viewElement().qualifiedName)
.addStatement("adapter = \$L.get(\$S)", INDEX_MAP_FIELD, VIEW_CLASS)
.endControlFlow()
.addStatement("return adapter")
.build()
@@ -157,42 +162,54 @@ class ViewAdapterIndexGenerator {
)
}
private val listOfString: TypeName =
ParameterizedTypeName.get(java.util.ArrayList::class.java, String::class.java)
fun addViewAdapter(processingEnv: ProcessingEnvironment, element: TypeElement) {
@OptIn(KspExperimental::class)
fun addViewAdapter(sym: KSClassDeclaration): Boolean {
val block = CodeBlock.builder()
val viewAdapter =
element.getAnnotation(ViewAdapter::class.java) ?: throw IllegalStateException()
val includeInDesigner = element.getAnnotation(IncludeInDesigner::class.java)
sym.getKSAnnotationsByType(ViewAdapter::class).iterator().run {
val result = next()
if (hasNext()) {
logger.error("${ViewAdapter::class.simpleName} can only be applied once.", sym)
return false
}
result
}
val viewName = getViewName(processingEnv, viewAdapter)
val superclasses = getViewSuperclasses(processingEnv, viewAdapter)
val includeInDesigner =
if (sym.isAnnotationPresent(IncludeInDesigner::class)) {
sym.getAnnotationsByType(IncludeInDesigner::class).iterator().run {
val result = next()
if (hasNext()) {
logger.error("${IncludeInDesigner::class.simpleName} can only be applied once.", sym)
return false
}
result
}
} else null
block.addStatement("final var superclasses = new \$T()", listOfString)
for (superclass in superclasses) {
block.addStatement("superclasses.add(\$S)", superclass)
}
val viewName = getViewName(viewAdapter)
val moduleNs = getModuleNs(viewAdapter)
val adapterType = ClassName.get(element)
val paramType = getViewTypeName(processingEnv, viewAdapter)
val adapterTypeName = ClassName.get(sym.packageName.asString(), sym.simpleName.asString())
val viewTypeName =
ClassName.get(viewName.substringBeforeLast('.'), viewName.substringAfterLast('.'))
block.add("\n")
block.addStatement(
"final var adapter = new \$T()",
ParameterizedTypeName.get(adapterType, paramType)
ParameterizedTypeName.get(adapterTypeName, viewTypeName)
)
block.addStatement("adapter.\$L(superclasses)", METHOD_SET_SUPERCLASS_HIERARCHY)
block.addStatement("adapter.\$L(\$S)", METHOD_SET_MODULE, viewAdapter.moduleNamespace)
block.addStatement("adapter.\$L(\$S)", METHOD_SET_MODULE, moduleNs)
block.addStatement("adapters.put(\$S, adapter)", viewName)
if (includeInDesigner != null) {
assertOverridesCreateWidget(processingEnv, element)
assertOverridesCreateWidget(sym)
block.addStatement(
"providers.computeIfAbsent(\$T.\$L, g -> new ArrayList<>()).add(adapter)",
"providers.computeIfAbsent(\$T.\$L, g -> new \$T<>()).add(adapter)",
ClassName.get(IncludeInDesigner.Group::class.java),
includeInDesigner.group
includeInDesigner.group,
ClassName.get(java.util.ArrayList::class.java)
)
}
@@ -201,19 +218,11 @@ class ViewAdapterIndexGenerator {
indexAddStatements.add(block.build())
indexAddStatements.unindent()
indexAddStatements.add("}\n")
return true
}
private fun assertOverridesCreateWidget(env: ProcessingEnvironment, element: TypeElement) {
element.enclosedElements.filterIsInstance<ExecutableElement>().find {
ADAPTER_FUNC_CREATE_WIDGET == it.simpleName.toString()
}
?: env.messager.printMessage(
ERROR,
"${element.qualifiedName} annotated with ${IncludeInDesigner::class.java.simpleName} must override $ADAPTER_FUNC_CREATE_WIDGET"
)
}
fun generate(outDir: String) {
fun generate(out: OutputStream) {
val javaCollections = ClassName.get(Collections::class.java)
indexAddStatements.addStatement(
@@ -229,61 +238,29 @@ class ViewAdapterIndexGenerator {
)
indexClassBuilder.addStaticBlock(indexAddStatements.build())
val file = JavaFile.builder(INDEX_PACKAGE_NAME, indexClassBuilder.build())
val generatedDir = File(outDir)
file.build().writeTo(generatedDir)
}
private fun addPrivateConstructor(builder: TypeSpec.Builder) {
builder.addMethod(MethodSpec.constructorBuilder().addModifiers(PRIVATE).build())
}
private fun getViewTypeName(
processingEnv: ProcessingEnvironment,
annotation: ViewAdapter
): TypeName {
return try {
ClassName.get(annotation.forView.java)
?: throw IllegalStateException("Cannot find type of $annotation")
} catch (err: MirroredTypeException) {
(processingEnv.typeUtils.asElement(err.typeMirror) as TypeElement).let { ClassName.get(it) }
OutputStreamWriter(out).use {
val file = JavaFile.builder(INDEX_PACKAGE_NAME, indexClassBuilder.build())
file.build().writeTo(it)
}
}
private fun getViewName(processingEnv: ProcessingEnvironment, annotation: ViewAdapter): String {
return try {
annotation.forView.qualifiedName
?: throw IllegalStateException("Cannot find type of $annotation")
} catch (err: MirroredTypeException) {
(processingEnv.typeUtils.asElement(err.typeMirror) as TypeElement).qualifiedName.toString()
}
private fun assertOverridesCreateWidget(sym: KSClassDeclaration): Boolean {
return sym.declarations
.filter { it is KSFunctionDeclaration }
.find { ADAPTER_FUNC_CREATE_WIDGET == it.simpleName.asString() } != null
}
private fun getViewSuperclasses(
processingEnv: ProcessingEnvironment,
annotation: ViewAdapter
): List<String> {
val result = mutableListOf<String>()
try {
val view = annotation.forView
var superclass = view.java.superclass
while (superclass != null) {
result.add(superclass.name)
superclass = superclass.superclass
}
} catch (err: MirroredTypeException) {
val typeElement = processingEnv.typeUtils.asElement(err.typeMirror) as TypeElement
result.add(typeElement.qualifiedName.toString())
var superclass = typeElement.superclass
while (superclass != null) {
val superType = processingEnv.typeUtils.asElement(superclass) as? TypeElement?
superType ?: break
result.add(superType.qualifiedName.toString())
superclass = superType.superclass
}
}
return result
private fun getViewName(viewAdapter: KSAnnotation): String {
return getViewType(viewAdapter).declaration.qualifiedName!!.asString()
}
private fun getViewType(viewAdapter: KSAnnotation) =
(getViewAdapterArg(viewAdapter, "forView") as KSType)
private fun getModuleNs(viewAdapter: KSAnnotation) =
(getViewAdapterArg(viewAdapter, "moduleNamespace") as String)
private fun getViewAdapterArg(viewAdapter: KSAnnotation, arg: String) =
viewAdapter.arguments.first { it.name!!.asString() == arg }.value
}
@@ -0,0 +1,111 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.annotations.ksp.inflater
import com.google.devtools.ksp.containingFile
import com.google.devtools.ksp.isOpen
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.itsaky.androidide.annotations.inflater.ViewAdapter
/**
* [SymbolProcessor] for [ViewAdapter] annotations.
*
* @author Akash Yadav
*/
class ViewAdapterSymbolProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
private val generator = ViewAdapterIndexGenerator(logger)
init {
generator.init()
}
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation(ViewAdapter::class.java.name)
if (!symbols.iterator().hasNext()) {
return emptyList()
}
val unprocessed = mutableListOf<KSAnnotated>()
val files = mutableListOf<KSFile>()
symbols.forEach {
it.containingFile?.also { file -> files.add(file) }
if (!process(it)) {
unprocessed.add(it)
}
}
val file =
codeGenerator.createNewFile(
Dependencies(true, *files.toTypedArray()),
INDEX_PACKAGE_NAME,
INDEX_CLASS_NAME,
"java"
)
generator.generate(file)
file.flush()
file.close()
return unprocessed
}
private fun process(sym: KSAnnotated): Boolean {
if (sym !is KSClassDeclaration) {
logger.error("${ViewAdapter::class} must be applied to a class.", sym)
return false
}
if (sym.parent !is KSFile) {
logger.error(
"${ViewAdapter::class} must be applied to a top level class. (${sym.parent})",
sym
)
return false
}
if (sym.primaryConstructor?.parameters?.size != 0) {
logger.error("A view adapter must have a single primary constructor", sym)
return false
}
if (sym.typeParameters.size != 1) {
logger.error("A view adapter must have exactly one type parameter", sym)
return false
}
if (!sym.isOpen()) {
logger.error("A view adapter implementation must be open.", sym)
return false
}
return generator.addViewAdapter(sym)
}
}
@@ -0,0 +1,29 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.annotations.ksp.inflater
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import kotlin.reflect.KClass
fun <T : Annotation> KSAnnotated.getKSAnnotationsByType(annotationKClass: KClass<T>): Sequence<KSAnnotation> {
return this.annotations.filter {
it.shortName.getShortName() == annotationKClass.simpleName && it.annotationType.resolve().declaration
.qualifiedName?.asString() == annotationKClass.qualifiedName
}
}
@@ -15,16 +15,13 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.annotations.processor
package com.itsaky.androidide.annotations.ksp.inflater
import com.squareup.javapoet.ClassName
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.TypeElement
import javax.lang.model.type.TypeMirror
const val KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated"
const val VIEW_CLASS = "android.view.View"
val ANDROID_VIEW_PCK = "android.view"
val ANDROID_VIEW_CLASS = "View"
val VIEW_CLASS = "$ANDROID_VIEW_PCK.$ANDROID_VIEW_CLASS"
const val ADAPTER_BASE_CLASS_PCK = "com.itsaky.androidide.inflater"
const val ADAPTER_BASE_CLASS_NAME = "IViewAdapter"
@@ -39,21 +36,7 @@ const val INDEX_PROVIDER_MAP_FIELD = "widgetProviders"
const val METHOD_SET_SUPERCLASS_HIERARCHY = "setSuperclassHierarchy"
const val METHOD_SET_MODULE = "setModuleNamespace"
val viewAdapter = ClassName.get(ADAPTER_BASE_CLASS_PCK, ADAPTER_BASE_CLASS_NAME)
val viewAdapterInterface: ClassName =
ClassName.get("com.itsaky.androidide.inflater", "IViewAdapterIndex")
private var adapterElement: TypeElement? = null
private var viewElement: TypeElement? = null
fun ProcessingEnvironment.adapterElement(): TypeElement {
return adapterElement
?: elementUtils.getTypeElement(ADAPTER_BASE_CLASS)!!.also { adapterElement = it }
}
fun ProcessingEnvironment.viewElement(): TypeElement {
return viewElement ?: elementUtils.getTypeElement(VIEW_CLASS)!!.also { viewElement = it }
}
fun ProcessingEnvironment.erasure(typeElement: TypeElement): TypeMirror? {
return typeUtils.erasure(typeElement.asType())
}
val viewClass = ClassName.get(ANDROID_VIEW_PCK, ANDROID_VIEW_CLASS)
@@ -0,0 +1,18 @@
#
# This file is part of AndroidIDE.
#
# AndroidIDE is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# AndroidIDE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
#
com.itsaky.androidide.annotations.ksp.IDESymbolProcessorProvider
@@ -1,145 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.annotations.processor
import com.google.auto.service.AutoService
import com.itsaky.androidide.annotations.inflater.ViewAdapter
import com.itsaky.androidide.annotations.uidesigner.IncludeInDesigner
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind.CLASS
import javax.lang.model.element.ElementKind.PACKAGE
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier.FINAL
import javax.lang.model.element.TypeElement
import javax.tools.Diagnostic.Kind.ERROR
@Suppress("unused")
@AutoService(Processor::class)
class ViewAdapterAnnotationProcessor : AbstractProcessor() {
private val adapterElement by lazy { processingEnv.erasure(processingEnv.adapterElement()) }
private val viewElement by lazy { processingEnv.erasure(processingEnv.viewElement()) }
private val generator = ViewAdapterIndexGenerator()
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(ViewAdapter::class.java.name, IncludeInDesigner::class.java.name)
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment
): Boolean {
if (roundEnv.processingOver()) {
return true
}
generator.init(processingEnv)
roundEnv.getElementsAnnotatedWith(ViewAdapter::class.java).forEach {
if (it.kind != CLASS) {
processingEnv.messager.printMessage(
ERROR,
"@ViewAdapter can only be used with classes. Used on $it"
)
return true
}
process(it)
}
generator.generate(
processingEnv.options[KAPT_KOTLIN_GENERATED]
?: throw IllegalStateException("Cannot find kapt output directory path")
)
return false
}
private fun process(element: Element) {
if (element.enclosingElement?.kind != PACKAGE || element !is TypeElement) {
processingEnv.messager.printMessage(
ERROR,
"@ViewAdapter can only be used on top-level classes. Used on $element"
)
return
}
val constructors =
processingEnv.elementUtils.getAllMembers(element).filter {
"<init>".contentEquals(it.simpleName)
}
if (constructors.size != 1) {
processingEnv.messager.printMessage(
ERROR,
"A view adapter must have only default constructor"
)
return
}
if ((constructors.first() as ExecutableElement).parameters.isNotEmpty()) {
processingEnv.messager.printMessage(
ERROR,
"View adapter constructor should not have any parameters"
)
return
}
if (
!processingEnv.typeUtils.isAssignable(
processingEnv.typeUtils.erasure(element.asType()),
adapterElement
)
) {
processingEnv.messager.printMessage(
ERROR,
"Class must be a subtype of $ADAPTER_BASE_CLASS $element $adapterElement"
)
return
}
if (element.typeParameters.size != 1) {
processingEnv.messager.printMessage(
ERROR,
"A view adapter must have exactly one type parameter"
)
}
val typeParam = element.typeParameters[0]
if (!processingEnv.typeUtils.isAssignable(typeParam.asType(), viewElement)) {
processingEnv.messager.printMessage(
ERROR,
"The type parameter of view adapter must be assignable from android.view.View"
)
}
if (element.modifiers.contains(FINAL)) {
processingEnv.messager.printMessage(ERROR, "${element.qualifiedName} cannot be final")
return
}
generator.addViewAdapter(processingEnv, element)
}
}
Binary file not shown.
+2
View File
@@ -6,6 +6,7 @@ editor = "0.20.4"
glide = "4.14.2"
androidx-vectordrawable = "1.1.0"
androidx-navigation = "2.5.3"
ksp = "1.8.0-1.0.8"
[libraries]
@@ -30,6 +31,7 @@ common-kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.re
common-jkotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
common-eventbus_ap = { module = "org.greenrobot:eventbus-annotation-processor", version = "3.3.1" }
common-javapoet = { module = "com.squareup:javapoet", version = "1.13.0" }
common-ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
# AndroidX
androidx-annotation = { module = "androidx.annotation:annotation", version = "1.5.0" }
+1 -1
View File
@@ -38,7 +38,7 @@ dependencyResolutionManagement {
rootProject.name = "AndroidIDE"
include(
":annotation-processors",
":annotation-ksp",
":annotations",
":actions",
":app",
+2 -2
View File
@@ -20,7 +20,7 @@ dependencies {
implementation(libs.common.kotlin)
implementation(libs.common.utilcode)
implementation(libs.google.material)
implementation(projects.actions)
implementation(projects.annotations)
implementation(projects.common)
@@ -31,7 +31,7 @@ dependencies {
implementation(projects.lsp.xml)
implementation(projects.resources)
implementation(projects.xmlInflater)
testImplementation(libs.tests.junit)
testImplementation(libs.tests.google.truth)
testImplementation(libs.tests.robolectric)
@@ -33,7 +33,6 @@ import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem.Location.UI_DESIGNER_TOOLBAR
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.app.BaseIDEActivity
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.actions.clearUiDesignerActions
import com.itsaky.androidide.uidesigner.actions.registerUiDesignerActions
import com.itsaky.androidide.uidesigner.databinding.ActivityUiDesignerBinding
@@ -53,7 +52,7 @@ class UIDesignerActivity : BaseIDEActivity() {
private var binding: ActivityUiDesignerBinding? = null
private val log = ILogger.newInstance("UIDesignerActivity")
private val viewModel by viewModels<WorkspaceViewModel>()
private val workspace: DesignerWorkspaceFragment?
get() = this.binding?.workspace?.getFragment<DesignerWorkspaceFragment>()
@@ -183,9 +182,7 @@ class UIDesignerActivity : BaseIDEActivity() {
}
fun setupHierarchy(view: com.itsaky.androidide.inflater.IView) {
binding?.hierarchy?.setupWithView(view) {
workspace()?.showViewInfo(it)
}
binding?.hierarchy?.setupWithView(view) { workspace()?.showViewInfo(it) }
}
fun openHierarchyView() {
@@ -46,8 +46,8 @@ internal object Widgets {
}
this.widgets =
IViewAdapterIndex.instance.getWidgetProviders(WIDGETS)
.flatMap { it.getUiWidgets() }
.sortedBy { it.name.simpleName() }
?.flatMap { it.getUiWidgets() }
?.sortedBy { it.name.simpleName() } ?: emptyList()
}
}
@@ -59,8 +59,8 @@ internal object Widgets {
this.widgets =
IViewAdapterIndex.instance.getWidgetProviders(LAYOUTS)
.flatMap { it.getUiWidgets() }
.sortedBy { it.name.simpleName() }
?.flatMap { it.getUiWidgets() }
?.sortedBy { it.name.simpleName() } ?: emptyList()
}
}
}
+19 -17
View File
@@ -1,32 +1,34 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("UnstableApiUsage")
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-parcelize")
id("com.google.devtools.ksp") version libs.versions.ksp
}
android { namespace = "com.itsaky.androidide.inflater" }
dependencies {
kapt(projects.annotationProcessors)
ksp(projects.annotationKsp)
implementation(libs.androidx.appcompat)
implementation(libs.common.kotlin)
@@ -30,15 +30,6 @@ import com.itsaky.androidide.inflater.models.UiWidget
*/
abstract class IViewAdapter<T : View> : AbstractParser() {
/** Superclasses of the view that this adpater handles. */
var superclassHierarchy: List<String> = emptyList()
set(value) {
if (field.isNotEmpty()) {
throw UnsupportedOperationException()
}
field = value
}
/**
* The package name or namespace of the module/artifact in which the view that this adapter
* handles is defined. The value is set to "android" by default unless explicitly specified in the
@@ -39,7 +39,7 @@ interface IViewAdapterIndex {
* @return The [IViewAdapter] for the given view name. If no adapter is found for the given view
* name, returns adapter for `android.view.View` if available or `null`.
*/
fun getAdapter(view: String): IViewAdapter<*>?
fun getAdapter(view: String): IViewAdapter<out View>?
/**
* Get the adapter for the given view.
@@ -47,15 +47,15 @@ interface IViewAdapterIndex {
* @param view The fully qualified name of the view.
* @return The view adapter instance or `null` if not found.
*/
fun getViewAdapter(view: String): IViewAdapter<*>
fun getViewAdapter(view: String): IViewAdapter<out View>?
/**
* Get the view adapters which provide widgets to the UI designer.
*
* @param group The group to get widget providers for.
* @return The widget providers for the [group].
*/
fun getWidgetProviders(group: IncludeInDesigner.Group) : List<IViewAdapter<*>>
fun getWidgetProviders(group: IncludeInDesigner.Group): List<IViewAdapter<out View>>?
}
/**
@@ -65,7 +65,7 @@ interface IViewAdapterIndex {
* @return The [IViewAdapter] for the given view name. If no adapter is found for the given view
* name, returns adapter for `android.view.View` if available or `null`.
*/
fun getAdapter(view: String): IViewAdapter<*>? {
fun getAdapter(view: String): IViewAdapter<out View>? {
return IViewAdapterIndex.instance.getAdapter(view)
}
@@ -75,7 +75,7 @@ fun getAdapter(view: String): IViewAdapter<*>? {
* @param view The fully qualified name of the view.
* @return The view adapter instance or `null` if not found.
*/
fun getViewAdapter(view: String): IViewAdapter<*> {
fun getViewAdapter(view: String): IViewAdapter<out View>? {
return IViewAdapterIndex.instance.getViewAdapter(view)
}