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 extends TextEdit> 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 {