mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: add ability to remove attributes from views
This commit is contained in:
@@ -19,32 +19,27 @@ package com.itsaky.androidide.actions
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.itsaky.androidide.utils.DialogUtils
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
fun ActionItem.requireContext(data: ActionData): Context {
|
||||
return data.get(Context::class.java)
|
||||
?: throw IllegalArgumentException("No context instance provided")
|
||||
}
|
||||
|
||||
fun ActionItem.requireFile(data: ActionData): File {
|
||||
fun requireFile(data: ActionData): File {
|
||||
return data.get(File::class.java) ?: throw IllegalArgumentException("No file instance provided")
|
||||
}
|
||||
|
||||
fun ActionItem.requirePath(data: ActionData): Path {
|
||||
fun requirePath(data: ActionData): Path {
|
||||
return requireFile(data).toPath()
|
||||
}
|
||||
|
||||
fun ActionItem.requireEditor(data: ActionData): CodeEditor {
|
||||
fun requireEditor(data: ActionData): CodeEditor {
|
||||
return data.get(CodeEditor::class.java)
|
||||
?: throw IllegalArgumentException("An editor instance is required but none was provided")
|
||||
}
|
||||
|
||||
fun ActionItem.newDialogBuilder(data: ActionData): MaterialAlertDialogBuilder {
|
||||
val klass = Class.forName("com.itsaky.androidide.utils.DialogUtils")
|
||||
val method = klass.getDeclaredMethod("newMaterialDialogBuilder", Context::class.java)
|
||||
return method.invoke(null, data.get(Context::class.java)!!) as MaterialAlertDialogBuilder
|
||||
fun newDialogBuilder(data: ActionData): MaterialAlertDialogBuilder {
|
||||
val context = data.get(Context::class.java)!!
|
||||
return DialogUtils.newMaterialDialogBuilder(context)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +49,7 @@ fun ActionItem.newDialogBuilder(data: ActionData): MaterialAlertDialogBuilder {
|
||||
* @param types The type of objects to look for.
|
||||
* @return `true` if the [data] has the given [types], `false` otherwise.
|
||||
*/
|
||||
fun ActionItem.hasRequiredData(data: ActionData, vararg types: Class<*>): Boolean {
|
||||
fun hasRequiredData(data: ActionData, vararg types: Class<*>): Boolean {
|
||||
for (type in types) {
|
||||
data.get(type) ?: return false
|
||||
}
|
||||
|
||||
+47
-49
@@ -14,51 +14,48 @@
|
||||
* 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.utils
|
||||
|
||||
package com.itsaky.androidide.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.itsaky.androidide.resources.R;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.itsaky.androidide.resources.R.string
|
||||
import com.itsaky.androidide.resources.R.style
|
||||
import org.jetbrains.annotations.Contract
|
||||
|
||||
/**
|
||||
* Utility class for creating dialogs.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class DialogUtils {
|
||||
|
||||
object DialogUtils {
|
||||
|
||||
/**
|
||||
* Create a new alert dialog with two buttons: <span>Yes</span> and <span>No</span>. This method
|
||||
* simply calls {@link #newYesNoDialog(Context, String, String, DialogInterface.OnClickListener,
|
||||
* DialogInterface.OnClickListener)} with default values for title and message.
|
||||
* simply calls [.newYesNoDialog] with default values for title and message.
|
||||
*
|
||||
* @param context The context for the dialog.
|
||||
* @param positiveClickListener A listener that will be invoked on the <span>Yes</span> button
|
||||
* click.
|
||||
* click.
|
||||
* @param negativeClickListener A listener that will be invoked on the <span>No</span> button
|
||||
* click.
|
||||
* click.
|
||||
* @return The newly created dialog.
|
||||
*/
|
||||
@NonNull
|
||||
public static MaterialAlertDialogBuilder newYesNoDialog(
|
||||
Context context,
|
||||
DialogInterface.OnClickListener positiveClickListener,
|
||||
DialogInterface.OnClickListener negativeClickListener) {
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun newYesNoDialog(
|
||||
context: Context,
|
||||
positiveClickListener: OnClickListener? = null,
|
||||
negativeClickListener: OnClickListener? = null
|
||||
): MaterialAlertDialogBuilder {
|
||||
return newYesNoDialog(
|
||||
context,
|
||||
context.getString(R.string.msg_yesno_def_title),
|
||||
context.getString(R.string.msg_yesno_def_message),
|
||||
positiveClickListener,
|
||||
negativeClickListener);
|
||||
context,
|
||||
context.getString(string.msg_yesno_def_title),
|
||||
context.getString(string.msg_yesno_def_message),
|
||||
positiveClickListener,
|
||||
negativeClickListener)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new alert dialog with two buttons: <span>Yes</span> and <span>No</span>.
|
||||
*
|
||||
@@ -66,36 +63,37 @@ public class DialogUtils {
|
||||
* @param title The title of the dialog.
|
||||
* @param message The message of the dialog.
|
||||
* @param positiveClickListener A listener that will be invoked on the <span>Yes</span> button
|
||||
* click.
|
||||
* click.
|
||||
* @param negativeClickListener A listener that will be invoked on the <span>No</span> button
|
||||
* click.
|
||||
* click.
|
||||
* @return The newly created dialog instance.
|
||||
*/
|
||||
@NonNull
|
||||
public static MaterialAlertDialogBuilder newYesNoDialog(
|
||||
Context context,
|
||||
String title,
|
||||
String message,
|
||||
DialogInterface.OnClickListener positiveClickListener,
|
||||
DialogInterface.OnClickListener negativeClickListener) {
|
||||
final var builder = DialogUtils.newMaterialDialogBuilder(context);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(message);
|
||||
builder.setPositiveButton(R.string.yes, positiveClickListener);
|
||||
builder.setNegativeButton(R.string.no, negativeClickListener);
|
||||
|
||||
return builder;
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun newYesNoDialog(
|
||||
context: Context,
|
||||
title: String,
|
||||
message: String? = null,
|
||||
positiveClickListener: OnClickListener? = null,
|
||||
negativeClickListener: OnClickListener? = null
|
||||
): MaterialAlertDialogBuilder {
|
||||
val builder = newMaterialDialogBuilder(context)
|
||||
builder.setTitle(title)
|
||||
builder.setMessage(message)
|
||||
builder.setPositiveButton(string.yes, positiveClickListener)
|
||||
builder.setNegativeButton(string.no, negativeClickListener)
|
||||
return builder
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new MaterialAlertDialogBuilder with the app's default style.
|
||||
*
|
||||
* @param context The context for the dialog builder.
|
||||
* @return The new MaterialAlertDialogBuilder instance.
|
||||
*/
|
||||
@NonNull
|
||||
@JvmStatic
|
||||
@Contract("_ -> new")
|
||||
public static MaterialAlertDialogBuilder newMaterialDialogBuilder(Context context) {
|
||||
return new MaterialAlertDialogBuilder(context, R.style.AppTheme_MaterialAlertDialog);
|
||||
fun newMaterialDialogBuilder(context: Context): MaterialAlertDialogBuilder {
|
||||
return MaterialAlertDialogBuilder(context, style.AppTheme_MaterialAlertDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import com.itsaky.androidide.lsp.java.JavaLanguageServer
|
||||
import com.itsaky.androidide.lsp.java.rewrite.Rewrite
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.utils.DocumentUtils
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,6 @@ import com.itsaky.androidide.actions.ActionData
|
||||
import com.itsaky.androidide.actions.requireEditor
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
|
||||
/** @author Akash Yadav */
|
||||
class CommentAction : BaseJavaCodeAction() {
|
||||
|
||||
-1
@@ -20,7 +20,6 @@ import com.itsaky.androidide.actions.ActionData
|
||||
import com.itsaky.androidide.actions.requireEditor
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
|
||||
/** @author Akash Yadav */
|
||||
class UncommentAction : BaseJavaCodeAction() {
|
||||
|
||||
Binary file not shown.
@@ -46,6 +46,7 @@ dependencies {
|
||||
|
||||
implementation project(path: ':common')
|
||||
implementation project(path: ':logger')
|
||||
implementation project(path: ':lookup')
|
||||
implementation project(path: ':lsp:api')
|
||||
implementation project(path: ':lsp:xml')
|
||||
implementation project(path: ':resources')
|
||||
|
||||
+56
-19
@@ -18,7 +18,9 @@
|
||||
package com.itsaky.androidide.uidesigner.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
@@ -26,9 +28,12 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IView.SingleAttributeChangeListener
|
||||
import com.itsaky.androidide.uidesigner.R
|
||||
import com.itsaky.androidide.uidesigner.adapters.ViewAttrListAdapter.VH
|
||||
import com.itsaky.androidide.uidesigner.databinding.LayoutViewattrItemBinding
|
||||
import com.itsaky.androidide.uidesigner.models.UiAttribute
|
||||
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
|
||||
import com.itsaky.androidide.utils.DialogUtils
|
||||
|
||||
/**
|
||||
* A [RecyclerView.Adapter] which shows the list of attributes of the selected view in the UI
|
||||
@@ -36,11 +41,15 @@ import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class ViewAttrListAdapter(attributes: List<IAttribute>, private val viewModel: WorkspaceViewModel?, private val onClick: (IAttribute) -> Unit) :
|
||||
RecyclerView.Adapter<VH>() {
|
||||
|
||||
private val attributes = attributes.sortedBy { it.name }
|
||||
|
||||
class ViewAttrListAdapter(
|
||||
attributes: List<IAttribute>,
|
||||
private val viewModel: WorkspaceViewModel?,
|
||||
private val onDeleteAttr: (IAttribute) -> Boolean,
|
||||
private val onClick: (IAttribute) -> Unit
|
||||
) : RecyclerView.Adapter<VH>() {
|
||||
|
||||
private val attributes = attributes.sortedBy { it.name }.toMutableList()
|
||||
|
||||
class VH(val binding: LayoutViewattrItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||
@@ -54,28 +63,56 @@ class ViewAttrListAdapter(attributes: List<IAttribute>, private val viewModel: W
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||
val binding = holder.binding
|
||||
val attr = this.attributes[position]
|
||||
|
||||
val attr = this.attributes[position] as UiAttribute
|
||||
|
||||
binding.attrName.text = "${attr.namespace.prefix}:${attr.name}"
|
||||
binding.attrValue.text = attr.value
|
||||
|
||||
|
||||
if (!attr.isRequired) {
|
||||
binding.deleteAttr.visibility = View.VISIBLE
|
||||
binding.deleteAttr.setOnClickListener {
|
||||
confirmDeleteAttr(binding.deleteAttr.context, attr, position)
|
||||
}
|
||||
} else binding.deleteAttr.visibility = View.INVISIBLE
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
onClick(attr)
|
||||
val viewModel = this.viewModel ?: return@setOnClickListener
|
||||
val attrUpdateListener = object : SingleAttributeChangeListener() {
|
||||
override fun onAttributeUpdated(view: IView, attribute: IAttribute, oldValue: String) {
|
||||
binding.attrValue.text = attribute.value
|
||||
}
|
||||
}
|
||||
val viewInfoScreenObserver = object : Observer<Int> {
|
||||
override fun onChanged(t: Int?) {
|
||||
if (t == WorkspaceViewModel.SCREEN_VIEW_INFO) {
|
||||
viewModel._viewInfoScreen.removeObserver(this)
|
||||
val attrUpdateListener =
|
||||
object : SingleAttributeChangeListener() {
|
||||
override fun onAttributeUpdated(view: IView, attribute: IAttribute, oldValue: String) {
|
||||
binding.attrValue.text = attribute.value
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel._viewInfoScreen.observe(binding.root.context as LifecycleOwner, viewInfoScreenObserver)
|
||||
val viewInfoScreenObserver =
|
||||
object : Observer<Int> {
|
||||
override fun onChanged(t: Int?) {
|
||||
if (t == WorkspaceViewModel.SCREEN_VIEW_INFO) {
|
||||
viewModel._viewInfoScreen.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel._viewInfoScreen.observe(
|
||||
binding.root.context as LifecycleOwner,
|
||||
viewInfoScreenObserver
|
||||
)
|
||||
viewModel.view?.registerAttributeChangeListener(attrUpdateListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmDeleteAttr(context: Context, attribute: UiAttribute, position: Int) {
|
||||
DialogUtils.newYesNoDialog(
|
||||
context = context,
|
||||
title = context.getString(R.string.title_confirm_delete),
|
||||
message = context.getString(R.string.msg_confirm_delete),
|
||||
positiveClickListener = { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
if (onDeleteAttr(attribute)) {
|
||||
this.attributes.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
}
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
+9
-16
@@ -27,15 +27,15 @@ import androidx.transition.ChangeBounds
|
||||
import androidx.transition.TransitionManager
|
||||
import com.blankj.utilcode.util.SizeUtils
|
||||
import com.itsaky.androidide.fragments.BaseFragment
|
||||
import com.itsaky.androidide.inflater.IInflateEventsListener
|
||||
import com.itsaky.androidide.inflater.IInflationEvent
|
||||
import com.itsaky.androidide.inflater.ILayoutInflater
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
import com.itsaky.androidide.inflater.IViewGroup.SingleOnHierarchyChangeListener
|
||||
import com.itsaky.androidide.inflater.InflationFinishEvent
|
||||
import com.itsaky.androidide.inflater.InflationStartEvent
|
||||
import com.itsaky.androidide.inflater.OnInflateViewEvent
|
||||
import com.itsaky.androidide.inflater.events.IInflateEventsListener
|
||||
import com.itsaky.androidide.inflater.events.IInflationEvent
|
||||
import com.itsaky.androidide.inflater.events.InflationFinishEvent
|
||||
import com.itsaky.androidide.inflater.events.InflationStartEvent
|
||||
import com.itsaky.androidide.inflater.events.OnInflateViewEvent
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.inflater.internal.ViewImpl
|
||||
import com.itsaky.androidide.inflater.viewGroup
|
||||
@@ -47,8 +47,8 @@ import com.itsaky.androidide.uidesigner.drag.WidgetTouchListener
|
||||
import com.itsaky.androidide.uidesigner.drawable.UiViewLayeredForeground
|
||||
import com.itsaky.androidide.uidesigner.fragments.ViewInfoFragment.Companion.TAG
|
||||
import com.itsaky.androidide.uidesigner.models.PlaceholderView
|
||||
import com.itsaky.androidide.uidesigner.models.UiView
|
||||
import com.itsaky.androidide.uidesigner.models.UiViewGroup
|
||||
import com.itsaky.androidide.uidesigner.utils.UiLayoutInflater
|
||||
import com.itsaky.androidide.uidesigner.utils.bgDesignerView
|
||||
import com.itsaky.androidide.uidesigner.utils.layeredForeground
|
||||
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
|
||||
@@ -70,6 +70,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
|
||||
|
||||
private var isInflating = false
|
||||
private val viewInfo by lazy { ViewInfoSheet() }
|
||||
private val inflater by lazy { UiLayoutInflater() }
|
||||
private val workspaceView by lazy {
|
||||
UiViewGroup(LayoutFile(File(""), ""), LinearLayout::class.qualifiedName!!, binding!!.workspace)
|
||||
}
|
||||
@@ -152,14 +153,6 @@ class DesignerWorkspaceFragment : BaseFragment() {
|
||||
placeholder.file = file
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterceptCreateView(view: IView): IView {
|
||||
view as ViewImpl
|
||||
if (view is IViewGroup) {
|
||||
return UiViewGroup(view.file, view.name, view.viewGroup)
|
||||
}
|
||||
return UiView(view.file, view.name, view.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,8 +170,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
|
||||
|
||||
viewModel._workspaceScreen.observe(viewLifecycleOwner) { binding?.flipper?.displayedChild = it }
|
||||
viewModel._errText.observe(viewLifecycleOwner) { binding?.errText?.text = it }
|
||||
|
||||
val inflater = ILayoutInflater.newInflater()
|
||||
|
||||
inflater.inflationEventListener = this.inflateListener
|
||||
var hasError = false
|
||||
val inflated =
|
||||
@@ -199,6 +191,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
this.binding = null
|
||||
this.inflater.close()
|
||||
}
|
||||
|
||||
private fun setupView(view: IView) {
|
||||
|
||||
+5
-5
@@ -24,9 +24,6 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IView.SingleAttributeChangeListener
|
||||
import com.itsaky.androidide.uidesigner.adapters.ViewAttrListAdapter
|
||||
import com.itsaky.androidide.uidesigner.databinding.LayoutViewInfoBinding
|
||||
import com.itsaky.androidide.uidesigner.databinding.LayoutViewInfoHeaderBinding
|
||||
@@ -52,7 +49,7 @@ class ViewInfoFragment : Fragment() {
|
||||
field = value
|
||||
header = LayoutViewInfoHeaderBinding.bind(value.root)
|
||||
}
|
||||
|
||||
|
||||
private var header: LayoutViewInfoHeaderBinding? = null
|
||||
|
||||
companion object {
|
||||
@@ -86,7 +83,10 @@ class ViewInfoFragment : Fragment() {
|
||||
header.name.text = view.simpleName
|
||||
header.desc.text = view.name
|
||||
binding.attrList.adapter =
|
||||
ViewAttrListAdapter(view.attributes, viewModel) {
|
||||
ViewAttrListAdapter(attributes = view.attributes, viewModel = viewModel, onDeleteAttr = {
|
||||
view.removeAttribute(it)
|
||||
true
|
||||
}) {
|
||||
viewModel.selectedAttr = it
|
||||
viewModel.viewInfoScreen = SCREEN_VALUE_EDITOR
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.uidesigner.models
|
||||
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.INamespace
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.inflater.internal.ViewAdapterIndex
|
||||
|
||||
/**
|
||||
* UI Designer specific implementation of [IAttribute].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
internal data class UiAttribute
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
override val namespace: INamespace = INamespace.ANDROID,
|
||||
override val name: String,
|
||||
override var value: String
|
||||
) : AttributeImpl(namespace, name, value) {
|
||||
|
||||
constructor(src: IAttribute) : this(namespace = src.namespace, name = src.name, value = src.value)
|
||||
|
||||
/**
|
||||
* Whether this attribute is required or not. Required attributes cannot be removed from a view
|
||||
* once applied.
|
||||
*/
|
||||
internal var isRequired = false
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun isRequired(view: IView, attribute: IAttribute): Boolean {
|
||||
val adapter = ViewAdapterIndex.getAdapter(view.name) ?: return false
|
||||
return adapter.isRequiredAttribute(attribute)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.uidesigner.utils
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.INamespace
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IComponentFactory
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.uidesigner.models.UiAttribute
|
||||
import com.itsaky.androidide.uidesigner.models.UiView
|
||||
import com.itsaky.androidide.uidesigner.models.UiViewGroup
|
||||
|
||||
/**
|
||||
* Creates layout inflater components for UI Designer.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
open class UiInflaterComponentFactory : IComponentFactory {
|
||||
|
||||
override fun createView(file: LayoutFile, name: String, view: View): IView {
|
||||
if (view is ViewGroup) {
|
||||
return UiViewGroup(file, name, view)
|
||||
}
|
||||
return UiView(file, name, view)
|
||||
}
|
||||
|
||||
override fun createAttr(
|
||||
view: IView,
|
||||
namespace: INamespace,
|
||||
name: String,
|
||||
value: String
|
||||
): IAttribute {
|
||||
return UiAttribute(namespace = namespace, name = name, value = value).apply {
|
||||
isRequired = UiAttribute.isRequired(view, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.uidesigner.utils
|
||||
|
||||
import com.itsaky.androidide.inflater.ILayoutInflater
|
||||
import com.itsaky.androidide.inflater.internal.LayoutInflaterImpl
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
|
||||
/**
|
||||
* Layout inflater implmentation for the UI designer.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class UiLayoutInflater : LayoutInflaterImpl() {
|
||||
|
||||
init {
|
||||
this.componentFactory = UiInflaterComponentFactory()
|
||||
Lookup.DEFAULT.update(ILayoutInflater.LOOKUP_KEY, this)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -65,7 +65,7 @@ internal class ValueCompletionProvider(
|
||||
this.start()
|
||||
}
|
||||
|
||||
val copy = (attribute as AttributeImpl).copy()
|
||||
val copy = (attribute as AttributeImpl).copyAttr(view = view)
|
||||
copy.value = value
|
||||
view.updateAttribute(copy)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.uidesigner.R.drawable
|
||||
import com.itsaky.androidide.uidesigner.R.string
|
||||
import com.itsaky.androidide.uidesigner.models.UiAttribute
|
||||
import com.itsaky.androidide.uidesigner.models.UiWidget
|
||||
import com.itsaky.androidide.uidesigner.models.UiWidgetCategory
|
||||
|
||||
@@ -167,7 +168,7 @@ internal object Widgets {
|
||||
) {
|
||||
override fun createView(context: Context, parent: ViewGroup, layoutFile: LayoutFile): IView {
|
||||
return super.createView(context, parent, layoutFile).apply {
|
||||
addAttribute(AttributeImpl(name = "orientation", value = "horizontal"))
|
||||
addAttribute(UiAttribute(name = "orientation", value = "horizontal"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ internal object Widgets {
|
||||
) {
|
||||
override fun createView(context: Context, parent: ViewGroup, layoutFile: LayoutFile): IView {
|
||||
return super.createView(context, parent, layoutFile).apply {
|
||||
addAttribute(AttributeImpl(name = "orientation", value = "vertical"))
|
||||
addAttribute(UiAttribute(name = "orientation", value = "vertical"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/delete_attr"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/delete_attr"
|
||||
tools:text="android:layout_width" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
@@ -46,9 +46,24 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="@id/delete_attr"
|
||||
app:layout_constraintEnd_toEndOf="@id/attr_name"
|
||||
app:layout_constraintStart_toStartOf="@id/attr_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/attr_name"
|
||||
tools:text="match_parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete_attr"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/title_viewaction_delete"
|
||||
app:icon="@drawable/ic_delete"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.inflater
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
|
||||
import com.itsaky.androidide.inflater.internal.ViewImpl
|
||||
|
||||
/** Default implementation of [IComponentFactory]. */
|
||||
open class DefaultComponentFactory : IComponentFactory {
|
||||
|
||||
override fun createView(file: LayoutFile, name: String, view: View): IView {
|
||||
return if (view is ViewGroup) {
|
||||
ViewGroupImpl(file, name, view)
|
||||
} else ViewImpl(file, name, view)
|
||||
}
|
||||
|
||||
override fun createAttr(
|
||||
view: IView,
|
||||
namespace: INamespace,
|
||||
name: String,
|
||||
value: String
|
||||
): IAttribute {
|
||||
return AttributeImpl(namespace = namespace, name = name, value = value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.inflater
|
||||
|
||||
import android.view.View
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.lookup.Lookup.Key
|
||||
|
||||
/** Creates instances of views and attributes. */
|
||||
interface IComponentFactory {
|
||||
|
||||
/**
|
||||
* Creates a new instance of [IView].
|
||||
*
|
||||
* @param file The layout file.
|
||||
* @param name The fully qualified name of the view being created.
|
||||
* @param view The Android view object.
|
||||
* @return The [IView] instance.
|
||||
*/
|
||||
fun createView(file: LayoutFile, name: String, view: View): IView
|
||||
|
||||
/**
|
||||
* Creates a new [IAttribute] instance.
|
||||
*
|
||||
* @param view The view to which the attribute will be applied.
|
||||
* @param namespace The namespace of the attribute.
|
||||
* @param name The name of the attribute.
|
||||
* @param value The value of the attribute.
|
||||
* @return The [IAttribute] instance.
|
||||
*/
|
||||
fun createAttr(view: IView, namespace: INamespace, name: String, value: String): IAttribute
|
||||
|
||||
companion object {
|
||||
@JvmField val LAYOUT_INFLATER_COMPONENT_FACTORY_KEY = Key<IComponentFactory>()
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,11 @@
|
||||
package com.itsaky.androidide.inflater
|
||||
|
||||
import android.view.ViewGroup
|
||||
import com.itsaky.androidide.inflater.events.IInflateEventsListener
|
||||
import com.itsaky.androidide.inflater.internal.LayoutInflaterImpl
|
||||
import com.itsaky.androidide.lookup.Lookup.Key
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -26,23 +30,61 @@ import java.io.File
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
abstract class ILayoutInflater {
|
||||
interface ILayoutInflater : Closeable {
|
||||
|
||||
/** The listener which listens to layout inflation events. */
|
||||
abstract var inflationEventListener: IInflateEventsListener?
|
||||
var inflationEventListener: IInflateEventsListener?
|
||||
|
||||
/** The [IComponentFactory] which is used to create components of the layout inflater. */
|
||||
var componentFactory: IComponentFactory
|
||||
|
||||
/** The [AndroidModule] which is used to resolve resource references. */
|
||||
var module: AndroidModule?
|
||||
|
||||
/**
|
||||
* Inflate the given raw XML file.
|
||||
*
|
||||
* **NOTE** : The [startParse][com.itsaky.androidide.inflater.utils.startParse] method is called
|
||||
* if it hasn't been called yet. However, the [endParse]
|
||||
* [com.itsaky.androidide.inflater.utils.endParse] method is not called after the parse is done.
|
||||
* The caller is expected to call [close] when this inflater instance is no longer needed. The
|
||||
* [close] method calls the [endParse] [com.itsaky.androidide.inflater.utils.endParse].
|
||||
*
|
||||
* @param file The file to inflate.
|
||||
* @param parent The parent view.
|
||||
*/
|
||||
abstract fun inflate(file: File, parent: ViewGroup): List<IView>
|
||||
fun inflate(file: File, parent: ViewGroup): List<IView>
|
||||
|
||||
fun closeSilently() {
|
||||
try {
|
||||
close()
|
||||
} catch (err: Throwable) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** The [Key] that can be used to lookup the layout inflater service. */
|
||||
@JvmField val LOOKUP_KEY = Key<ILayoutInflater>()
|
||||
|
||||
/**
|
||||
* Creates a new [ILayoutInflater] instance.
|
||||
*
|
||||
* @param module The [AndroidModule] that will be used to resolve resource references.
|
||||
* @param componentFactory The [IComponentFactory] that will be used to create [ILayoutInflater]
|
||||
* components.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newInflater(): ILayoutInflater {
|
||||
return LayoutInflaterImpl()
|
||||
@JvmOverloads
|
||||
fun newInflater(
|
||||
module: AndroidModule? = null,
|
||||
componentFactory: IComponentFactory = DefaultComponentFactory()
|
||||
): ILayoutInflater {
|
||||
return LayoutInflaterImpl().also {
|
||||
it.module = module
|
||||
it.componentFactory = componentFactory
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,13 @@ abstract class IViewAdapter : AbstractParser() {
|
||||
* @param view The view to apply basic attributes to.
|
||||
*/
|
||||
abstract fun applyBasic(view: IView)
|
||||
|
||||
/**
|
||||
* Called to check whether the given attribute required for the view. A required view cannot be
|
||||
* removed by the user.
|
||||
*
|
||||
* @param attribute The attribute to check.
|
||||
* @return `true` if the attribute is required, `false` otherwise.
|
||||
*/
|
||||
abstract fun isRequiredAttribute(attribute: IAttribute): Boolean
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.inflater.events
|
||||
|
||||
/**
|
||||
* A listener which listens for XML layout inflation events.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface IInflateEventsListener {
|
||||
|
||||
/**
|
||||
* Called when an event occurs. The [event] parameter contains more information about the event.
|
||||
*
|
||||
* @param event The event.
|
||||
*/
|
||||
fun onEvent(event: IInflationEvent<*>)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.inflater.events
|
||||
|
||||
/**
|
||||
* An inflation event.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
abstract class IInflationEvent<T>(val data: T)
|
||||
+3
-31
@@ -15,38 +15,10 @@
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.inflater
|
||||
package com.itsaky.androidide.inflater.events
|
||||
|
||||
/**
|
||||
* A listener which listens for XML layout inflation events.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface IInflateEventsListener {
|
||||
|
||||
/**
|
||||
* Called when an event occurs. The [event] parameter contains more information about the event.
|
||||
*
|
||||
* @param event The event.
|
||||
*/
|
||||
fun onEvent(event: IInflationEvent<*>)
|
||||
|
||||
/**
|
||||
* Allows the listener to intercept the view creation event and return view instance of their own.
|
||||
*
|
||||
* @param view The view that was created. The properties of this view is not expected to be
|
||||
* modified (see [OnInflateViewEvent] instead).
|
||||
* @return The view that should replace the [view] instance.
|
||||
*/
|
||||
fun onInterceptCreateView(view: IView): IView = view
|
||||
}
|
||||
|
||||
/**
|
||||
* An inflation event.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
abstract class IInflationEvent<T>(val data: T)
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
|
||||
/** Indicates start of layout inflation. */
|
||||
class InflationStartEvent : IInflationEvent<Unit>(Unit)
|
||||
+43
-2
@@ -20,9 +20,50 @@ package com.itsaky.androidide.inflater.internal
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.INamespace
|
||||
import com.itsaky.androidide.inflater.INamespace.Companion.ANDROID
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.newAttribute
|
||||
|
||||
data class AttributeImpl @JvmOverloads constructor(
|
||||
open class AttributeImpl
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
override val namespace: INamespace = ANDROID,
|
||||
override val name: String,
|
||||
override var value: String
|
||||
) : IAttribute
|
||||
) : IAttribute {
|
||||
|
||||
@JvmOverloads
|
||||
fun copyAttr(
|
||||
view: IView? = null,
|
||||
namespace: INamespace = this.namespace,
|
||||
name: String = this.name,
|
||||
value: String = this.value
|
||||
): IAttribute {
|
||||
return newAttribute(view = view, namespace = namespace, name = name, value = value)
|
||||
}
|
||||
|
||||
internal fun immutableCopy(): IAttribute {
|
||||
return ImmutableAttributeImpl(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is AttributeImpl) return false
|
||||
|
||||
if (namespace != other.namespace) return false
|
||||
if (name != other.name) return false
|
||||
if (value != other.value) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = namespace.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "AttributeImpl(namespace=$namespace, name='$name', value='$value')"
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.inflater.internal
|
||||
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
|
||||
/**
|
||||
* Immutable implementation of [IAttribute].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
internal class ImmutableAttributeImpl(private val src: AttributeImpl) : IAttribute by src {
|
||||
override var value: String
|
||||
get() = src.value
|
||||
set(value) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.inflater.internal
|
||||
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
import com.itsaky.androidide.inflater.IViewGroup.OnHierarchyChangeListener
|
||||
|
||||
/**
|
||||
* Immutable implementation of [IViewGroup].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class ImmutableViewGroupImpl(private val src: IViewGroup) : IViewGroup by src {
|
||||
|
||||
override fun addChild(view: IView) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun addChild(index: Int, view: IView) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun removeChild(view: IView) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun removeChild(index: Int) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun set(index: Int, view: IView): IView {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun addOnHierarchyChangeListener(listener: OnHierarchyChangeListener) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun removeOnHierarchyChangeListener(listener: OnHierarchyChangeListener) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.inflater.internal
|
||||
|
||||
import android.view.View
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IView.AttributeChangeListener
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
|
||||
/**
|
||||
* An immutable implementation of [IView].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
internal class ImmutableViewImpl(private val src: ViewImpl) : IView by src {
|
||||
|
||||
override fun addAttribute(attribute: IAttribute, update: Boolean) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun removeAttribute(attribute: IAttribute) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun updateAttribute(attribute: IAttribute) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun onHighlightStateUpdated(highlight: Boolean) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun registerAttributeChangeListener(listener: AttributeChangeListener) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
|
||||
override fun unregisterAttributeChangeListener(listener: AttributeChangeListener) {
|
||||
throw UnsupportedOperationException("Immutable!")
|
||||
}
|
||||
}
|
||||
+54
-21
@@ -33,25 +33,30 @@ import com.android.aaptcompiler.ResourceName
|
||||
import com.android.aaptcompiler.XmlProcessor
|
||||
import com.android.aaptcompiler.extractPathData
|
||||
import com.itsaky.androidide.aapt.logging.IDELogger
|
||||
import com.itsaky.androidide.inflater.IInflateEventsListener
|
||||
import com.itsaky.androidide.inflater.DefaultComponentFactory
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.IComponentFactory
|
||||
import com.itsaky.androidide.inflater.IComponentFactory.Companion.LAYOUT_INFLATER_COMPONENT_FACTORY_KEY
|
||||
import com.itsaky.androidide.inflater.ILayoutInflater
|
||||
import com.itsaky.androidide.inflater.INamespace
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
import com.itsaky.androidide.inflater.InflateException
|
||||
import com.itsaky.androidide.inflater.InflationFinishEvent
|
||||
import com.itsaky.androidide.inflater.InflationStartEvent
|
||||
import com.itsaky.androidide.inflater.OnApplyAttributeEvent
|
||||
import com.itsaky.androidide.inflater.OnInflateViewEvent
|
||||
import com.itsaky.androidide.inflater.events.IInflateEventsListener
|
||||
import com.itsaky.androidide.inflater.events.InflationFinishEvent
|
||||
import com.itsaky.androidide.inflater.events.InflationStartEvent
|
||||
import com.itsaky.androidide.inflater.events.OnApplyAttributeEvent
|
||||
import com.itsaky.androidide.inflater.events.OnInflateViewEvent
|
||||
import com.itsaky.androidide.inflater.internal.utils.IDTable
|
||||
import com.itsaky.androidide.inflater.internal.utils.ViewFactory.createViewInstance
|
||||
import com.itsaky.androidide.inflater.internal.utils.ViewFactory.generateLayoutParams
|
||||
import com.itsaky.androidide.inflater.internal.utils.endParse
|
||||
import com.itsaky.androidide.inflater.internal.utils.isParsing
|
||||
import com.itsaky.androidide.inflater.utils.isParsing
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseLayoutReference
|
||||
import com.itsaky.androidide.inflater.internal.utils.startParse
|
||||
import com.itsaky.androidide.inflater.utils.endParse
|
||||
import com.itsaky.androidide.inflater.utils.startParse
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.xml.widgets.Widget
|
||||
import com.itsaky.androidide.xml.widgets.WidgetTable
|
||||
import java.io.File
|
||||
@@ -61,13 +66,19 @@ import java.io.File
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
open class LayoutInflaterImpl : ILayoutInflater {
|
||||
|
||||
override var inflationEventListener: IInflateEventsListener? = null
|
||||
private val log = ILogger.newInstance("LayoutInflaterImpl")
|
||||
override var module: AndroidModule? = null
|
||||
private var _primaryInflatingFile: File? = null
|
||||
private var _currentLayoutFile: LayoutFile? = null
|
||||
|
||||
override var componentFactory: IComponentFactory = DefaultComponentFactory()
|
||||
set(value) {
|
||||
field = value
|
||||
Lookup.DEFAULT.update(LAYOUT_INFLATER_COMPONENT_FACTORY_KEY, value)
|
||||
}
|
||||
|
||||
protected val primaryInflatingFile: File
|
||||
get() = this._primaryInflatingFile!!
|
||||
|
||||
@@ -78,15 +89,27 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
this._primaryInflatingFile = file
|
||||
IDTable.newRound()
|
||||
if (!isParsing) {
|
||||
startParse(file)
|
||||
if (this.module == null) {
|
||||
startParse(file)
|
||||
this.module = com.itsaky.androidide.inflater.utils.module
|
||||
} else {
|
||||
startParse(this.module!!)
|
||||
}
|
||||
}
|
||||
inflationEventListener?.onEvent(InflationStartEvent())
|
||||
return doInflate(file, parent).apply {
|
||||
inflationEventListener?.onEvent(InflationFinishEvent(this))
|
||||
_primaryInflatingFile = null
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
this.module = null
|
||||
this.inflationEventListener = null
|
||||
this._primaryInflatingFile = null
|
||||
this._currentLayoutFile = null
|
||||
endParse()
|
||||
}
|
||||
|
||||
protected open fun doInflate(file: File, parent: ViewGroup): List<IView> {
|
||||
val (processor, module) = processXmlFile(file)
|
||||
@@ -210,8 +233,7 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
val namespace =
|
||||
view.findNamespaceByUri(xmlAttribute.namespaceUri)
|
||||
?: throw InflateException("Unknown namespace : ${xmlAttribute.namespaceUri}")
|
||||
val attr =
|
||||
AttributeImpl(namespace = namespace, name = xmlAttribute.name, value = xmlAttribute.value)
|
||||
val attr = onCreateAttribute(view, namespace, xmlAttribute.name, xmlAttribute.value)
|
||||
view.addAttribute(attribute = attr, update = true)
|
||||
inflationEventListener?.onEvent(OnApplyAttributeEvent(view, attr))
|
||||
}
|
||||
@@ -275,16 +297,26 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
): IView {
|
||||
return try {
|
||||
val v = createViewInstance(widget.qualifiedName, parent.context)
|
||||
val view =
|
||||
if (v is ViewGroup) {
|
||||
ViewGroupImpl(currentLayoutFile, widget.qualifiedName, v)
|
||||
} else ViewImpl(currentLayoutFile, widget.qualifiedName, v)
|
||||
return inflationEventListener?.onInterceptCreateView(view) ?: view
|
||||
return componentFactory.createView(currentLayoutFile, widget.qualifiedName, v)
|
||||
} catch (err: Throwable) {
|
||||
onCreateUnsupportedView("Unable to create view for widget ${widget.qualifiedName}", parent)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onCreateAttribute(
|
||||
view: ViewImpl,
|
||||
namespace: INamespace,
|
||||
name: String,
|
||||
value: String
|
||||
): IAttribute {
|
||||
return componentFactory.createAttr(
|
||||
view = view,
|
||||
namespace = namespace,
|
||||
name = name,
|
||||
value = value
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun addNamespaceDecls(element: XmlElement, view: ViewImpl) {
|
||||
if (element.namespaceDeclarationCount > 0) {
|
||||
for (xmlNamespace in element.namespaceDeclarationList) {
|
||||
@@ -324,7 +356,8 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
}
|
||||
|
||||
private fun wrap(parent: ViewGroup): IViewGroup {
|
||||
return ViewGroupImpl(currentLayoutFile, parent.javaClass.name, parent)
|
||||
return componentFactory.createView(currentLayoutFile, parent.javaClass.name, parent)
|
||||
as IViewGroup
|
||||
}
|
||||
|
||||
private fun SourcePosition.lineCol(): String {
|
||||
|
||||
@@ -103,4 +103,8 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
|
||||
protected open fun notifyOnViewRemoved(child: IView) {
|
||||
this.hierarchyChangeListeners.forEach { it.onViewRemoved(this, child) }
|
||||
}
|
||||
|
||||
override fun immutableCopy(): IViewGroup {
|
||||
return ImmutableViewGroupImpl(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,22 +66,19 @@ constructor(
|
||||
}
|
||||
|
||||
override fun removeAttribute(attribute: IAttribute) {
|
||||
|
||||
// reset the value of the attribute to its default value
|
||||
attribute.value = ""
|
||||
updateAttributeInternal(attribute = attribute, notify = false)
|
||||
|
||||
this._attributes.remove(attribute)
|
||||
notifyAttrRemoved(attribute)
|
||||
// TODO(itsaky): Should attribute adapters handle this as well?
|
||||
}
|
||||
|
||||
override fun updateAttribute(attribute: IAttribute) {
|
||||
val existing =
|
||||
findAttribute(attribute)
|
||||
?: throw IllegalArgumentException("Attribute '${attribute.name}' not found")
|
||||
|
||||
val oldVal = existing.value
|
||||
existing.value = attribute.value
|
||||
applyAttribute(existing)
|
||||
notifyAttrUpdated(attribute, oldVal)
|
||||
updateAttributeInternal(attribute)
|
||||
}
|
||||
|
||||
|
||||
override fun findAttribute(namespaceUri: String, name: String): IAttribute? {
|
||||
return this.attributes.find { it.namespace.uri == namespaceUri && it.name == name }
|
||||
}
|
||||
@@ -144,6 +141,24 @@ constructor(
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
internal open fun immutableCopy() : IView {
|
||||
return ImmutableViewImpl(this)
|
||||
}
|
||||
|
||||
private fun updateAttributeInternal(attribute: IAttribute, notify: Boolean = true) {
|
||||
val existing =
|
||||
findAttribute(attribute)
|
||||
?: throw IllegalArgumentException("Attribute '${attribute.name}' not found")
|
||||
|
||||
val oldVal = existing.value
|
||||
existing.value = attribute.value
|
||||
applyAttribute(existing)
|
||||
|
||||
if (notify) {
|
||||
notifyAttrUpdated(attribute, oldVal)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyAttrAdded(attribute: IAttribute) {
|
||||
for (listener in this.attrChangeListeners) {
|
||||
listener.onAttributeAdded(this, attribute)
|
||||
|
||||
+8
-8
@@ -29,8 +29,8 @@ import com.itsaky.androidide.annotations.inflater.ViewAdapter
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.INamespace
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.inflater.newAttribute
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
@@ -43,12 +43,12 @@ open class TextViewAttrAdapter : ViewAttrAdapter() {
|
||||
|
||||
override fun apply(view: IView, attribute: IAttribute): Boolean {
|
||||
return doApply<TextView>(view, attribute) {
|
||||
_: LayoutFile,
|
||||
context: Context,
|
||||
_: LayoutParams,
|
||||
_: INamespace,
|
||||
name: String,
|
||||
value: String ->
|
||||
_: LayoutFile,
|
||||
context: Context,
|
||||
_: LayoutParams,
|
||||
_: INamespace,
|
||||
name: String,
|
||||
value: String ->
|
||||
val drawables = compoundDrawables
|
||||
val drawablesRelative = compoundDrawablesRelative
|
||||
var applied = true
|
||||
@@ -130,7 +130,7 @@ open class TextViewAttrAdapter : ViewAttrAdapter() {
|
||||
|
||||
override fun applyBasic(view: IView) {
|
||||
super.applyBasic(view)
|
||||
view.addAttribute(AttributeImpl(name = "text", value = "AndroidIDE"))
|
||||
view.addAttribute(newAttribute(view = view, name = "text", value = "AndroidIDE"))
|
||||
}
|
||||
|
||||
protected open fun parseTextStyle(value: String): Int {
|
||||
|
||||
+15
-3
@@ -36,9 +36,9 @@ import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.INamespace
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewAdapter
|
||||
import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.inflater.internal.ViewImpl
|
||||
import com.itsaky.androidide.inflater.newAttribute
|
||||
|
||||
/**
|
||||
* Attribute adapter for [View].
|
||||
@@ -132,8 +132,20 @@ open class ViewAttrAdapter : IViewAdapter() {
|
||||
}
|
||||
|
||||
override fun applyBasic(view: IView) {
|
||||
view.addAttribute(AttributeImpl(name = "layout_height", value = "wrap_content"))
|
||||
view.addAttribute(AttributeImpl(name = "layout_width", value = "wrap_content"))
|
||||
view.addAttribute(newAttribute(view = view, name = "layout_height", value = "wrap_content"))
|
||||
view.addAttribute(newAttribute(view = view, name = "layout_width", value = "wrap_content"))
|
||||
}
|
||||
|
||||
override fun isRequiredAttribute(attribute: IAttribute): Boolean {
|
||||
if (attribute.namespace.uri != INamespace.ANDROID.uri) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (attribute.name) {
|
||||
"layout_width",
|
||||
"layout_height" -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun applyLayoutParams(
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ import com.android.aaptcompiler.tryParseFlagSymbol
|
||||
import com.android.aaptcompiler.tryParseInt
|
||||
import com.android.aaptcompiler.tryParseReference
|
||||
import com.itsaky.androidide.inflater.drawable.DrawableParserFactory
|
||||
import com.itsaky.androidide.inflater.utils.module
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import java.io.File
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.android.aaptcompiler.ResourceGroup
|
||||
import com.android.aaptcompiler.ResourceName
|
||||
import com.android.aaptcompiler.ResourceTable
|
||||
import com.android.aaptcompiler.ResourceTablePackage
|
||||
import com.itsaky.androidide.inflater.utils.module
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
|
||||
private val log: ILogger = ILogger.newInstance("ParseLookupUtils")
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.inflater
|
||||
|
||||
import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
|
||||
/** Get the [ILayoutInflater] registered with [Lookup]. */
|
||||
fun lookupLayoutInflater(): ILayoutInflater? {
|
||||
return Lookup.DEFAULT.lookup(ILayoutInflater.LOOKUP_KEY)
|
||||
}
|
||||
|
||||
/** Get the [IComponentFactory] registered with [Lookup]. */
|
||||
fun lookupComponentFactory(): IComponentFactory? {
|
||||
return Lookup.DEFAULT.lookup(IComponentFactory.LAYOUT_INFLATER_COMPONENT_FACTORY_KEY)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun newAttribute(
|
||||
view: IView? = null,
|
||||
namespace: INamespace = INamespace.ANDROID,
|
||||
name: String,
|
||||
value: String
|
||||
): IAttribute {
|
||||
val componentFactory = lookupComponentFactory()
|
||||
if (componentFactory != null && view != null) {
|
||||
return componentFactory.createAttr(view, namespace, name, value)
|
||||
}
|
||||
return AttributeImpl(namespace, name, value)
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.inflater.internal.utils
|
||||
package com.itsaky.androidide.inflater.utils
|
||||
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
@@ -20,17 +20,16 @@ package com.itsaky.androidide.inflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.inflater.internal.LayoutInflaterImpl
|
||||
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
|
||||
import com.itsaky.androidide.inflater.internal.ViewImpl
|
||||
import com.itsaky.androidide.inflater.internal.utils.IDTable
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
import java.io.File
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.io.File
|
||||
|
||||
/** @author Akash Yadav */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@@ -70,11 +69,11 @@ class LayoutInflaterTest {
|
||||
|
||||
(inflated as ViewImpl).attributes.apply {
|
||||
assertThat(this)
|
||||
.contains(AttributeImpl(INamespace.ANDROID, "id", "@+id/template_view"))
|
||||
.contains(newAttribute(inflated, INamespace.ANDROID, "id", "@+id/template_view"))
|
||||
assertThat(this)
|
||||
.contains(AttributeImpl(INamespace.ANDROID, "layout_height", "match_parent"))
|
||||
.contains(newAttribute(inflated, INamespace.ANDROID, "layout_height", "match_parent"))
|
||||
assertThat(this)
|
||||
.contains(AttributeImpl(INamespace.ANDROID, "layout_width", "match_parent"))
|
||||
.contains(newAttribute(inflated, INamespace.ANDROID, "layout_width", "match_parent"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
package com.itsaky.androidide.inflater
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.itsaky.androidide.inflater.internal.utils.endParse
|
||||
import com.itsaky.androidide.inflater.internal.utils.startParse
|
||||
import com.itsaky.androidide.inflater.utils.endParse
|
||||
import com.itsaky.androidide.inflater.utils.startParse
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
|
||||
Reference in New Issue
Block a user