Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,6 +87,7 @@ class EditorActivity : BaseActivity() {
private val updateMenuIconsState: Runnable = Runnable { undoRedo!!.updateButtons() }
private var originalProductionXml: String? = null
private var originalDesignXml: String? = null
private var currentLayoutBasePath: String? = null

private val onBackPressedCallback =
object : OnBackPressedCallback(true) {
Expand DownExpand Up@@ -756,7 +757,8 @@ class EditorActivity : BaseActivity() {
originalProductionXml = production
originalDesignXml = design

binding.editorLayout.loadLayoutFromParser(design)
currentLayoutBasePath = File(layoutFile.path).parent
binding.editorLayout.loadLayoutFromParser(design, currentLayoutBasePath)

project.currentLayout = layoutFile
supportActionBar?.subtitle = layoutName
Expand All@@ -768,6 +770,7 @@ class EditorActivity : BaseActivity() {
binding.editorLayout.post {
binding.editorLayout.requestLayout()
binding.editorLayout.invalidate()
binding.editorLayout.updateUndoRedoHistory()
binding.editorLayout.markAsSaved()
}

Expand All@@ -785,7 +788,7 @@ class EditorActivity : BaseActivity() {
private fun restoreOriginalXmlIfNeeded() {
val xmlToRestore = originalDesignXml ?: originalProductionXml
if (!xmlToRestore.isNullOrBlank()) {
binding.editorLayout.loadLayoutFromParser(xmlToRestore)
binding.editorLayout.loadLayoutFromParser(xmlToRestore, currentLayoutBasePath)
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,8 @@ class PreviewLayoutActivity : BaseActivity() {
setContentView(binding.getRoot())
@Suppress("DEPRECATION")
val layoutFile = intent.extras?.getParcelable<LayoutFile>(Constants.EXTRA_KEY_LAYOUT)
val parser = XmlLayoutParser(this)
val basePath = layoutFile?.path?.let { java.io.File(it).parent }
val parser = XmlLayoutParser(this, basePath)
layoutFile?.readDesignFile()?.let { parser.parseFromXml(it, this) }

val previewContainer = binding.root.findViewById<ViewGroup>(R.id.preview_container)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -92,6 +92,7 @@ class DesignEditor : LinearLayout {
private var isModified = false
private lateinit var preferencesManager: PreferencesManager
private var parser: XmlLayoutParser? = null
private var currentBasePath: String? = null
private val attrTranslationX = "android:translationX"
private val attrTranslationY = "android:translationY"
private val widgetIdOverrides = mapOf(
Expand DownExpand Up@@ -450,11 +451,16 @@ class DesignEditor : LinearLayout {

private fun sanitizeIdName(base: String): String = widgetIdOverrides[base] ?: base

fun loadLayoutFromParser(xml: String) {
fun loadLayoutFromParser(xml: String, basePath: String? = null) {
clearAll()
if (xml.isEmpty()) return

val parser = XmlLayoutParser(context)
// Store basePath for undo/redo operations
if (basePath != null) {
currentBasePath = basePath
}

val parser = XmlLayoutParser(context, currentBasePath)
this.parser = parser

parser.parseFromXml(xml, context)
Expand DownExpand Up@@ -539,6 +545,9 @@ class DesignEditor : LinearLayout {
if (undoRedoManager == null) return
val result = XmlLayoutGenerator().generate(this, false)

// Don't add empty states to history
if (result.isEmpty()) return

undoRedoManager!!.addToHistory(result)
markAsModified()
}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
package org.appdevforall.codeonthego.layouteditor.tools

import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import org.appdevforall.codeonthego.layouteditor.editor.initializer.AttributeMap
import org.xmlpull.v1.XmlPullParser

object XmlParserUtils {

fun extractAttributes(parser: XmlPullParser): AttributeMap {
val map = AttributeMap()

for (i in 0 until parser.attributeCount) {
map.putValue(
parser.getAttributeName(i),
parser.getAttributeValue(i)
)
}

return map
}

fun getAttribute(
parser: XmlPullParser,
name: String
): String? =
(0 until parser.attributeCount)
.firstOrNull { parser.getAttributeName(it) == name }
?.let { parser.getAttributeValue(it) }

fun applyAttributes(
parser: XmlPullParser,
target: View,
attributeMap: MutableMap<View, AttributeMap>,
marker: String,
skip: String? = null
) {
val map = attributeMap[target] ?: AttributeMap()
map.putValue(marker, "true")

for (i in 0 until parser.attributeCount) {
val attrName = parser.getAttributeName(i)
if (attrName == skip) continue
map.putValue(attrName, parser.getAttributeValue(i))
}

attributeMap[target] = map
}

fun createIncludePlaceholder(
context: Context,
attributeMap: MutableMap<View, AttributeMap>,
marker: String
): View = View(context).also {
val attrs = AttributeMap().apply { putValue(marker, "true") }
attributeMap[it] = attrs
}

fun createMergeWrapper(context: Context): FrameLayout =
FrameLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,9 @@ public class XmlLayoutGenerator {
public String generate(@NonNull DesignEditor editor, boolean useSuperclasses) {
this.useSuperclasses = useSuperclasses;

// Clear builder to avoid accumulating content from previous calls
builder.setLength(0);

if (editor.getChildCount() == 0) {
return "";
}
Expand All@@ -43,6 +46,9 @@ private String peek(View view, HashMap<View, AttributeMap> attributeMap, int dep
if (tryWriteFragment(view, attributeMap, depth)) {
return builder.toString();
}
if (tryWriteMerge(view, attributeMap, depth)) {
return builder.toString();
}
String indent = getIndent(depth);
int nextDepth = depth;

Expand DownExpand Up@@ -145,6 +151,40 @@ private boolean tryWriteFragment(View view, HashMap<View, AttributeMap> attribut
return false;
}

private boolean tryWriteMerge(View view, HashMap<View, AttributeMap> attributeMap, int depth) {
AttributeMap attrs = attributeMap.get(view);

if (attrs != null && attrs.contains("tools:is_xml_merge")) {
String indent = getIndent(depth);
builder.append(indent).append("<merge");

for (String key : attrs.keySet()) {
if (key.equals("tools:is_xml_merge")) continue;

builder.append("\n").append(indent).append(TAB)
.append(key).append("=\"")
.append(StringEscapeUtils.escapeXml11(attrs.getValue(key)))
.append("\"");
}

// Check if merge has children
if (view instanceof ViewGroup group && group.getChildCount() > 0) {
builder.append(">\n\n");

for (int i = 0; i < group.getChildCount(); i++) {
peek(group.getChildAt(i), attributeMap, depth + 1);
}

builder.append(indent).append("</merge>\n\n");
} else {
// Handle empty merge
builder.append(" />\n\n");
}
return true;
}
return false;
}

@NonNull
private String getIndent(int depth) {
return TAB.repeat(depth);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,16 +16,19 @@ import org.appdevforall.codeonthego.layouteditor.managers.IdManager.clear
import org.appdevforall.codeonthego.layouteditor.utils.Constants
import org.appdevforall.codeonthego.layouteditor.utils.Constants.ATTR_INITIAL_POS
import org.appdevforall.codeonthego.layouteditor.utils.FileUtil
import org.appdevforall.codeonthego.layouteditor.editor.convert.ConvertImportedXml
import org.appdevforall.codeonthego.layouteditor.utils.InvokeUtil.createView
import org.appdevforall.codeonthego.layouteditor.utils.InvokeUtil.invokeMethod
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.io.File
import java.io.StringReader

class XmlLayoutParser(
context: Context,
private val basePath: String? = null,
) {
val viewAttributeMap: HashMap<View, AttributeMap> = HashMap()

Expand All@@ -35,6 +38,7 @@ class XmlLayoutParser(
companion object {
const val MARKER_IS_INCLUDE = "tools:is_xml_include"
const val MARKER_IS_FRAGMENT = "tools:is_xml_fragment"
const val MARKER_IS_MERGE = "tools:is_xml_merge"
}

enum class CustomAttrs(val key: String) {
Expand DownExpand Up@@ -132,29 +136,53 @@ class XmlLayoutParser(
continue
}

"include" -> {
val placeholder = View(context)

val attrs = AttributeMap()

for (i in 0 until parser.attributeCount) {
attrs.putValue(parser.getAttributeName(i), parser.getAttributeValue(i))
}

attrs.putValue(MARKER_IS_INCLUDE, "true")

viewAttributeMap[placeholder] = attrs
listViews.add(placeholder)

parser.next()
continue
}

"merge" -> {
Log.d("XmlParser", "Encountered <merge> tag, skipping itself")
}

else -> {
"include" -> {
val layoutAttr =
XmlParserUtils.getAttribute(parser, "layout")

val includedView = loadIncludedLayout(
context,
basePath,
layoutAttr
)

val view =
includedView ?: XmlParserUtils.createIncludePlaceholder(
context,
viewAttributeMap,
MARKER_IS_INCLUDE
)

listViews.add(view)

XmlParserUtils.applyAttributes(
parser = parser,
target = view,
attributeMap = viewAttributeMap,
marker = MARKER_IS_INCLUDE,
skip = if (includedView != null) null else "layout"
)

parser.next()
continue
}

"merge" -> {

val wrapper =
XmlParserUtils.createMergeWrapper(context)

applyMergeAttributes(
parser = parser,
target = wrapper,
attributeMap = viewAttributeMap,
marker = MARKER_IS_MERGE
)

listViews.add(wrapper)
}

else -> {
val result = createView(tagName, context)
if (result is Exception) {
throw result
Expand DownExpand Up@@ -333,4 +361,64 @@ class XmlLayoutParser(
attributeMap.putValue("android:layout_marginTop", "${topDp}dp")
}
}

fun loadIncludedLayout(
context: Context,
basePath: String?,
layoutAttr: String?
): View? {
if (layoutAttr == null || basePath == null) {
Log.w(
"XmlParser",
"Skipping include. layoutAttr=$layoutAttr basePath=$basePath"
)
return null
}

val layoutName = layoutAttr.substringAfterLast("/")
val file = File(basePath, "$layoutName.xml")

if (!file.exists()) {
Log.e(
"XmlParser",
"Included file not found: ${file.absolutePath}"
)
return null
}

return try {
val xml = file.readText()

val converted =
ConvertImportedXml(xml)
.getXmlConverted(context)
?: xml

val parser = XmlLayoutParser(context, basePath)
parser.parseFromXml(converted, context)
parser.root
} catch (e: Exception) {
Log.e(
"XmlParser",
"Failed to parse include: $layoutName",
e
)
null
}
}

fun applyMergeAttributes(
parser: XmlPullParser,
target: View,
attributeMap: MutableMap<View, AttributeMap>,
marker: String
) {
val map = XmlParserUtils.extractAttributes(parser)

map.putValue(marker, "true")
map.putValue("android:layout_width", "match_parent")
map.putValue("android:layout_height", "wrap_content")

attributeMap[target] = map
}
}