feat: add ability to remove attributes from views

This commit is contained in:
Akash Yadav
2022-12-06 22:04:37 +05:30
parent 1b8110c61d
commit c4cb67fc18
39 changed files with 853 additions and 198 deletions
@@ -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
}
@@ -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() {
@@ -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.
+1
View File
@@ -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')
@@ -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()
}
}
@@ -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) {
@@ -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)
}
}
}
@@ -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)
}
}
@@ -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
}
@@ -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)
@@ -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)
@@ -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')"
}
}
@@ -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!")
}
}
@@ -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!")
}
}
@@ -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!")
}
}
@@ -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)
@@ -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 {
@@ -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(
@@ -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)
}
@@ -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