diff --git a/app/src/main/assets/data/common/tooling-api-all.jar b/app/src/main/assets/data/common/tooling-api-all.jar index 3729ba62e..47e24cb5d 100644 Binary files a/app/src/main/assets/data/common/tooling-api-all.jar and b/app/src/main/assets/data/common/tooling-api-all.jar differ diff --git a/app/src/main/java/com/itsaky/androidide/fragments/MainFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/MainFragment.kt index 531b77709..5a01d3d45 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/MainFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/MainFragment.kt @@ -13,8 +13,8 @@ import com.itsaky.androidide.PreferencesActivity import com.itsaky.androidide.TerminalActivity import com.itsaky.androidide.adapters.MainActionsListAdapter import com.itsaky.androidide.app.BaseApplication +import com.itsaky.androidide.common.databinding.LayoutDialogProgressBinding import com.itsaky.androidide.databinding.FragmentMainBinding -import com.itsaky.androidide.databinding.LayoutDialogProgressBinding import com.itsaky.androidide.fragments.WizardFragment.OnProjectCreatedListener import com.itsaky.androidide.models.MainScreenAction import com.itsaky.androidide.preferences.databinding.LayoutDialogTextInputBinding diff --git a/app/src/main/java/com/itsaky/androidide/handlers/FileTreeActionHandler.kt b/app/src/main/java/com/itsaky/androidide/handlers/FileTreeActionHandler.kt index 0bb70b65e..f62909a8e 100644 --- a/app/src/main/java/com/itsaky/androidide/handlers/FileTreeActionHandler.kt +++ b/app/src/main/java/com/itsaky/androidide/handlers/FileTreeActionHandler.kt @@ -154,7 +154,7 @@ class FileTreeActionHandler : BaseEventHandler() { private fun createFileOptionsFragment(file: File): OptionsListFragment { val fragment = OptionsListFragment() fragment.addOption( - SheetOption(ID_COPY_PATH, R.drawable.ic_file_copy_path, string.copy_path, file) + SheetOption(ID_COPY_PATH, R.drawable.ic_copy, string.copy_path, file) ) fragment.addOption( SheetOption(ID_RENAME_FILE, R.drawable.ic_file_rename, string.rename_file, file) diff --git a/common/src/main/java/com/itsaky/androidide/utils/DialogUtils.kt b/common/src/main/java/com/itsaky/androidide/utils/DialogUtils.kt index 59b6efc54..c5607ec9e 100644 --- a/common/src/main/java/com/itsaky/androidide/utils/DialogUtils.kt +++ b/common/src/main/java/com/itsaky/androidide/utils/DialogUtils.kt @@ -18,7 +18,10 @@ package com.itsaky.androidide.utils import android.content.Context import android.content.DialogInterface.OnClickListener +import android.view.LayoutInflater +import android.view.View import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.itsaky.androidide.common.databinding.LayoutDialogProgressBinding import com.itsaky.androidide.resources.R.string import com.itsaky.androidide.resources.R.style import org.jetbrains.annotations.Contract @@ -29,7 +32,37 @@ import org.jetbrains.annotations.Contract * @author Akash Yadav */ object DialogUtils { - + + @JvmStatic + @JvmOverloads + fun newProgressDialog( + context: Context, + title: String, + message: String? = null, + cancelable: Boolean = false, + onCancelClick: OnClickListener? = null + ): MaterialAlertDialogBuilder { + val binding = LayoutDialogProgressBinding.inflate(LayoutInflater.from(context)) + val builder = newMaterialDialogBuilder(context) + builder.setTitle(title) + builder.setView(binding.root) + builder.setCancelable(cancelable) + + if (message != null) { + binding.message.text = message + binding.message.visibility = View.VISIBLE + } + + if (onCancelClick != null) { + builder.setPositiveButton(android.R.string.cancel) { dialog, which -> + dialog.dismiss() + onCancelClick.onClick(dialog, which) + } + } + + return builder + } + /** * Create a new alert dialog with two buttons: Yes and No. This method * simply calls [.newYesNoDialog] with default values for title and message. @@ -53,9 +86,10 @@ object DialogUtils { context.getString(string.msg_yesno_def_title), context.getString(string.msg_yesno_def_message), positiveClickListener, - negativeClickListener) + negativeClickListener + ) } - + /** * Create a new alert dialog with two buttons: Yes and No. * @@ -84,7 +118,7 @@ object DialogUtils { builder.setNegativeButton(string.no, negativeClickListener) return builder } - + /** * Creates a new MaterialAlertDialogBuilder with the app's default style. * @@ -96,4 +130,4 @@ object DialogUtils { fun newMaterialDialogBuilder(context: Context): MaterialAlertDialogBuilder { return MaterialAlertDialogBuilder(context, style.AppTheme_MaterialAlertDialog) } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/layout_dialog_progress.xml b/common/src/main/res/layout/layout_dialog_progress.xml similarity index 100% rename from app/src/main/res/layout/layout_dialog_progress.xml rename to common/src/main/res/layout/layout_dialog_progress.xml diff --git a/dependencies.gradle b/dependencies.gradle index 1ae831c31..03838bb57 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -77,11 +77,11 @@ ext { lsp4j : 'org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0', jb_annotations: 'org.jetbrains:annotations:23.0.0' ] - + git = [ - jgit : 'org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r' + jgit: 'org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r' ] - + tests = [ junit : 'junit:junit:4.13.2', google_truth : 'com.google.truth:truth:1.1.3', @@ -89,5 +89,6 @@ ext { androidx_test_core: 'androidx.test:core:1.5.0', androidx_junit : 'androidx.test.ext:junit:1.1.4', androidx_espresso : 'androidx.test.espresso:espresso-core:3.5.0', + mockito_kotlin : 'org.mockito.kotlin:mockito-kotlin:4.1.0' ] } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatter.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatter.java index c2913dc82..a5ffc1893 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatter.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatter.java @@ -18,7 +18,6 @@ package com.itsaky.androidide.lsp.xml.providers.format; import com.itsaky.androidide.lsp.models.TextEdit; -import com.itsaky.androidide.lsp.xml.models.XMLServerSettings; import com.itsaky.androidide.models.Range; import com.itsaky.androidide.utils.ILogger; @@ -27,27 +26,25 @@ import org.eclipse.lemminx.dom.DOMDocument; import java.util.List; -/** - * XML formatter support. - * - */ +/** XML formatter support. */ public class XMLFormatter { private static final ILogger LOG = ILogger.newInstance("XMLFormatter"); - + /** - * Returns a List containing a single TextEdit, containing the newly formatted - * changes of the document. + * Returns a List containing a single TextEdit, containing the newly formatted changes of the + * document. * - * @param range specified range in which formatting will be done + * @param range specified range in which formatting will be done * @return List containing a TextEdit with formatting changes */ public List format(DOMDocument xmlDocument, Range range) { try { - XMLFormatterDocument formatterDocument = new XMLFormatterDocument(xmlDocument.getTextDocument(), range); + XMLFormatterDocument formatterDocument = + new XMLFormatterDocument(xmlDocument.getTextDocument(), range); return formatterDocument.format(); } catch (BadLocationException e) { LOG.error("Formatting failed due to BadLocation", e); } return null; } -} \ No newline at end of file +} diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java index 55e9e25a4..0e3b2b6c9 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java @@ -44,9 +44,7 @@ public class XMLBuilder { private final String lineDelimiter; private final StringBuilder xml; private final String whitespacesIndent; - - private static final Logger LOGGER = Logger.getLogger(XMLBuilder.class.getName()); - + public XMLBuilder(String whitespacesIndent, String lineDelimiter) { this.whitespacesIndent = whitespacesIndent; this.lineDelimiter = lineDelimiter; @@ -122,11 +120,11 @@ public class XMLBuilder { public XMLBuilder addSingleAttribute( DOMAttr attr, boolean surroundWithQuotes, boolean prependSpace) { return addSingleAttribute( - attr.getName(), attr.getOriginalValue(), surroundWithQuotes, prependSpace, attr); + attr.getName(), attr.getOriginalValue(), surroundWithQuotes, prependSpace); } public XMLBuilder addSingleAttribute(String name, String value, boolean surroundWithQuotes) { - return addSingleAttribute(name, value, surroundWithQuotes, true, null); + return addSingleAttribute(name, value, surroundWithQuotes, true); } /** @@ -140,7 +138,7 @@ public class XMLBuilder { * @return this XML Builder */ private XMLBuilder addSingleAttribute( - String name, String value, boolean surroundWithQuotes, boolean prependSpace, DOMAttr attr) { + String name, String value, boolean surroundWithQuotes, boolean prependSpace) { if (prependSpace) { appendSpace(); } diff --git a/resources/src/main/res/drawable/ic_file_copy_path.xml b/resources/src/main/res/drawable/ic_copy.xml similarity index 100% rename from resources/src/main/res/drawable/ic_file_copy_path.xml rename to resources/src/main/res/drawable/ic_copy.xml diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index 14d71552a..c324faaca 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 e414230f0..3e7414881 100644 --- a/uidesigner/build.gradle +++ b/uidesigner/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation project(path: ':actions') implementation project(path: ':common') + implementation project(path: ':editor') implementation project(path: ':logger') implementation project(path: ':lookup') implementation project(path: ':lsp:api') @@ -56,4 +57,5 @@ dependencies { testImplementation tests.junit testImplementation tests.google_truth testImplementation tests.robolectric + testImplementation tests.mockito_kotlin } \ No newline at end of file diff --git a/uidesigner/src/main/AndroidManifest.xml b/uidesigner/src/main/AndroidManifest.xml index 6570288a8..f3d9fed2a 100644 --- a/uidesigner/src/main/AndroidManifest.xml +++ b/uidesigner/src/main/AndroidManifest.xml @@ -17,9 +17,12 @@ - + + \ No newline at end of file diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/ShowXmlActivity.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/ShowXmlActivity.kt new file mode 100644 index 000000000..ae529cbcc --- /dev/null +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/ShowXmlActivity.kt @@ -0,0 +1,107 @@ +/* + * 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 + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import com.blankj.utilcode.util.ReflectUtils +import com.itsaky.androidide.app.BaseIDEActivity +import com.itsaky.androidide.editor.IEditorProvider +import com.itsaky.androidide.lookup.Lookup +import com.itsaky.androidide.preferences.internal.fontSize +import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE +import com.itsaky.androidide.uidesigner.databinding.ActivityShowXmlBinding +import com.itsaky.androidide.utils.jetbrainsMono +import io.github.rosemoe.sora.event.ScrollEvent +import io.github.rosemoe.sora.event.Unsubscribe +import io.github.rosemoe.sora.widget.CodeEditor + +/** + * An activity to show the XML code generated by UI Designer. + * + * @author Akash Yadav + */ +class ShowXmlActivity : BaseIDEActivity() { + + private var editor: CodeEditor? = null + private var unsubscribe: Unsubscribe? = null + private var binding: ActivityShowXmlBinding? = null + + companion object { + const val KEY_XML = "ide.uidesigner.viewXml.Xml" + } + + override fun bindLayout(): View { + this.binding = ActivityShowXmlBinding.inflate(layoutInflater) + this.editor = + Lookup.DEFAULT.lookup(IEditorProvider::class.java)!!.createEditor(this) as CodeEditor + + setSupportActionBar(this.binding!!.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + this.binding!!.toolbar.setNavigationOnClickListener { finish() } + + this.editor?.let { editor -> + editor.colorScheme = SchemeAndroidIDE.newInstance(this@ShowXmlActivity) + editor.editable = false + editor.typefaceText = jetbrainsMono() + editor.typefaceLineNumber = jetbrainsMono() + + // TODO(itsaky): Move language implementations to separate module + editor.setEditorLanguage( + ReflectUtils.reflect("com.itsaky.androidide.language.xml.XMLLanguage").newInstance().get() + ) + + editor.setText(intent?.getStringExtra(KEY_XML) ?: "") + editor.setTextSize(fontSize) + editor.subscribeEvent(ScrollEvent::class.java) { event, unsubscribe -> + this.unsubscribe = unsubscribe + val fab = this.binding?.copy ?: return@subscribeEvent + event.apply { + if (startY > endY && fab.isExtended) { + fab.shrink() + } + if (endY == 0 || startY < endY && !fab.isExtended) { + fab.extend() + } + } + } + + binding!!.editorContainer.let { container -> + container.removeAllViews() + container.addView(editor, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) + } + } + + return this.binding!!.root + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onDestroy() { + super.onDestroy() + this.binding = null + this.unsubscribe?.unsubscribe() + this.editor?.release() + this.editor = null + } +} diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/ShowXmlAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/ShowXmlAction.kt new file mode 100644 index 000000000..909a25245 --- /dev/null +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/ShowXmlAction.kt @@ -0,0 +1,103 @@ +/* + * 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.actions + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import com.blankj.utilcode.util.ThreadUtils +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.inflater.internal.ViewImpl +import com.itsaky.androidide.inflater.viewGroup +import com.itsaky.androidide.tasks.executeAsyncProvideError +import com.itsaky.androidide.uidesigner.R +import com.itsaky.androidide.uidesigner.ShowXmlActivity +import com.itsaky.androidide.uidesigner.utils.ViewToXml +import com.itsaky.androidide.utils.DialogUtils +import com.itsaky.androidide.utils.ILogger +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException + +/** + * Navigates the user to [ShowXmlActivity]. + * + * @author Akash Yadav + */ +class ShowXmlAction(context: Context) : UiDesignerAction() { + override val id: String = "ide.uidesigner.showXml" + + private val log = ILogger.newInstance("ShowXmlAction") + + init { + label = context.getString(R.string.xml) + icon = ContextCompat.getDrawable(context, R.drawable.ic_language_xml) + } + + override fun execAction(data: ActionData): Any { + data.requireActivity().apply { + val workspace = data.requireWorkspace().workspaceView + val future: CompletableFuture = + executeAsyncProvideError({ + if (workspace.childCount == 0) { + throw CompletionException( + IllegalStateException("No views have been added to workspace") + ) + } + + if (workspace.childCount > 1) { + throw CompletionException( + IllegalStateException("Invalid view hierarchy. More than one root views found.") + ) + } + + return@executeAsyncProvideError ViewToXml.generateXml(workspace[0] as ViewImpl) + }) { _, _ -> + } + + val progress = + DialogUtils.newProgressDialog( + this, + getString(R.string.title_generating_xml), + getString(R.string.please_wait), + false + ) { _, _ -> + future.cancel(true) + } + .show() + + future.whenComplete { result, error -> + ThreadUtils.runOnUiThread { + + progress.dismiss() + + if (result.isNullOrBlank() || error != null) { + log.error("Unable to generate XML code", error) + return@runOnUiThread + } + + val intent = + Intent(this, ShowXmlActivity::class.java).apply { + putExtra(ShowXmlActivity.KEY_XML, result) + } + startActivity(intent) + } + } + } + return true + } +} diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/uiDesignerActions.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/uiDesignerActions.kt index 3bb12a35e..c9728fae8 100644 --- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/uiDesignerActions.kt +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/uiDesignerActions.kt @@ -25,6 +25,7 @@ fun registerUiDesignerActions(context: Context) { ActionsRegistry.getInstance().apply { registerAction(UndoAction(context)) registerAction(RedoAction(context)) + registerAction(ShowXmlAction(context)) } } 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 be542bd0d..530468e5b 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 @@ -117,16 +117,18 @@ class DesignerWorkspaceFragment : BaseFragment() { var hasError = false val inflated = try { - inflater.inflate(viewModel.file, binding!!.workspace) + inflater.inflate(viewModel.file, workspaceView) } catch (e: Throwable) { log.error(e) viewModel.errText = "${e.message}${e.cause?.message?.let { "\n$it" } ?: ""}" hasError = true emptyList() } + if (inflated.isEmpty() && !hasError) { viewModel.errText = getString(R.string.msg_empty_ui_layout) } + binding!! .workspace .setOnDragListener(WidgetDragListener(workspaceView, this.placeholder, touchSlop)) diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/ViewToXml.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/ViewToXml.kt new file mode 100644 index 000000000..f73210af3 --- /dev/null +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/ViewToXml.kt @@ -0,0 +1,84 @@ +/* + * 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.IView +import com.itsaky.androidide.inflater.IViewGroup +import com.itsaky.androidide.inflater.internal.ViewImpl +import com.itsaky.androidide.lsp.xml.utils.XMLBuilder + +/** + * Generates XML code for [IView]. + * + * @author Akash Yadav + */ +object ViewToXml { + + @JvmStatic + fun generateXml(view: ViewImpl): String { + val builder = XMLBuilder("", System.lineSeparator()) + builder.apply { + appendXmlProlog() + linefeed() + appendView(view) + } + return builder.toString() + } + + private fun XMLBuilder.appendView(view: ViewImpl, indent: Int = 1) { + startElement(view.tag, false) + + for (namespace in view.namespaceDecls) { + linefeed() + indent(indent) + addSingleAttribute("xmlns:${namespace.prefix}", namespace.uri, true) + } + + for (attr in view.attributes) { + linefeed() + indent(indent) + addSingleAttribute("${attr.namespace.prefix}:${attr.name}", attr.value, true) + } + + if (view is IViewGroup) { + + closeStartElement() + + for (i in 0 until view.childCount) { + linefeed() + linefeed() + indent(indent) + appendView(view[i] as ViewImpl, indent + 1) + } + + linefeed() + linefeed() + + endElement(view.tag) + } else { + selfCloseElement() + } + } + + private fun XMLBuilder.appendXmlProlog() { + startPrologOrPI("xml") + addSingleAttribute("version", "1.0", true) + addSingleAttribute("encoding", "utf-8", true) + endPrologOrPI() + } +} diff --git a/uidesigner/src/main/res/layout/activity_show_xml.xml b/uidesigner/src/main/res/layout/activity_show_xml.xml new file mode 100644 index 000000000..ade1c1b96 --- /dev/null +++ b/uidesigner/src/main/res/layout/activity_show_xml.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file 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 a27c7fa1c..144dfbb7a 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 @@ -55,6 +55,20 @@ interface ILayoutInflater : Closeable { */ fun inflate(file: File, parent: ViewGroup): List + /** + * 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. + */ + fun inflate(file: File, parent: IViewGroup): List + fun closeSilently() { try { close() @@ -62,7 +76,7 @@ interface ILayoutInflater : Closeable { // ignored } } - + companion object { /** The [Key] that can be used to lookup the layout inflater service. */ diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt index f5866f37e..c82059de9 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt @@ -41,6 +41,9 @@ interface IView { /** The parent view. */ var parent: IViewGroup? + /** The namespaces that have been declared in this view. */ + val namespaceDecls: Collection + /** The attributes of this view. */ val attributes: List 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 493ec8eb1..3c48c7740 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 @@ -86,20 +86,16 @@ open class LayoutInflaterImpl : ILayoutInflater { get() = this._currentLayoutFile!! override fun inflate(file: File, parent: ViewGroup): List { - this._primaryInflatingFile = file - IDTable.newRound() - if (!isParsing) { - if (this.module == null) { - startParse(file) - this.module = com.itsaky.androidide.inflater.utils.module - } else { - startParse(this.module!!) - } - } - inflationEventListener?.onEvent(InflationStartEvent()) + startInflation(file) return doInflate(file, parent).apply { - inflationEventListener?.onEvent(InflationFinishEvent(this)) - _primaryInflatingFile = null + finishInflation() + } + } + + override fun inflate(file: File, parent: IViewGroup) : List { + startInflation(file) + return doInflate(file, parent).apply { + finishInflation() } } @@ -115,6 +111,11 @@ open class LayoutInflaterImpl : ILayoutInflater { val (processor, module) = processXmlFile(file) return doInflate(processor, parent, module) } + + protected open fun doInflate(file: File, parent: IViewGroup): List { + val (processor, module) = processXmlFile(file) + return doInflate(processor, parent, module) + } protected open fun doInflate( processor: XmlProcessor, @@ -123,6 +124,14 @@ open class LayoutInflaterImpl : ILayoutInflater { ): List { return doInflate(processor, module) { wrap(parent) } } + + protected open fun doInflate( + processor: XmlProcessor, + parent: IViewGroup, + module: AndroidModule, + ): List { + return doInflate(processor, module) { parent } + } protected open fun doInflate( processor: XmlProcessor, @@ -320,7 +329,7 @@ open class LayoutInflaterImpl : ILayoutInflater { protected open fun addNamespaceDecls(element: XmlElement, view: ViewImpl) { if (element.namespaceDeclarationCount > 0) { for (xmlNamespace in element.namespaceDeclarationList) { - view.namespaceDecls[xmlNamespace.uri] = NamespaceImpl(xmlNamespace.prefix, xmlNamespace.uri) + view.namespaces[xmlNamespace.uri] = NamespaceImpl(xmlNamespace.prefix, xmlNamespace.uri) } } } @@ -354,6 +363,25 @@ open class LayoutInflaterImpl : ILayoutInflater { private fun onCreateUnsupportedView(message: String, parent: ViewGroup): IView { return ErrorView(currentLayoutFile, parent.context, message) } + + private fun List.finishInflation() { + inflationEventListener?.onEvent(InflationFinishEvent(this)) + _primaryInflatingFile = null + } + + private fun startInflation(file: File) { + this._primaryInflatingFile = file + IDTable.newRound() + if (!isParsing) { + if (this.module == null) { + startParse(file) + this.module = com.itsaky.androidide.inflater.utils.module + } else { + startParse(this.module!!) + } + } + inflationEventListener?.onEvent(InflationStartEvent()) + } private fun wrap(parent: ViewGroup): IViewGroup { return componentFactory.createView(currentLayoutFile, parent.javaClass.name, parent) 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 12ddae826..d46081bdc 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 @@ -46,9 +46,11 @@ constructor( private val attrChangeListeners = mutableListOf() private val _attributes = mutableListOf() - internal val namespaceDecls = mutableMapOf() + internal val namespaces = mutableMapOf() override var parent: IViewGroup? = null + override val namespaceDecls: Collection + get() = this.namespaces.values override val attributes: List get() = this._attributes @@ -116,7 +118,7 @@ constructor( fun findNamespaces(): Set { return hashSetOf().apply { - addAll(namespaceDecls.values) + addAll(namespaces.values) if (parent is ViewImpl) { (parent as? ViewImpl)?.findNamespaces()?.let { addAll(it) } } @@ -124,7 +126,7 @@ constructor( } fun findNamespaceByUri(uri: String): INamespace? { - return this.namespaceDecls[uri] ?: (parent as? ViewImpl)?.findNamespaceByUri(uri) + return this.namespaces[uri] ?: (parent as? ViewImpl)?.findNamespaceByUri(uri) } open fun immutable() : IView {