1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.text.input.internal.undo
18 
19 import androidx.compose.foundation.internal.checkPrecondition
20 import androidx.compose.foundation.internal.requirePrecondition
21 import androidx.compose.runtime.saveable.Saver
22 import androidx.compose.runtime.saveable.SaverScope
23 import androidx.compose.runtime.snapshots.SnapshotStateList
24 import androidx.compose.ui.util.fastForEach
25 import kotlin.collections.removeFirst as removeFirstKt
26 import kotlin.collections.removeLast as removeLastKt
27 
28 /**
29  * A generic purpose undo/redo stack manager.
30  *
31  * @param initialUndoStack Previous undo stack if this manager is being restored from a saved state.
32  * @param initialRedoStack Previous redo stack if this manager is being restored from a saved state.
33  * @param capacity Maximum number of elements that can be hosted by this UndoManager. Total element
34  *   count is the sum of undo and redo stack sizes.
35  */
36 internal class UndoManager<T>(
37     initialUndoStack: List<T> = emptyList(),
38     initialRedoStack: List<T> = emptyList(),
39     private val capacity: Int = 100
40 ) {
41 
<lambda>null42     private var undoStack = SnapshotStateList<T>().apply { addAll(initialUndoStack) }
<lambda>null43     private var redoStack = SnapshotStateList<T>().apply { addAll(initialRedoStack) }
44 
45     internal val canUndo: Boolean
46         get() = undoStack.isNotEmpty()
47 
48     internal val canRedo: Boolean
49         get() = redoStack.isNotEmpty()
50 
51     val size: Int
52         get() = undoStack.size + redoStack.size
53 
54     init {
<lambda>null55         requirePrecondition(capacity >= 0) { "Capacity must be a positive integer" }
<lambda>null56         requirePrecondition(size <= capacity) {
57             "Initial list of undo and redo operations have a size greater than the given capacity."
58         }
59     }
60 
recordnull61     fun record(undoableAction: T) {
62         // First clear the redo stack.
63         redoStack.clear()
64 
65         while (size > capacity - 1) { // leave room for the immediate `add`
66             undoStack.removeFirstKt()
67         }
68         undoStack.add(undoableAction)
69     }
70 
71     /**
72      * Request undo.
73      *
74      * This method returns the item that was on top of the undo stack. By the time this function
75      * returns, the given item has already been carried to the redo stack.
76      */
undonull77     fun undo(): T {
78         checkPrecondition(canUndo) {
79             "It's an error to call undo while there is nothing to undo. " +
80                 "Please first check `canUndo` value before calling the `undo` function."
81         }
82 
83         val topOperation = undoStack.removeLastKt()
84 
85         redoStack.add(topOperation)
86         return topOperation
87     }
88 
89     /**
90      * Request redo.
91      *
92      * This method returns the item that was on top of the redo stack. By the time this function
93      * returns, the given item has already been carried back to the undo stack.
94      */
redonull95     fun redo(): T {
96         checkPrecondition(canRedo) {
97             "It's an error to call redo while there is nothing to redo. " +
98                 "Please first check `canRedo` value before calling the `redo` function."
99         }
100 
101         val topOperation = redoStack.removeLastKt()
102 
103         undoStack.add(topOperation)
104         return topOperation
105     }
106 
clearHistorynull107     fun clearHistory() {
108         undoStack.clear()
109         redoStack.clear()
110     }
111 
112     companion object {
113 
114         /**
115          * Saver factory for a generic [UndoManager].
116          *
117          * @param itemSaver Since [UndoManager] is defined as a generic class, a specific item saver
118          *   is required to _serialize_ each individual item in undo and redo stacks.
119          */
createSavernull120         inline fun <reified T> createSaver(itemSaver: Saver<T, Any>) =
121             object : Saver<UndoManager<T>, Any> {
122                 /**
123                  * Saves the contents of given [value] to a list.
124                  *
125                  * List's structure is
126                  * - Capacity
127                  * - n; Number of items in undo stack
128                  * - m; Number of items in redo stack
129                  * - n items in order from undo stack
130                  * - m items in order from redo stack
131                  */
132                 override fun SaverScope.save(value: UndoManager<T>): Any = buildList {
133                     add(value.capacity)
134                     add(value.undoStack.size)
135                     add(value.redoStack.size)
136                     value.undoStack.fastForEach { with(itemSaver) { add(save(it)) } }
137                     value.redoStack.fastForEach { with(itemSaver) { add(save(it)) } }
138                 }
139 
140                 @Suppress("UNCHECKED_CAST")
141                 override fun restore(value: Any): UndoManager<T> {
142                     val list = value as List<Any>
143                     val (capacity, undoSize, redoSize) = (list as List<Int>)
144                     var i = 3
145                     val undoStackItems = buildList {
146                         while (i < undoSize + 3) {
147                             add(itemSaver.restore(list[i])!!)
148                             i++
149                         }
150                     }
151                     val redoStackItems = buildList {
152                         while (i < undoSize + redoSize + 3) {
153                             add(itemSaver.restore(list[i])!!)
154                             i++
155                         }
156                     }
157                     return UndoManager(undoStackItems, redoStackItems, capacity)
158                 }
159             }
160     }
161 }
162