Mark Delphi
Mark Delphi

Reputation: 1765

Undo/Redo, how to handle doOnTextChanged and doBeforeTextChanged in TextField of Jetpack Compose?

I need to implement redo/undo for TextField in Jetpack Compose. For an EditText I used this one and it worked well. However, for Jetpack Compose there is no such a listener. I would implement an own one based on that one for EditText, but I'm missing these two listener methods, which are not available for TextField:

doOnTextChanged { text, start, before, count -> }
doBeforeTextChanged { text, start, count, after -> }

In TextField is only one listener to use

onValuesChange = { }

that only string without start and count returns.

How would I achieve a redo/undo to implement for a TextField in Jetpack Compose?

Edit

This is what I did so far. Would be great to make it functionable:

class EditTextDo {

    private var mIsUndoOrRedo = false
    private val editHistory: EditHistory? = null

    fun redo() {
        val edit = editHistory?.getNext() ?: return

        // Do Redo
    }

    fun undo() {
        val edit = editHistory?.getPrevious() ?: return

        // Do Undo
    }

    fun canUndo(): Boolean {
        editHistory?.let {
            return it.position > 0
        }
        return false
    }

    fun canRedo(): Boolean {
        editHistory?.let {
            return it.position < it.history.size
        }
        return false
    }

}

class EditHistory {

    var position = 0

    private var maxHistorySize = -1

    val history = LinkedList<EditItem>()

    private fun clear() {
        position = 0
        history.clear()
    }

    fun add(item: EditItem) {
        while (history.size > position) {
            history.removeLast()
        }
        history.add(item)
        position++
        if (maxHistorySize >= 0)
            trimHistory()
    }

    fun getNext(): EditItem? {
        if (position >= history.size) {
            return null
        }
        val item = history[position]
        position++
        return item
    }

    fun getPrevious(): EditItem? {
        if (position == 0) {
            return null
        }
        position--
        return history[position]
    }

    private fun setMaxHistorySize(maxHistorySize: Int) {
        this.maxHistorySize = maxHistorySize
        if (maxHistorySize >= 0)
            trimHistory()
    }

    private fun trimHistory() {
        while (history.size > maxHistorySize) {
            history.removeFirst()
            position--
        }
        if (position < 0)
            position = 0
    }
}

data class EditItem(val start: Int, val before: CharSequence, val after: CharSequence)

Upvotes: 4

Views: 1014

Answers (1)

z.g.y
z.g.y

Reputation: 6207

This may not exactly address your post but hopefully it help. I have a simple undo/redo Textfield on my project using Queue structure to keep track of the input history, I'm not specifying history size though.

@Composable
fun TextFieldWithHistory() {

    val undoRedoState = remember { UndoRedoState() }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
    ) {
        TextField(
            value = undoRedoState.input,
            onValueChange = {
                undoRedoState.onInput(it)
            }
        )

        Button(
            onClick = {
                undoRedoState.undo()
            }) {
            Text(text = "Undo")
        }


        Button(
            onClick = {
                undoRedoState.redo()
            }) {
            Text(text = "Redo")
        }
    }
}


class UndoRedoState {

    var input by mutableStateOf(TextFieldValue(""))
    var undoHistory = ArrayDeque<TextFieldValue?>()
    var redoHistory = ArrayDeque<TextFieldValue?>()

    init {
        undoHistory.add(input)
    }

    fun onInput(value: TextFieldValue) {
        // always set the cursor at the end (selection = text length)
        val updatedValue =  value.copy(value.text, selection = TextRange(value.text.length))
        undoHistory.add(updatedValue)
        input = updatedValue
    }

    fun undo() {

        if (undoHistory.size > 1) {

            // pop the last
            val pop = undoHistory.removeLastOrNull()
            pop?.let {
                if (it.text.isNotEmpty()) {
                    redoHistory.add(it)
                }
            }

            // peek the last
            val peek = undoHistory.lastOrNull()
            peek?.let{
                input = it
            }
        }
    }

    fun redo() {
        val pop = redoHistory.removeLastOrNull()
        pop?.let {
            if (it.text.isNotEmpty()) {
                undoHistory.add(it)
                input = it
            }
        }
    }
}

Upvotes: 2

Related Questions