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