diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java
index e8a9d6d31..ad04baf74 100755
--- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java
+++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java
@@ -261,8 +261,7 @@ public class EditorActivity extends IDEActivity
data.put(File.class, currentEditor.getEditor().getFile());
}
- ActionsRegistry.getInstance()
- .fillMenu(data, com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR, menu);
+ ActionsRegistry.getInstance().fillMenu(data, ActionItem.Location.EDITOR_TOOLBAR, menu);
}
@Nullable
diff --git a/app/src/main/java/com/itsaky/androidide/actions/EditorActivityAction.kt b/app/src/main/java/com/itsaky/androidide/actions/EditorActivityAction.kt
index d72ae279b..62995706d 100644
--- a/app/src/main/java/com/itsaky/androidide/actions/EditorActivityAction.kt
+++ b/app/src/main/java/com/itsaky/androidide/actions/EditorActivityAction.kt
@@ -33,7 +33,7 @@ abstract class EditorActivityAction : ActionItem {
override var requiresUIThread: Boolean = false
override fun prepare(data: ActionData) {
- if (!hasRequiredData(data, Context::class.java)) {
+ if (!data.hasRequiredData(Context::class.java)) {
markInvisible()
}
}
diff --git a/app/src/main/java/com/itsaky/androidide/actions/text/UndoAction.kt b/app/src/main/java/com/itsaky/androidide/actions/text/UndoAction.kt
index 0ba1293b5..fe6409487 100644
--- a/app/src/main/java/com/itsaky/androidide/actions/text/UndoAction.kt
+++ b/app/src/main/java/com/itsaky/androidide/actions/text/UndoAction.kt
@@ -24,11 +24,11 @@ import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
/** @author Akash Yadav */
-class UndoAction() : EditorRelatedAction() {
+class UndoAction(context: Context) : EditorRelatedAction() {
override val id: String = "editor_undo"
- constructor(context: Context) : this() {
+ init {
label = context.getString(R.string.undo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_undo)
}
diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar
index 7ec1199bb..7d3b31668 100644
Binary files a/tests/test-home/.androidide/init/model.jar and b/tests/test-home/.androidide/init/model.jar differ
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/UIDesignerActivity.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/UIDesignerActivity.kt
index 2662bbb78..fd9ec3587 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/UIDesignerActivity.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/UIDesignerActivity.kt
@@ -17,15 +17,22 @@
package com.itsaky.androidide.uidesigner
+import android.annotation.SuppressLint
+import android.content.Context
import android.os.Bundle
+import android.view.Menu
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle
+import androidx.appcompat.view.menu.MenuBuilder
import androidx.core.view.GravityCompat
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
-import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import androidx.fragment.app.Fragment
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.ActionItem.Location.UI_DESIGNER_TOOLBAR
+import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.app.BaseIDEActivity
+import com.itsaky.androidide.uidesigner.actions.clearUiDesignerActions
+import com.itsaky.androidide.uidesigner.actions.registerUiDesignerActions
import com.itsaky.androidide.uidesigner.databinding.ActivityUiDesignerBinding
import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
@@ -83,10 +90,45 @@ class UIDesignerActivity : BaseIDEActivity() {
binding!!.root.closeDrawer(GravityCompat.START)
}
}
+
+ registerUiDesignerActions(this)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ registerUiDesignerActions(this)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ clearUiDesignerActions()
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ ensureToolbarMenu(menu)
+ return true
+ }
+
+ @SuppressLint("RestrictedApi")
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ if (menu is MenuBuilder) {
+ menu.setOptionalIconsVisible(true)
+ }
+ return true
+ }
+
+ private fun ensureToolbarMenu(menu: Menu) {
+ menu.clear()
+
+ val data = ActionData()
+ data.put(Context::class.java, this)
+ data.put(Fragment::class.java, binding!!.workspace.getFragment())
+
+ ActionsRegistry.getInstance().fillMenu(data, UI_DESIGNER_TOOLBAR, menu)
+ }
}
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/RedoAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/RedoAction.kt
new file mode 100644
index 000000000..72976587a
--- /dev/null
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/RedoAction.kt
@@ -0,0 +1,49 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.uidesigner.actions
+
+import android.content.Context
+import androidx.core.content.ContextCompat
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.uidesigner.R
+
+/**
+ * Redo action for UI Designer.
+ *
+ * @author Akash Yadav
+ */
+class RedoAction(context: Context) : UiDesignerAction() {
+
+ override val id: String = "ide.uidesigner.redo"
+
+ init {
+ label = context.getString(R.string.redo)
+ icon = ContextCompat.getDrawable(context, R.drawable.ic_redo)
+ }
+
+ override fun prepare(data: ActionData) {
+ visible = true
+ enabled = data.requireWorkspace().undoManager.canRedo()
+ }
+
+ override fun execAction(data: ActionData): Any {
+ data.requireWorkspace().undoManager.redo()
+ data.requireActivity().invalidateOptionsMenu()
+ return true
+ }
+}
\ No newline at end of file
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/UiDesignerAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/UiDesignerAction.kt
new file mode 100644
index 000000000..848df7ce1
--- /dev/null
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/UiDesignerAction.kt
@@ -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 .
+ */
+
+package com.itsaky.androidide.uidesigner.actions
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.fragment.app.Fragment
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.ActionItem
+import com.itsaky.androidide.actions.ActionItem.Location
+import com.itsaky.androidide.actions.ActionItem.Location.UI_DESIGNER_TOOLBAR
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.uidesigner.UIDesignerActivity
+import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment
+
+/**
+ * Base class for actions that are shown in the UI Designer activity.
+ *
+ * @author Akash Yadav
+ */
+abstract class UiDesignerAction : ActionItem {
+
+ override var requiresUIThread: Boolean = true
+ override var location: Location = UI_DESIGNER_TOOLBAR
+ override var label: String = ""
+ override var icon: Drawable? = null
+ override var enabled: Boolean = true
+ override var visible: Boolean = true
+
+ override fun prepare(data: ActionData) {
+ if (!data.hasRequiredData(Context::class.java, Fragment::class.java)) {
+ markInvisible()
+ return
+ }
+ }
+
+ fun ActionData.requireActivity(): UIDesignerActivity {
+ return get(Context::class.java) as? UIDesignerActivity
+ ?: throw IllegalArgumentException("UIDesignerActivity instance is required in ActionData")
+ }
+
+ fun ActionData.requireWorkspace(): DesignerWorkspaceFragment {
+ return get(Fragment::class.java) as? DesignerWorkspaceFragment
+ ?: throw IllegalArgumentException(
+ "DesignerWorkspaceFragment instance is required in ActionData"
+ )
+ }
+}
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/UndoAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/UndoAction.kt
new file mode 100644
index 000000000..3a69e3ffe
--- /dev/null
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/UndoAction.kt
@@ -0,0 +1,50 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.uidesigner.actions
+
+import android.content.Context
+import androidx.core.content.ContextCompat
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.uidesigner.R
+
+/**
+ * Undo action for UI Designer.
+ *
+ * @author Akash Yadav
+ */
+class UndoAction(context: Context) : UiDesignerAction() {
+
+ override val id: String = "ide.uidesigner.undo"
+
+ init {
+ label = context.getString(R.string.undo)
+ icon = ContextCompat.getDrawable(context, R.drawable.ic_undo)
+ }
+
+ override fun prepare(data: ActionData) {
+ visible = true
+ enabled = data.requireWorkspace().undoManager.canUndo()
+ }
+
+ override fun execAction(data: ActionData): Any {
+ data.requireWorkspace().undoManager.undo()
+ data.requireActivity().invalidateOptionsMenu()
+ return true
+ }
+}
\ No newline at end of file
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/uiDesignerActions.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/uiDesignerActions.kt
new file mode 100644
index 000000000..3bb12a35e
--- /dev/null
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/actions/uiDesignerActions.kt
@@ -0,0 +1,33 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.uidesigner.actions
+
+import android.content.Context
+import com.itsaky.androidide.actions.ActionItem.Location.UI_DESIGNER_TOOLBAR
+import com.itsaky.androidide.actions.ActionsRegistry
+
+fun registerUiDesignerActions(context: Context) {
+ ActionsRegistry.getInstance().apply {
+ registerAction(UndoAction(context))
+ registerAction(RedoAction(context))
+ }
+}
+
+fun clearUiDesignerActions() {
+ ActionsRegistry.getInstance().clearActions(UI_DESIGNER_TOOLBAR)
+}
\ No newline at end of file
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/drag/WidgetDragListener.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/drag/WidgetDragListener.kt
index 49ef47024..506d245c3 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/drag/WidgetDragListener.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/drag/WidgetDragListener.kt
@@ -26,6 +26,8 @@ import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment.Comp
import com.itsaky.androidide.uidesigner.models.UiView
import com.itsaky.androidide.uidesigner.models.UiViewGroup
import com.itsaky.androidide.uidesigner.models.UiWidget
+import kotlin.math.max
+import kotlin.math.min
/**
* Listens for drag events in the given view group.
@@ -69,9 +71,10 @@ internal class WidgetDragListener(val view: UiViewGroup, private val placeholder
else -> throw IllegalArgumentException("A local state of UiWidget or IView is expected")
}
- val index = view.indexOfChild(this.placeholder)
+ var index = view.indexOfChild(this.placeholder) - 1
this.placeholder.removeFromParent()
-
+ index = min(max(0, index), view.childCount)
+
this.view.addChild(index, child)
this.view.onHighlightStateUpdated(false)
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt
index 5ea04a527..1c7817ae4 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/fragments/DesignerWorkspaceFragment.kt
@@ -27,7 +27,6 @@ import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import com.blankj.utilcode.util.SizeUtils
import com.itsaky.androidide.fragments.BaseFragment
-import com.itsaky.androidide.inflater.ILayoutInflater
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.IViewGroup.SingleOnHierarchyChangeListener
@@ -48,6 +47,10 @@ import com.itsaky.androidide.uidesigner.drawable.UiViewLayeredForeground
import com.itsaky.androidide.uidesigner.fragments.ViewInfoFragment.Companion.TAG
import com.itsaky.androidide.uidesigner.models.PlaceholderView
import com.itsaky.androidide.uidesigner.models.UiViewGroup
+import com.itsaky.androidide.uidesigner.undo.UndoManager
+import com.itsaky.androidide.uidesigner.undo.ViewAddedAction
+import com.itsaky.androidide.uidesigner.undo.ViewMovedAction
+import com.itsaky.androidide.uidesigner.undo.ViewRemovedAction
import com.itsaky.androidide.uidesigner.utils.UiLayoutInflater
import com.itsaky.androidide.uidesigner.utils.bgDesignerView
import com.itsaky.androidide.uidesigner.utils.layeredForeground
@@ -75,6 +78,9 @@ class DesignerWorkspaceFragment : BaseFragment() {
UiViewGroup(LayoutFile(File(""), ""), LinearLayout::class.qualifiedName!!, binding!!.workspace)
}
+ val undoManager: UndoManager
+ get() = viewModel.undoManager
+
private val placeholder by lazy {
val view =
View(requireContext()).apply {
@@ -98,18 +104,39 @@ class DesignerWorkspaceFragment : BaseFragment() {
)
}
- override fun beforeViewAdded(group: IViewGroup, view: IView) {
+ private fun pushAction(view: IView, parent: IViewGroup, index: Int, added: Boolean) {
+ if (view is PlaceholderView) {
+ return
+ }
+
+ val lastAction = undoManager.peekUndo()
+
+ val action =
+ if (added && lastAction is ViewRemovedAction && lastAction.child == view) {
+ undoManager.popUndo()
+ ViewMovedAction(view, lastAction.parent, parent, lastAction.index, index)
+ } else if (added) {
+ ViewAddedAction(view, parent, index)
+ } else {
+ ViewRemovedAction(view, parent, index)
+ }
+
+ undoManager.push(action)
+ requireActivity().invalidateOptionsMenu()
+ }
+
+ override fun beforeViewAdded(group: IViewGroup, view: IView, index: Int) {
animateLayoutChange()
}
- override fun beforeViewRemoved(group: IViewGroup, view: IView) {
+ override fun beforeViewRemoved(group: IViewGroup, view: IView, index: Int) {
animateLayoutChange()
}
- override fun onViewAdded(group: IViewGroup, view: IView) {
+ override fun onViewAdded(group: IViewGroup, view: IView, index: Int) {
if (!isInflating && view !is PlaceholderView) {
- // when the inflation process is in progress, this method will be called
+ // when the inflation process is in progress, setupView method will be called
// after OnInflateViewEvent
setupView(view)
}
@@ -117,12 +144,16 @@ class DesignerWorkspaceFragment : BaseFragment() {
if (workspaceView.viewGroup.childCount > 0 && viewModel.workspaceScreen == SCREEN_ERROR) {
viewModel.workspaceScreen = SCREEN_WORKSPACE
}
+
+ pushAction(view, group, index, true)
}
- override fun onViewRemoved(group: IViewGroup, view: IView) {
+ override fun onViewRemoved(group: IViewGroup, view: IView, index: Int) {
if (workspaceView.viewGroup.childCount == 0) {
viewModel.errText = getString(R.string.msg_empty_ui_layout)
}
+
+ pushAction(view, group, index, false)
}
}
@@ -170,7 +201,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
viewModel._workspaceScreen.observe(viewLifecycleOwner) { binding?.flipper?.displayedChild = it }
viewModel._errText.observe(viewLifecycleOwner) { binding?.errText?.text = it }
-
+
inflater.inflationEventListener = this.inflateListener
var hasError = false
val inflated =
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt
index e276d04c1..05511ddb9 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt
@@ -27,7 +27,6 @@ import com.itsaky.androidide.inflater.LayoutBehavior.VERTICAL
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.ViewAdapterIndex
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
-import com.itsaky.androidide.utils.ILogger
/**
* Extension of [IView] for the UI Designer.
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/IUiAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/IUiAction.kt
index 1b1310e30..98f92494e 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/IUiAction.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/IUiAction.kt
@@ -17,8 +17,6 @@
package com.itsaky.androidide.uidesigner.undo
-import android.os.Parcelable
-
/**
* An action performed in the UI Designer.
*
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/UndoManager.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/UndoManager.kt
index 4098bca2a..870746de8 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/UndoManager.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/UndoManager.kt
@@ -24,19 +24,30 @@ package com.itsaky.androidide.uidesigner.undo
*/
class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFAULT_STACK_SIZE) {
- private var stackPointer = 0
- private var stack = mutableListOf()
+ internal var enabled = true
+ private set
+
+ private var undoStack = ArrayDeque()
+ private var redoStack = ArrayDeque()
companion object {
const val DEFAULT_STACK_SIZE = 30
}
+
+ fun enable() {
+ this.enabled = true
+ }
+
+ fun disable() {
+ this.enabled = false
+ }
fun canUndo(): Boolean {
- return stackPointer > 0
+ return enabled && undoStack.isNotEmpty()
}
fun canRedo(): Boolean {
- return stackPointer < stack.size
+ return enabled && redoStack.isNotEmpty()
}
fun undo() {
@@ -44,8 +55,13 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
return
}
- stack[stackPointer - 1].undo()
- --stackPointer
+ disable()
+ val action = undoStack.removeLast()
+ action.undo()
+
+ redoStack.addLast(action)
+ trimStacks()
+ enable()
}
fun redo() {
@@ -53,8 +69,13 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
return
}
- stack[stackPointer].redo()
- ++stackPointer
+ disable()
+ val action = redoStack.removeLast()
+ action.redo()
+
+ undoStack.addLast(action)
+ trimStacks()
+ enable()
}
/**
@@ -63,30 +84,37 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
* @param action The action to push.
*/
fun push(action: IUiAction) {
- if (stack.isEmpty()) {
- stack.add(action)
- ++stackPointer
+ if (!enabled) {
return
}
- removeRedoable()
-
- stack.add(action)
- ++stackPointer
-
- trimStack()
+ redoStack.clear()
+ undoStack.addLast(action)
+ trimStacks()
}
-
- private fun removeRedoable() {
- while(stackPointer < stack.size) {
- stack.removeAt(stack.size - 1)
+
+ fun peekUndo(): IUiAction? {
+ return undoStack.lastOrNull()
+ }
+
+ fun peekRedo(): IUiAction? {
+ return redoStack.lastOrNull()
+ }
+
+ fun popUndo(): IUiAction? {
+ return undoStack.removeLastOrNull()
+ }
+
+ fun popRedo(): IUiAction? {
+ return redoStack.removeLastOrNull()
+ }
+
+ private fun trimStacks() {
+ while (undoStack.size > maxStackSize) {
+ undoStack.removeFirst()
}
- }
-
- private fun trimStack() {
- while (stackPointer > 1 && stack.size > maxStackSize) {
- stack.removeAt(0)
- --stackPointer
+ while (redoStack.size > maxStackSize) {
+ redoStack.removeFirst()
}
}
}
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAction.kt
index 49ddb5287..37e9b6b26 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAction.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAction.kt
@@ -28,8 +28,10 @@ import com.itsaky.androidide.inflater.IViewGroup
* @property index The index of [child] in [parent].
* @author Akash Yadav
*/
-abstract class ViewAction(protected val child: IView, protected val parent: IViewGroup) :
- IUiAction {
-
- protected var index: Int = parent.indexOfChild(child)
-}
+abstract class ViewAction
+@JvmOverloads
+constructor(
+ val child: IView,
+ val parent: IViewGroup,
+ var index: Int = parent.indexOfChild(child)
+) : IUiAction
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAddedAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAddedAction.kt
index 90bf0feb2..14500a0c9 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAddedAction.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewAddedAction.kt
@@ -25,13 +25,16 @@ import com.itsaky.androidide.inflater.IViewGroup
*
* @author Akash Yadav
*/
-class ViewAddedAction(view: IView, parent: IViewGroup) : ViewAction(view, parent) {
-
+class ViewAddedAction
+@JvmOverloads
+constructor(view: IView, parent: IViewGroup, index: Int = parent.indexOfChild(view)) :
+ ViewAction(view, parent, index) {
+
override fun undo() {
child.removeFromParent()
}
-
+
override fun redo() {
parent.addChild(index, child)
}
-}
\ No newline at end of file
+}
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewMovedAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewMovedAction.kt
index 05f82c4a5..a4a47060a 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewMovedAction.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewMovedAction.kt
@@ -35,11 +35,11 @@ class ViewMovedAction(
override fun undo() {
child.removeFromParent()
- fromParent.addChild(fromIndex, child)
+ fromParent.addChild(fromParent.validateIndex(fromIndex), child)
}
override fun redo() {
child.removeFromParent()
- toParent.addChild(toIndex, child)
+ toParent.addChild(toParent.validateIndex(toIndex), child)
}
}
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewRemovedAction.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewRemovedAction.kt
index 8f6a11784..f1d626e30 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewRemovedAction.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/ViewRemovedAction.kt
@@ -19,23 +19,22 @@ package com.itsaky.androidide.uidesigner.undo
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroup
+import kotlin.math.max
+import kotlin.math.min
/**
* Represents the action when the user removes a view from a view group.
*
* @author Akash Yadav
*/
-class ViewRemovedAction(child: IView, parent: IViewGroup, index: Int) : ViewAction(child, parent) {
-
- init {
- this.index = index
- }
-
+class ViewRemovedAction(child: IView, parent: IViewGroup, index: Int) :
+ ViewAction(child, parent, index) {
+
override fun undo() {
- parent.addChild(index, child)
+ parent.addChild(parent.validateIndex(index), child)
}
-
+
override fun redo() {
- parent.removeChild(child)
+ child.removeFromParent()
}
-}
\ No newline at end of file
+}
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/undoUtils.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/undoUtils.kt
new file mode 100644
index 000000000..3a52c2312
--- /dev/null
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/undo/undoUtils.kt
@@ -0,0 +1,26 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.uidesigner.undo
+
+import com.itsaky.androidide.inflater.IViewGroup
+import kotlin.math.max
+import kotlin.math.min
+
+internal fun IViewGroup.validateIndex(index: Int) : Int {
+ return min(max(0, index), childCount)
+}
\ No newline at end of file
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/WorkspaceHierarchyChangeListener.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/WorkspaceHierarchyChangeListener.kt
new file mode 100644
index 000000000..8c5f1c380
--- /dev/null
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/utils/WorkspaceHierarchyChangeListener.kt
@@ -0,0 +1,43 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.uidesigner.utils
+
+import androidx.transition.ChangeBounds
+import androidx.transition.TransitionManager
+import com.itsaky.androidide.inflater.IView
+import com.itsaky.androidide.inflater.IViewGroup
+import com.itsaky.androidide.inflater.viewGroup
+import com.itsaky.androidide.uidesigner.R
+import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment
+import com.itsaky.androidide.uidesigner.models.PlaceholderView
+import com.itsaky.androidide.uidesigner.models.UiViewGroup
+import com.itsaky.androidide.uidesigner.undo.ViewMovedAction
+import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
+
+/**
+ * Handles hierarchy changes in the UI Designer's workspace.
+ *
+ * @author Akash Yadav
+ */
+internal class WorkspaceHierarchyChangeListener(
+ private val viewModel: WorkspaceViewModel,
+ private val workspaceView: UiViewGroup
+) : IViewGroup.SingleOnHierarchyChangeListener() {
+
+
+}
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/viewmodel/WorkspaceViewModel.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/viewmodel/WorkspaceViewModel.kt
index 68067400a..f134b1c63 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/viewmodel/WorkspaceViewModel.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/viewmodel/WorkspaceViewModel.kt
@@ -21,6 +21,7 @@ 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.undo.UndoManager
import java.io.File
class WorkspaceViewModel : ViewModel() {
@@ -30,6 +31,7 @@ class WorkspaceViewModel : ViewModel() {
internal val _viewInfoScreen = MutableLiveData(SCREEN_VIEW_INFO)
internal val _view = MutableLiveData(null)
internal val _selectedAttr = MutableLiveData(null)
+ internal val _undoManager = MutableLiveData(UndoManager())
private val _file = MutableLiveData()
companion object {
@@ -38,6 +40,9 @@ class WorkspaceViewModel : ViewModel() {
const val SCREEN_VIEW_INFO = 0
const val SCREEN_VALUE_EDITOR = 1
}
+
+ val undoManager: UndoManager
+ get() = this._undoManager.value!!
var file: File
get() = _file.value!!
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt
index 53c2410dc..2f726f228 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt
@@ -110,7 +110,7 @@ interface IViewGroup : IView {
* @param group The group in which the view will be added.
* @param view The view that will be added to the given view group.
*/
- fun beforeViewAdded(group: IViewGroup, view: IView) {}
+ fun beforeViewAdded(group: IViewGroup, view: IView, index: Int) {}
/**
* Called just before the [view] is removed from the [group].
@@ -118,7 +118,7 @@ interface IViewGroup : IView {
* @param group The group from which the view will be removed.
* @param view The view that will be removed from the given view group.
*/
- fun beforeViewRemoved(group: IViewGroup, view: IView) {}
+ fun beforeViewRemoved(group: IViewGroup, view: IView, index: Int) {}
/**
* Called when a view is added.
@@ -126,7 +126,7 @@ interface IViewGroup : IView {
* @param group The view group in which the view was added.
* @param view The added view.
*/
- fun onViewAdded(group: IViewGroup, view: IView)
+ fun onViewAdded(group: IViewGroup, view: IView, index: Int)
/**
* Called when a view is removed.
@@ -134,12 +134,12 @@ interface IViewGroup : IView {
* @param group The view group from which the view was removed.
* @param view The removed view.
*/
- fun onViewRemoved(group: IViewGroup, view: IView)
+ fun onViewRemoved(group: IViewGroup, view: IView, index: Int)
}
/** Allows overriding a single method in [OnHierarchyChangeListener]. */
open class SingleOnHierarchyChangeListener : OnHierarchyChangeListener {
- override fun onViewAdded(group: IViewGroup, view: IView) {}
- override fun onViewRemoved(group: IViewGroup, view: IView) {}
+ override fun onViewAdded(group: IViewGroup, view: IView, index: Int) {}
+ override fun onViewRemoved(group: IViewGroup, view: IView, index: Int) {}
}
}
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt
index d18f213b9..c131f8c31 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt
@@ -42,20 +42,21 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
if (view.parent != null) {
throw IllegalStateException("View already has a parent")
}
- notifyBeforeViewAdded(view)
+ notifyBeforeViewAdded(view, index)
val idx = if (index < 0) childCount else index
this.children.add(idx, view)
this.view.addView(view.view, idx)
view.parent = this
- notifyOnViewAdded(view)
+ notifyOnViewAdded(view, index)
}
override fun removeChild(view: IView) {
- notifyBeforeViewRemoved(view)
+ val index = indexOfChild(view)
+ notifyBeforeViewRemoved(view, index)
this.view.removeView(view.view)
this.children.remove(view)
view.parent = null
- notifyOnViewRemoved(view)
+ notifyOnViewRemoved(view, index)
}
override fun removeChild(index: Int) {
@@ -88,20 +89,20 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
this.hierarchyChangeListeners.remove(listener)
}
- protected open fun notifyBeforeViewAdded(child: IView) {
- this.hierarchyChangeListeners.forEach { it.beforeViewAdded(this, child) }
+ protected open fun notifyBeforeViewAdded(child: IView, index: Int) {
+ this.hierarchyChangeListeners.forEach { it.beforeViewAdded(this, child, index) }
}
- protected open fun notifyBeforeViewRemoved(child: IView) {
- this.hierarchyChangeListeners.forEach { it.beforeViewRemoved(this, child) }
+ protected open fun notifyBeforeViewRemoved(child: IView, index: Int) {
+ this.hierarchyChangeListeners.forEach { it.beforeViewRemoved(this, child, index) }
}
- protected open fun notifyOnViewAdded(child: IView) {
- this.hierarchyChangeListeners.forEach { it.onViewAdded(this, child) }
+ protected open fun notifyOnViewAdded(child: IView, index: Int) {
+ this.hierarchyChangeListeners.forEach { it.onViewAdded(this, child, index) }
}
- protected open fun notifyOnViewRemoved(child: IView) {
- this.hierarchyChangeListeners.forEach { it.onViewRemoved(this, child) }
+ protected open fun notifyOnViewRemoved(child: IView, index: Int) {
+ this.hierarchyChangeListeners.forEach { it.onViewRemoved(this, child, index) }
}
override fun immutableCopy(): IViewGroup {