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 {