All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.compose.foundation.text.UndoManager.kt Maven / Gradle / Ivy

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.foundation.text

import androidx.compose.ui.text.input.TextFieldValue

internal val SNAPSHOTS_INTERVAL_MILLIS = 5000

internal expect fun timeNowMillis(): Long

/**
 * It keeps last snapshots of [TextFieldValue]. The total number of kept snapshots is limited but
 * total number of characters in them and should not be more than [maxStoredCharacters] We add a new
 * [TextFieldValue] to the chain in one of three conditions:
 * 1. Keyboard command was executed (something was pasted, word was deleted etc.)
 * 2. Before undo
 * 3. If the last "snapshot" is older than [SNAPSHOTS_INTERVAL_MILLIS]
 *
 * In any case, we are not adding [TextFieldValue] if the content is the same. If text is the same
 * but selection is changed we are not adding a new entry to the chain but update the selection for
 * the last one.
 */
internal class UndoManager(val maxStoredCharacters: Int = 100_000) {
    private class Entry(var next: Entry? = null, var value: TextFieldValue)

    private var undoStack: Entry? = null
    private var redoStack: Entry? = null
    private var storedCharacters: Int = 0
    private var lastSnapshot: Long? = null
    private var forceNextSnapshot = false

    /**
     * It gives an undo manager a chance to save a snapshot if needed because either it's time for
     * periodic snapshotting or snapshot was previously forced via [forceNextSnapshot]. It can be
     * called during every TextField recomposition.
     */
    fun snapshotIfNeeded(value: TextFieldValue, now: Long = timeNowMillis()) {
        if (forceNextSnapshot || now > (lastSnapshot ?: 0) + SNAPSHOTS_INTERVAL_MILLIS) {
            lastSnapshot = now
            makeSnapshot(value)
        }
    }

    /** It forces making a snapshot during the next [snapshotIfNeeded] call */
    fun forceNextSnapshot() {
        forceNextSnapshot = true
    }

    /** Unconditionally makes a new snapshot (if a value differs from the last one) */
    fun makeSnapshot(value: TextFieldValue) {
        forceNextSnapshot = false
        if (value == undoStack?.value) {
            return
        } else if (value.text == undoStack?.value?.text) {
            // if text is the same, but selection / composition is different we a not making a
            // new record, but update the last one
            undoStack?.value = value
            return
        }
        undoStack = Entry(value = value, next = undoStack)
        redoStack = null
        storedCharacters += value.text.length

        if (storedCharacters > maxStoredCharacters) {
            removeLastUndo()
        }
    }

    private fun removeLastUndo() {
        var entry = undoStack
        if (entry?.next == null) return
        while (entry?.next?.next != null) {
            entry = entry.next
        }
        entry?.next = null
    }

    fun undo(): TextFieldValue? {
        return undoStack?.let { undoEntry ->
            undoEntry.next?.let { nextEntry ->
                undoStack = nextEntry
                storedCharacters -= undoEntry.value.text.length
                redoStack = Entry(value = undoEntry.value, next = redoStack)
                nextEntry.value
            }
        }
    }

    fun redo(): TextFieldValue? {
        return redoStack?.let { redoEntry ->
            redoStack = redoEntry.next
            undoStack = Entry(value = redoEntry.value, next = undoStack)
            storedCharacters += redoEntry.value.text.length
            redoEntry.value
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy