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