mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: add undo/redo support for view attributes
This commit is contained in:
Binary file not shown.
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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>() {
|
||||
|
||||
+4
@@ -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
|
||||
|
||||
+19
-5
@@ -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,
|
||||
|
||||
+2
@@ -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)
|
||||
|
||||
+64
@@ -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>()
|
||||
|
||||
+22
-3
@@ -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)
|
||||
}
|
||||
|
||||
+125
-27
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user