1 /* <lambda>null2 * Copyright 2021 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 18 19 import androidx.compose.ui.text.input.TextFieldValue 20 21 internal val SNAPSHOTS_INTERVAL_MILLIS = 5000 22 23 internal expect fun timeNowMillis(): Long 24 25 /** 26 * It keeps last snapshots of [TextFieldValue]. The total number of kept snapshots is limited but 27 * total number of characters in them and should not be more than [maxStoredCharacters] We add a new 28 * [TextFieldValue] to the chain in one of three conditions: 29 * 1. Keyboard command was executed (something was pasted, word was deleted etc.) 30 * 2. Before undo 31 * 3. If the last "snapshot" is older than [SNAPSHOTS_INTERVAL_MILLIS] 32 * 33 * In any case, we are not adding [TextFieldValue] if the content is the same. If text is the same 34 * but selection is changed we are not adding a new entry to the chain but update the selection for 35 * the last one. 36 */ 37 internal class UndoManager(val maxStoredCharacters: Int = 100_000) { 38 private class Entry(var next: Entry? = null, var value: TextFieldValue) 39 40 private var undoStack: Entry? = null 41 private var redoStack: Entry? = null 42 private var storedCharacters: Int = 0 43 private var lastSnapshot: Long? = null 44 private var forceNextSnapshot = false 45 46 /** 47 * It gives an undo manager a chance to save a snapshot if needed because either it's time for 48 * periodic snapshotting or snapshot was previously forced via [forceNextSnapshot]. It can be 49 * called during every TextField recomposition. 50 */ 51 fun snapshotIfNeeded(value: TextFieldValue, now: Long = timeNowMillis()) { 52 if (forceNextSnapshot || now > (lastSnapshot ?: 0) + SNAPSHOTS_INTERVAL_MILLIS) { 53 lastSnapshot = now 54 makeSnapshot(value) 55 } 56 } 57 58 /** It forces making a snapshot during the next [snapshotIfNeeded] call */ 59 fun forceNextSnapshot() { 60 forceNextSnapshot = true 61 } 62 63 /** Unconditionally makes a new snapshot (if a value differs from the last one) */ 64 fun makeSnapshot(value: TextFieldValue) { 65 forceNextSnapshot = false 66 if (value == undoStack?.value) { 67 return 68 } else if (value.text == undoStack?.value?.text) { 69 // if text is the same, but selection / composition is different we a not making a 70 // new record, but update the last one 71 undoStack?.value = value 72 return 73 } 74 undoStack = Entry(value = value, next = undoStack) 75 redoStack = null 76 storedCharacters += value.text.length 77 78 if (storedCharacters > maxStoredCharacters) { 79 removeLastUndo() 80 } 81 } 82 83 private fun removeLastUndo() { 84 var entry = undoStack 85 if (entry?.next == null) return 86 while (entry?.next?.next != null) { 87 entry = entry.next 88 } 89 entry?.next = null 90 } 91 92 fun undo(): TextFieldValue? { 93 return undoStack?.let { undoEntry -> 94 undoEntry.next?.let { nextEntry -> 95 undoStack = nextEntry 96 storedCharacters -= undoEntry.value.text.length 97 redoStack = Entry(value = undoEntry.value, next = redoStack) 98 nextEntry.value 99 } 100 } 101 } 102 103 fun redo(): TextFieldValue? { 104 return redoStack?.let { redoEntry -> 105 redoStack = redoEntry.next 106 undoStack = Entry(value = redoEntry.value, next = undoStack) 107 storedCharacters += redoEntry.value.text.length 108 redoEntry.value 109 } 110 } 111 } 112