feat: add undo/redo support for view attributes

This commit is contained in:
Akash Yadav
2022-12-10 22:39:18 +05:30
parent 19d9b77c0d
commit f14030e542
27 changed files with 454 additions and 55 deletions
Binary file not shown.
@@ -41,7 +41,7 @@ import com.itsaky.androidide.utils.DialogUtils
*
* @author Akash Yadav
*/
class ViewAttrListAdapter(
internal class ViewAttrListAdapter(
attributes: List<IAttribute>,
private val viewModel: WorkspaceViewModel?,
private val onDeleteAttr: (IAttribute) -> Boolean,
@@ -27,7 +27,7 @@ import com.itsaky.androidide.uidesigner.models.UiWidgetCategory
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
/** @author Akash Yadav */
class WidgetsCategoryAdapter(categories: List<UiWidgetCategory>, private val viewModel: WorkspaceViewModel) : RecyclerView.Adapter<VH>() {
internal class WidgetsCategoryAdapter(categories: List<UiWidgetCategory>, private val viewModel: WorkspaceViewModel) : RecyclerView.Adapter<VH>() {
private val categories =
categories.let {
@@ -29,7 +29,7 @@ import com.itsaky.androidide.uidesigner.drag.WidgetDragShadowBuilder
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
/** @author Akash Yadav */
class WidgetsItemAdapter(
internal class WidgetsItemAdapter(
private val widgets: List<UiWidget>,
private val viewModel : WorkspaceViewModel
) : RecyclerView.Adapter<VH>() {
@@ -81,6 +81,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
}
private val hierarchyHandler by lazy { WorkspaceViewHierarchyHandler() }
private val attrHandler by lazy { WorkspaceViewAttrHandler() }
private val inflationHandler by lazy { WorkspaceLayoutInflationHandler() }
companion object {
@@ -99,6 +100,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
): View {
this.binding = FragmentDesignerWorkspaceBinding.inflate(inflater, container, false)
hierarchyHandler.init(this)
attrHandler.init(this)
inflationHandler.init(this)
return this.binding!!.root
}
@@ -130,11 +132,13 @@ class DesignerWorkspaceFragment : BaseFragment() {
super.onDestroyView()
this.binding = null
this.hierarchyHandler.release()
this.attrHandler.release()
this.inflationHandler.release()
this.inflater.close()
}
internal fun setupView(view: IView) {
view.registerAttributeChangeListener(attrHandler)
view.view.setOnTouchListener(
WidgetTouchListener(view, requireContext()) {
viewModel.view = it
@@ -27,8 +27,10 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.itsaky.androidide.uidesigner.adapters.ViewAttrListAdapter
import com.itsaky.androidide.uidesigner.databinding.LayoutViewInfoBinding
import com.itsaky.androidide.uidesigner.databinding.LayoutViewInfoHeaderBinding
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel.Companion.SCREEN_VALUE_EDITOR
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel.Companion.SCREEN_VIEW_INFO
/**
* A [BottomSheetDialogFragment] which shows information about a clicked view.
@@ -68,6 +70,12 @@ class ViewInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel._view.observe(viewLifecycleOwner) { showViewInfo() }
viewModel._viewInfoScreen.observe(viewLifecycleOwner) {
viewModel.undoManager.enabled = it != SCREEN_VALUE_EDITOR
if (it == SCREEN_VIEW_INFO) {
viewModel.notifyAttrUpdated()
}
}
}
override fun onDestroyView() {
@@ -83,11 +91,17 @@ class ViewInfoFragment : Fragment() {
header.name.text = view.simpleName
header.desc.text = view.name
binding.attrList.adapter =
ViewAttrListAdapter(attributes = view.attributes, viewModel = viewModel, onDeleteAttr = {
view.removeAttribute(it)
true
}) {
viewModel.selectedAttr = it
ViewAttrListAdapter(
attributes = view.attributes,
viewModel = viewModel,
onDeleteAttr = {
view.removeAttribute(it)
true
}
) {
// Store a copy of the attribute so that we can check if the value of the attribute has changed in WorkspaceViewModel.notifyAttrUpdated()
//
viewModel.selectedAttr = (it as UiAttribute).copyAttr()
viewModel.viewInfoScreen = SCREEN_VALUE_EDITOR
}
@@ -17,6 +17,7 @@
package com.itsaky.androidide.uidesigner.fragments
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -49,6 +50,12 @@ class ViewInfoSheet : BottomSheetDialogFragment() {
}
}
}
override fun onDismiss(dialog: DialogInterface) {
viewModel.undoManager.enable()
viewModel.notifyAttrUpdated()
super.onDismiss(dialog)
}
override fun onCreateView(
inflater: LayoutInflater,
@@ -45,9 +45,11 @@ class WorkspaceLayoutInflationHandler : IInflateEventsListener {
val frag = this.fragment ?: return
if (event is InflationStartEvent) {
frag.isInflating = true
frag.undoManager.disable()
}
if (event is InflationFinishEvent) {
frag.isInflating = false
frag.undoManager.enable()
}
if (event is OnInflateViewEvent) {
frag.setupView(event.data)
@@ -0,0 +1,64 @@
/*
* 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.fragments
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.uidesigner.undo.AttrAddedAction
import com.itsaky.androidide.uidesigner.undo.AttrRemovedAction
import com.itsaky.androidide.uidesigner.undo.AttrUpdatedAction
/**
* Handles view attribute changes in [DesignerWorkspaceFragment].
*
* @author Akash Yadav
*/
internal class WorkspaceViewAttrHandler : IView.AttributeChangeListener {
private var fragment: DesignerWorkspaceFragment? = null
internal fun init(fragment: DesignerWorkspaceFragment) {
this.fragment = fragment
}
internal fun release() {
this.fragment = null
}
override fun onAttributeAdded(view: IView, attribute: IAttribute) {
val frag = this.fragment ?: return
frag.undoManager.push(AttrAddedAction(view = view, attr = attribute as UiAttribute))
}
override fun onAttributeRemoved(view: IView, attribute: IAttribute) {
val frag = this.fragment ?: return
frag.undoManager.push(AttrRemovedAction(view = view, attr = attribute as UiAttribute))
}
override fun onAttributeUpdated(view: IView, attribute: IAttribute, oldValue: String) {
val frag = this.fragment ?: return
frag.undoManager.push(
AttrUpdatedAction(
view = view,
attr = (attribute as UiAttribute).copyAttr(view = view) as UiAttribute,
oldValue = oldValue
)
)
}
}
@@ -28,7 +28,7 @@ import com.itsaky.androidide.inflater.internal.ViewAdapterIndex
*
* @author Akash Yadav
*/
internal data class UiAttribute
internal open class UiAttribute
@JvmOverloads
constructor(
override val namespace: INamespace = INamespace.ANDROID,
@@ -0,0 +1,29 @@
/*
* 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.undo
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
/**
* [UndoManager] action related to view attributes.
*
* @author Akash Yadav
*/
internal abstract class AttrAction(protected val view: IView, protected val attr: UiAttribute) : IUiAction
@@ -0,0 +1,37 @@
/*
* 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.undo
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
/**
* Represents the action when the user adds an attribute to an [IView].
*
* @author Akash Yadav
*/
internal class AttrAddedAction(view: IView, attr: UiAttribute) : AttrAction(view, attr) {
override fun undo() {
view.removeAttribute(attr)
}
override fun redo() {
view.addAttribute(attr, true)
}
}
@@ -0,0 +1,37 @@
/*
* 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.undo
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
/**
* Represents the action when the user removes an attribute from an [IView].
*
* @author Akash Yadav
*/
internal class AttrRemovedAction(view: IView, attr: UiAttribute) : AttrAction(view, attr) {
override fun undo() {
view.addAttribute(attr)
}
override fun redo() {
view.removeAttribute(attr)
}
}
@@ -0,0 +1,40 @@
/*
* 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.undo
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
/**
* Represents the action when the user updates an attribute in an [IView].
*
* @author Akash Yadav
*/
internal class AttrUpdatedAction(view: IView, attr: UiAttribute, private val oldValue: String) :
AttrAction(view, attr) {
override fun undo() {
// NOTE : We need to provide the view instance so that the new attribute object created will be
// an instance of UiAttribute
view.updateAttribute(attr.copyAttr(view = view, value = oldValue))
}
override fun redo() {
view.updateAttribute(attr)
}
}
@@ -25,7 +25,6 @@ package com.itsaky.androidide.uidesigner.undo
class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFAULT_STACK_SIZE) {
internal var enabled = true
private set
private var undoStack = ArrayDeque<IUiAction>()
private var redoStack = ArrayDeque<IUiAction>()
@@ -21,10 +21,13 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.uidesigner.undo.AttrUpdatedAction
import com.itsaky.androidide.uidesigner.undo.UndoManager
import com.itsaky.androidide.utils.ILogger
import java.io.File
class WorkspaceViewModel : ViewModel() {
internal class WorkspaceViewModel : ViewModel() {
internal val _drawerOpened = MutableLiveData(false)
internal val _errText = MutableLiveData("")
internal val _workspaceScreen = MutableLiveData(SCREEN_WORKSPACE)
@@ -40,7 +43,7 @@ class WorkspaceViewModel : ViewModel() {
const val SCREEN_VIEW_INFO = 0
const val SCREEN_VALUE_EDITOR = 1
}
val undoManager: UndoManager
get() = this._undoManager.value!!
@@ -80,10 +83,26 @@ class WorkspaceViewModel : ViewModel() {
set(value) {
_viewInfoScreen.value = value
}
var selectedAttr: IAttribute?
get() = this._selectedAttr.value
set(value) {
this._selectedAttr.value = value
}
fun notifyAttrUpdated() {
val attr = this.selectedAttr ?: return
val view = this.view ?: return
val undoManager = this.undoManager
val existing = view.findAttribute(attr.namespace.uri, attr.name)
if (existing !is UiAttribute || existing.value == attr.value) {
// value of the attribute is same as before
return
}
undoManager.push(AttrUpdatedAction(view, existing.copyAttr(view = view) as UiAttribute, attr.value))
this.selectedAttr = null
}
}
@@ -17,11 +17,49 @@
package com.itsaky.androidide.uidesigner
import android.content.Context
import android.view.View
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import com.itsaky.androidide.inflater.IComponentFactory.Companion.LAYOUT_INFLATER_COMPONENT_FACTORY_KEY
import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.inflater.internal.utils.ViewFactory
import com.itsaky.androidide.inflater.viewGroup
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.uidesigner.utils.UiInflaterComponentFactory
import java.io.File
import org.robolectric.Robolectric
private val activity by lazy { Robolectric.buildActivity(AppCompatActivity::class.java).get() }
fun requiresActivity(block: AppCompatActivity.() -> Unit) {
fun requiresActivity(initComponentFactory: Boolean = true, block: AppCompatActivity.() -> Unit) {
if (initComponentFactory) {
Lookup.DEFAULT.apply {
lookup(LAYOUT_INFLATER_COMPONENT_FACTORY_KEY)
?: register(LAYOUT_INFLATER_COMPONENT_FACTORY_KEY, UiInflaterComponentFactory())
}
}
activity.block()
}
}
fun Context.createLayout(parent: IViewGroup? = null): ViewGroupImpl {
return ViewGroupImpl(
LayoutFile(File(""), ""),
LinearLayout::class.qualifiedName!!,
LinearLayout(this)
)
.apply { parent?.let { applyLayoutParams(it) } }
}
fun Context.createView(parent: IViewGroup? = null): ViewImpl {
return ViewImpl(LayoutFile(File(""), ""), View::class.qualifiedName!!, View(this)).apply {
parent?.let { applyLayoutParams(it) }
}
}
fun ViewImpl.applyLayoutParams(parent: IViewGroup) {
view.layoutParams = ViewFactory.generateLayoutParams(parent.viewGroup)
}
@@ -17,14 +17,11 @@
package com.itsaky.androidide.uidesigner.undo
import android.view.View
import android.widget.LinearLayout
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.uidesigner.createView
import com.itsaky.androidide.uidesigner.createLayout
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.uidesigner.requiresActivity
import java.io.File
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -36,8 +33,8 @@ class UndoManagerTest {
@Test
fun `test single view addition undo redo`() {
requiresActivity {
val parent = ViewGroupImpl(LayoutFile(File(""), ""), "", LinearLayout(this))
val child = ViewImpl(LayoutFile(File(""), ""), "", View(this))
val parent = createLayout()
val child = createView(parent = parent)
val undoManager = UndoManager()
assertThat(undoManager.canUndo()).isFalse()
@@ -72,8 +69,8 @@ class UndoManagerTest {
@Test
fun `test single view removal undo redo`() {
requiresActivity {
val parent = ViewGroupImpl(LayoutFile(File(""), ""), "", LinearLayout(this))
val child = ViewImpl(LayoutFile(File(""), ""), "", View(this))
val parent = createLayout()
val child = createView(parent = parent)
val undoManager = UndoManager()
parent.addChild(child)
@@ -102,22 +99,21 @@ class UndoManagerTest {
assertThat(parent.childCount).isEqualTo(0)
}
}
@Test
fun `test undo redo when view moved from one parent to another`() {
requiresActivity {
val firstParent = ViewGroupImpl(LayoutFile(File(""), ""), "", LinearLayout(this))
val secondParent = ViewGroupImpl(LayoutFile(File(""), ""), "", LinearLayout(this))
val child = ViewImpl(LayoutFile(File(""), ""), "", View(this))
val firstParent = createLayout()
val secondParent = createLayout()
val child = createView(parent = firstParent)
val undoManager = UndoManager()
// view initially added to first parent
firstParent.addChild(child)
assertThat(firstParent.childCount).isEqualTo(1)
assertThat(secondParent.childCount).isEqualTo(0)
assertThat(firstParent[0]).isEqualTo(child)
// view moved to second parent
child.removeFromParent()
secondParent.addChild(child)
@@ -127,7 +123,7 @@ class UndoManagerTest {
assertThat(firstParent.childCount).isEqualTo(0)
assertThat(secondParent.childCount).isEqualTo(1)
assertThat(secondParent[0]).isEqualTo(child)
// undo the last action
// this should move child from secondParent to firstParent
undoManager.undo()
@@ -136,7 +132,7 @@ class UndoManagerTest {
assertThat(firstParent.childCount).isEqualTo(1)
assertThat(secondParent.childCount).isEqualTo(0)
assertThat(firstParent[0]).isEqualTo(child)
// redo the last action
// this should move child from firstParent to secondParent
undoManager.redo()
@@ -151,8 +147,8 @@ class UndoManagerTest {
@Test
fun `test actions not redoable after new action push`() {
requiresActivity {
val parent = ViewGroupImpl(LayoutFile(File(""), ""), "", LinearLayout(this))
val child = ViewImpl(LayoutFile(File(""), ""), "", View(this))
val parent = createLayout()
val child = createView(parent = parent)
val undoManager = UndoManager()
// --------------------------------------------------------
@@ -180,9 +176,9 @@ class UndoManagerTest {
@Test
fun `test multiple view undo redo`() {
requiresActivity {
val parent = ViewGroupImpl(LayoutFile(File(""), ""), "", LinearLayout(this))
val firstChild = ViewImpl(LayoutFile(File(""), ""), "", View(this))
val secondChild = ViewImpl(LayoutFile(File(""), ""), "", View(this))
val parent = createLayout()
val firstChild = createView(parent = parent)
val secondChild = createView(parent = parent)
val undoManager = UndoManager()
assertThat(undoManager.canUndo()).isFalse()
@@ -233,16 +229,16 @@ class UndoManagerTest {
@Test
fun `test indexed child addition and removal`() {
requiresActivity {
val parent = ViewGroupImpl(LayoutFile(File(""), ""), "", LinearLayout(this))
val parent = createLayout()
val undoManager = UndoManager()
for (i in 1..5) {
parent.addChild(ViewImpl(LayoutFile(File(""), ""), "", View(this)))
parent.addChild(createView(parent = parent))
}
assertThat(parent.childCount).isEqualTo(5)
val child = ViewImpl(LayoutFile(File(""), ""), "", View(this))
val child = createView(parent = parent)
parent.addChild(3, child)
undoManager.push(ViewAddedAction(child, parent))
@@ -267,4 +263,106 @@ class UndoManagerTest {
assertThat(parent[3]).isEqualTo(child)
}
}
@Test
fun `test undo and redo for simple view attr addition`() {
requiresActivity {
val child = createView(parent = createLayout())
val attr = UiAttribute(name = "layout_height", value = "match_parent")
val undoManager = UndoManager()
child.addAttribute(attr)
undoManager.push(AttrAddedAction(child, attr))
assertThat(child.attributes).hasSize(1)
assertThat(undoManager.canUndo()).isTrue()
assertThat(undoManager.canRedo()).isFalse()
undoManager.undo()
assertThat(child.attributes).hasSize(0)
assertThat(undoManager.canUndo()).isFalse()
assertThat(undoManager.canRedo()).isTrue()
undoManager.redo()
assertThat(child.attributes).hasSize(1)
assertThat(undoManager.canUndo()).isTrue()
assertThat(undoManager.canRedo()).isFalse()
}
}
@Test
fun `test undo and redo for simple view attr removal`() {
requiresActivity {
val child = createView(parent = createLayout())
val attr = UiAttribute(name = "layout_height", value = "match_parent")
val undoManager = UndoManager()
undoManager.push(AttrRemovedAction(child, attr))
assertThat(child.attributes).hasSize(0)
assertThat(undoManager.canUndo()).isTrue()
assertThat(undoManager.canRedo()).isFalse()
undoManager.undo()
assertThat(child.attributes).hasSize(1)
assertThat(undoManager.canUndo()).isFalse()
assertThat(undoManager.canRedo()).isTrue()
undoManager.redo()
assertThat(child.attributes).hasSize(0)
assertThat(undoManager.canUndo()).isTrue()
assertThat(undoManager.canRedo()).isFalse()
}
}
@Test
fun `test undo and redo for simple view attr update`() {
requiresActivity {
val child = createView(parent = createLayout())
val attr = UiAttribute(name = "layout_height", value = "match_parent").immutable()
val undoManager = UndoManager()
child.addAttribute(UiAttribute(attr))
assertThat(child.attributes).hasSize(1)
assertThat(undoManager.canUndo()).isFalse()
assertThat(undoManager.canRedo()).isFalse()
child.findAttribute(attr.namespace.uri, attr.name).apply {
assertThat(this).isNotNull()
assertThat(this!!.value).isEqualTo("match_parent")
}
val copy = UiAttribute(attr).apply {
value = "wrap_content"
}
child.updateAttribute(copy)
undoManager.push(AttrUpdatedAction(child, copy, attr.value))
assertThat(child.attributes).hasSize(1)
assertThat(undoManager.canUndo()).isTrue()
assertThat(undoManager.canRedo()).isFalse()
child.findAttribute(attr.namespace.uri, attr.name).apply {
assertThat(this).isNotNull()
assertThat(this!!.value).isEqualTo("wrap_content")
}
undoManager.undo()
assertThat(child.attributes).hasSize(1)
assertThat(undoManager.canUndo()).isFalse()
assertThat(undoManager.canRedo()).isTrue()
child.findAttribute(attr.namespace.uri, attr.name).apply {
assertThat(this).isNotNull()
assertThat(this!!.value).isEqualTo("match_parent")
}
undoManager.redo()
assertThat(child.attributes).hasSize(1)
assertThat(undoManager.canUndo()).isTrue()
assertThat(undoManager.canRedo()).isFalse()
child.findAttribute(attr.namespace.uri, attr.name).apply {
assertThat(this).isNotNull()
assertThat(this!!.value).isEqualTo("wrap_content")
}
}
}
}
@@ -21,7 +21,7 @@ import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.INamespace
import com.itsaky.androidide.inflater.INamespace.Companion.ANDROID
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.newAttribute
import com.itsaky.androidide.inflater.utils.newAttribute
open class AttributeImpl
@JvmOverloads
@@ -41,7 +41,7 @@ constructor(
return newAttribute(view = view, namespace = namespace, name = name, value = value)
}
internal fun immutableCopy(): IAttribute {
fun immutable(): IAttribute {
return ImmutableAttributeImpl(this)
}
@@ -24,7 +24,7 @@ import com.itsaky.androidide.inflater.IAttribute
*
* @author Akash Yadav
*/
internal class ImmutableAttributeImpl(private val src: AttributeImpl) : IAttribute by src {
class ImmutableAttributeImpl(private val src: AttributeImpl) : IAttribute by src {
override var value: String
get() = src.value
set(value) {
@@ -28,7 +28,7 @@ import com.itsaky.androidide.inflater.IViewGroup
*
* @author Akash Yadav
*/
internal class ImmutableViewImpl(private val src: ViewImpl) : IView by src {
class ImmutableViewImpl(private val src: ViewImpl) : IView by src {
override fun addAttribute(attribute: IAttribute, update: Boolean) {
throw UnsupportedOperationException("Immutable!")
@@ -105,7 +105,7 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
this.hierarchyChangeListeners.forEach { it.onViewRemoved(this, child, index) }
}
override fun immutableCopy(): IViewGroup {
override fun immutable(): IViewGroup {
return ImmutableViewGroupImpl(this)
}
}
@@ -94,6 +94,10 @@ constructor(
}
override fun registerAttributeChangeListener(listener: AttributeChangeListener) {
if (this.attrChangeListeners.contains(listener)) {
log.warn("Attempt to register an already-registered AttributeChangeListener")
return
}
this.attrChangeListeners.add(listener)
}
@@ -122,6 +126,10 @@ constructor(
fun findNamespaceByUri(uri: String): INamespace? {
return this.namespaceDecls[uri] ?: (parent as? ViewImpl)?.findNamespaceByUri(uri)
}
open fun immutable() : IView {
return ImmutableViewImpl(this)
}
protected open fun hasAttribute(attribute: IAttribute): Boolean {
return hasAttribute(attribute.namespace.uri, attribute.name)
@@ -141,10 +149,6 @@ constructor(
builder.append("\n")
}
internal open fun immutableCopy() : IView {
return ImmutableViewImpl(this)
}
private fun updateAttributeInternal(attribute: IAttribute, notify: Boolean = true) {
val existing =
findAttribute(attribute)
@@ -30,7 +30,7 @@ import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.INamespace
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.newAttribute
import com.itsaky.androidide.inflater.utils.newAttribute
import java.util.regex.Pattern
/**
@@ -38,7 +38,7 @@ import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewAdapter
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.inflater.newAttribute
import com.itsaky.androidide.inflater.utils.newAttribute
/**
* Attribute adapter for [View].
@@ -15,14 +15,20 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.inflater
package com.itsaky.androidide.inflater.utils
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IComponentFactory
import com.itsaky.androidide.inflater.ILayoutInflater
import com.itsaky.androidide.inflater.ILayoutInflater.Companion
import com.itsaky.androidide.inflater.INamespace
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.internal.AttributeImpl
import com.itsaky.androidide.lookup.Lookup
/** Get the [ILayoutInflater] registered with [Lookup]. */
fun lookupLayoutInflater(): ILayoutInflater? {
return Lookup.DEFAULT.lookup(ILayoutInflater.LOOKUP_KEY)
return Lookup.DEFAULT.lookup(Companion.LOOKUP_KEY)
}
/** Get the [IComponentFactory] registered with [Lookup]. */
@@ -24,6 +24,7 @@ import com.itsaky.androidide.inflater.internal.LayoutInflaterImpl
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.inflater.internal.utils.IDTable
import com.itsaky.androidide.inflater.utils.newAttribute
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
import org.junit.Test