diff --git a/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt b/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt index b60c03a0b..1d524f68b 100644 --- a/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt +++ b/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt @@ -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 } diff --git a/app/src/main/java/com/itsaky/androidide/utils/DialogUtils.java b/common/src/main/java/com/itsaky/androidide/utils/DialogUtils.kt similarity index 52% rename from app/src/main/java/com/itsaky/androidide/utils/DialogUtils.java rename to common/src/main/java/com/itsaky/androidide/utils/DialogUtils.kt index af6135bfc..59b6efc54 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/DialogUtils.java +++ b/common/src/main/java/com/itsaky/androidide/utils/DialogUtils.kt @@ -14,51 +14,48 @@ * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . */ +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: Yes and No. 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 Yes button - * click. + * click. * @param negativeClickListener A listener that will be invoked on the No 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: Yes and No. * @@ -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 Yes button - * click. + * click. * @param negativeClickListener A listener that will be invoked on the No 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) } -} +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt index 28f28cef5..fef561221 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt @@ -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 /** diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt index 5d9f56f5d..88639994f 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt @@ -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() { diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt index 9cd615464..1894bf2c9 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt @@ -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() { diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index a99019d50..5f4ec9982 100644 Binary files a/tests/test-home/.androidide/init/model.jar and b/tests/test-home/.androidide/init/model.jar differ diff --git a/uidesigner/build.gradle b/uidesigner/build.gradle index 77d211d3d..cc687e38b 100644 --- a/uidesigner/build.gradle +++ b/uidesigner/build.gradle @@ -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') diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/adapters/ViewAttrListAdapter.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/adapters/ViewAttrListAdapter.kt index 64a9bae5c..2ce2dcf3f 100644 --- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/adapters/ViewAttrListAdapter.kt +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/adapters/ViewAttrListAdapter.kt @@ -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, private val viewModel: WorkspaceViewModel?, private val onClick: (IAttribute) -> Unit) : - RecyclerView.Adapter() { - - private val attributes = attributes.sortedBy { it.name } - +class ViewAttrListAdapter( + attributes: List, + private val viewModel: WorkspaceViewModel?, + private val onDeleteAttr: (IAttribute) -> Boolean, + private val onClick: (IAttribute) -> Unit +) : RecyclerView.Adapter() { + + 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, 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 { - 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 { + 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() + } } diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt index 7b529f99a..5ea04a527 100644 --- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt @@ -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) { diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/ViewInfoFragment.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/ViewInfoFragment.kt index dd291d303..a51b87005 100644 --- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/ViewInfoFragment.kt +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/ViewInfoFragment.kt @@ -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 } diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiAttribute.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiAttribute.kt new file mode 100644 index 000000000..7cde2a732 --- /dev/null +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiAttribute.kt @@ -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 . + */ + +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) + } + } +} diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/UiInflaterComponentFactory.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/UiInflaterComponentFactory.kt new file mode 100644 index 000000000..e26986d67 --- /dev/null +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/UiInflaterComponentFactory.kt @@ -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 . + */ + +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) + } + } +} diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/UiLayoutInflater.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/UiLayoutInflater.kt new file mode 100644 index 000000000..9577a24cc --- /dev/null +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/UiLayoutInflater.kt @@ -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 . + */ + +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) + } +} diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/ValueCompletionProvider.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/ValueCompletionProvider.kt index 4f58496de..4abf65b85 100644 --- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/ValueCompletionProvider.kt +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/ValueCompletionProvider.kt @@ -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) } diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/Widgets.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/Widgets.kt index a17169cb2..dfdc4e6bc 100644 --- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/Widgets.kt +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/Widgets.kt @@ -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")) } } } diff --git a/uidesigner/src/main/res/layout/layout_viewattr_item.xml b/uidesigner/src/main/res/layout/layout_viewattr_item.xml index 4b28e5e02..ee0f81fd5 100644 --- a/uidesigner/src/main/res/layout/layout_viewattr_item.xml +++ b/uidesigner/src/main/res/layout/layout_viewattr_item.xml @@ -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" /> + + \ No newline at end of file diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/DefaultComponentFactory.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/DefaultComponentFactory.kt new file mode 100644 index 000000000..e5eae8c79 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/DefaultComponentFactory.kt @@ -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 . + */ + +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) + } +} \ No newline at end of file diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IComponentFactory.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IComponentFactory.kt new file mode 100644 index 000000000..71b629121 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IComponentFactory.kt @@ -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 . + */ + +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() + } +} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/ILayoutInflater.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/ILayoutInflater.kt index fa00bdbba..a27c7fa1c 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/ILayoutInflater.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/ILayoutInflater.kt @@ -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 + fun inflate(file: File, parent: ViewGroup): List + + 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() + + /** + * 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 + } } } } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewAdapter.kt index 39774cfba..cc79c4213 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewAdapter.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewAdapter.kt @@ -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 } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/IInflateEventsListener.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/IInflateEventsListener.kt new file mode 100644 index 000000000..ea8b20333 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/IInflateEventsListener.kt @@ -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 . + */ + +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<*>) +} \ No newline at end of file diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/IInflationEvent.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/IInflationEvent.kt new file mode 100644 index 000000000..ed30465a5 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/IInflationEvent.kt @@ -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 . + */ + +package com.itsaky.androidide.inflater.events + +/** + * An inflation event. + * + * @author Akash Yadav + */ +abstract class IInflationEvent(val data: T) \ No newline at end of file diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/inflationEvents.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/inflationEvents.kt similarity index 63% rename from xml-inflater/src/main/java/com/itsaky/androidide/inflater/inflationEvents.kt rename to xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/inflationEvents.kt index 27ab1b48e..7a3912c4a 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/inflationEvents.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/events/inflationEvents.kt @@ -15,38 +15,10 @@ * along with AndroidIDE. If not, see . */ -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(val data: T) +import com.itsaky.androidide.inflater.IAttribute +import com.itsaky.androidide.inflater.IView /** Indicates start of layout inflation. */ class InflationStartEvent : IInflationEvent(Unit) diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AttributeImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AttributeImpl.kt index 622a607a3..0501131a8 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AttributeImpl.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AttributeImpl.kt @@ -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')" + } +} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableAttributeImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableAttributeImpl.kt new file mode 100644 index 000000000..52a3d6f59 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableAttributeImpl.kt @@ -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 . + */ + +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!") + } +} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableViewGroupImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableViewGroupImpl.kt new file mode 100644 index 000000000..fba86e458 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableViewGroupImpl.kt @@ -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 . + */ + +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!") + } +} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableViewImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableViewImpl.kt new file mode 100644 index 000000000..629b46990 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ImmutableViewImpl.kt @@ -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 . + */ + +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!") + } +} \ No newline at end of file diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt index 69a773721..493ec8eb1 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt @@ -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 { 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 { diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt index 83d85b150..d18f213b9 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt @@ -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) + } } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt index 93d6fcabf..f17b3e268 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt @@ -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) diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/TextViewAttrAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/TextViewAttrAdapter.kt index cde934269..cd846b544 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/TextViewAttrAdapter.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/TextViewAttrAdapter.kt @@ -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(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 { diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt index 2584bbf6d..7cd2ed41d 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt @@ -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( diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt index 8106dab16..ed9d3cffc 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt @@ -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 diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/lookup.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/lookup.kt index f34d35a25..885fa2981 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/lookup.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/lookup.kt @@ -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") diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/inflaterUtils.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/inflaterUtils.kt new file mode 100644 index 000000000..b15024928 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/inflaterUtils.kt @@ -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 . + */ + +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) +} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/module.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/module.kt similarity index 96% rename from xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/module.kt rename to xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/module.kt index 2a67e35e0..c18b0eb00 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/module.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/module.kt @@ -15,7 +15,7 @@ * along with AndroidIDE. If not, see . */ -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 diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/viewUtils.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/viewUtils.kt similarity index 100% rename from xml-inflater/src/main/java/com/itsaky/androidide/inflater/viewUtils.kt rename to xml-inflater/src/main/java/com/itsaky/androidide/inflater/utils/viewUtils.kt diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt index 8ece4a4a1..c7641ee90 100644 --- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt +++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt @@ -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")) } } } diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt index f98bfc682..e567c12e3 100644 --- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt +++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt @@ -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