mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: add option to see generated XML directly from UI Designer
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: <span>Yes</span> and <span>No</span>. 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: <span>Yes</span> and <span>No</span>.
|
||||
*
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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'
|
||||
]
|
||||
}
|
||||
|
||||
+8
-11
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
@@ -17,9 +17,12 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<application android:theme="@style/Theme.AndroidIDE">
|
||||
<activity
|
||||
android:name=".UIDesignerActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".ShowXmlActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String?> =
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ fun registerUiDesignerActions(context: Context) {
|
||||
ActionsRegistry.getInstance().apply {
|
||||
registerAction(UndoAction(context))
|
||||
registerAction(RedoAction(context))
|
||||
registerAction(ShowXmlAction(context))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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))
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ This file is part of AndroidIDE.
|
||||
~
|
||||
~ AndroidIDE is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ AndroidIDE is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:title="@string/xml" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/editor_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="28dp"
|
||||
android:text="@string/copy"
|
||||
app:icon="@drawable/ic_copy" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -55,6 +55,20 @@ interface ILayoutInflater : Closeable {
|
||||
*/
|
||||
fun inflate(file: File, parent: ViewGroup): List<IView>
|
||||
|
||||
/**
|
||||
* 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<IView>
|
||||
|
||||
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. */
|
||||
|
||||
@@ -41,6 +41,9 @@ interface IView {
|
||||
/** The parent view. */
|
||||
var parent: IViewGroup?
|
||||
|
||||
/** The namespaces that have been declared in this view. */
|
||||
val namespaceDecls: Collection<INamespace>
|
||||
|
||||
/** The attributes of this view. */
|
||||
val attributes: List<IAttribute>
|
||||
|
||||
|
||||
+42
-14
@@ -86,20 +86,16 @@ open class LayoutInflaterImpl : ILayoutInflater {
|
||||
get() = this._currentLayoutFile!!
|
||||
|
||||
override fun inflate(file: File, parent: ViewGroup): List<IView> {
|
||||
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<IView> {
|
||||
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<IView> {
|
||||
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<IView> {
|
||||
return doInflate(processor, module) { wrap(parent) }
|
||||
}
|
||||
|
||||
protected open fun doInflate(
|
||||
processor: XmlProcessor,
|
||||
parent: IViewGroup,
|
||||
module: AndroidModule,
|
||||
): List<IView> {
|
||||
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<IView>.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)
|
||||
|
||||
@@ -46,9 +46,11 @@ constructor(
|
||||
|
||||
private val attrChangeListeners = mutableListOf<AttributeChangeListener>()
|
||||
private val _attributes = mutableListOf<IAttribute>()
|
||||
internal val namespaceDecls = mutableMapOf<String, INamespace>()
|
||||
internal val namespaces = mutableMapOf<String, INamespace>()
|
||||
|
||||
override var parent: IViewGroup? = null
|
||||
override val namespaceDecls: Collection<INamespace>
|
||||
get() = this.namespaces.values
|
||||
override val attributes: List<IAttribute>
|
||||
get() = this._attributes
|
||||
|
||||
@@ -116,7 +118,7 @@ constructor(
|
||||
|
||||
fun findNamespaces(): Set<INamespace> {
|
||||
return hashSetOf<INamespace>().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 {
|
||||
|
||||
Reference in New Issue
Block a user