mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: add undo redo actions in UI Designer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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.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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+33
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
+5
-2
@@ -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)
|
||||
|
||||
|
||||
+38
-7
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
package com.itsaky.androidide.uidesigner.undo
|
||||
|
||||
import android.os.Parcelable
|
||||
|
||||
/**
|
||||
* An action performed in the UI Designer.
|
||||
*
|
||||
|
||||
@@ -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<IUiAction>()
|
||||
internal var enabled = true
|
||||
private set
|
||||
|
||||
private var undoStack = ArrayDeque<IUiAction>()
|
||||
private var redoStack = ArrayDeque<IUiAction>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-10
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
+43
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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() {
|
||||
|
||||
|
||||
}
|
||||
+5
@@ -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<IView>(null)
|
||||
internal val _selectedAttr = MutableLiveData<IAttribute>(null)
|
||||
internal val _undoManager = MutableLiveData(UndoManager())
|
||||
private val _file = MutableLiveData<File>()
|
||||
|
||||
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!!
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user