From 0159157e36b16ea572ffb165183a6deb9709dc64 Mon Sep 17 00:00:00 2001
From: David Allison <62114487+david-allison@users.noreply.github.com>
Date: Wed, 21 Oct 2026 15:11:32 +0100
Subject: [PATCH] fix(note-editor): allow toolbar button cursor at extremities
Editing a toolbar button did not allow either field to be blank.
But this was useful: `
` would want the cursor to be placed afterwards.
Fixes 22660
Assisted-by: GPT-6-Astra xhigh
Assisted-by: GPT-6.1-Sol xhigh
---
.../java/com/ichi2/anki/NoteEditorFragment.kt | 12 +-
.../anki/noteeditor/NoteEditorToolbarTest.kt | 111 ++++++++++++++++++
2 files changed, 121 insertions(+), 2 deletions(-)
create mode 100644 AnkiDroid/src/test/java/com/ichi2/anki/noteeditor/NoteEditorToolbarTest.kt
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt
index 1a8f7281fe0f..d89ad1edc391 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt
@@ -2439,8 +2439,8 @@ class NoteEditorFragment :
CustomToolbarButton(
index = currentButtonIndex,
buttonText = buttonText.ifEmpty { currentButton.buttonText },
- prefix = prefix.ifEmpty { currentButton.prefix },
- suffix = suffix.ifEmpty { currentButton.suffix },
+ prefix = prefix,
+ suffix = suffix,
)
saveToolbarButtons(toolbarButtons)
@@ -2565,6 +2565,14 @@ class NoteEditorFragment :
)
}
editToolbarDialog.show()
+
+ fun updateSaveButton() {
+ editToolbarDialog.positiveButton.isEnabled = !et.text.isNullOrBlank() || !et2.text.isNullOrBlank()
+ }
+
+ et.doAfterTextChanged { updateSaveButton() }
+ et2.doAfterTextChanged { updateSaveButton() }
+ updateSaveButton()
}
private fun setNoteTypePosition() {
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/noteeditor/NoteEditorToolbarTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/noteeditor/NoteEditorToolbarTest.kt
new file mode 100644
index 000000000000..c20eb9ec81bb
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/noteeditor/NoteEditorToolbarTest.kt
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.noteeditor
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.edit
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.ichi2.anki.NoteEditorActivity
+import com.ichi2.anki.RobolectricTest
+import com.ichi2.anki.common.destinations.NoteEditorDestination
+import com.ichi2.anki.common.destinations.toIntent
+import com.ichi2.anki.common.preferences.sharedPrefs
+import com.ichi2.anki.databinding.DialogNoteEditorToolbarEditCustomItemBinding
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.GraphicsMode
+import org.robolectric.shadows.ShadowDialog
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class NoteEditorToolbarTest : RobolectricTest() {
+ @Test
+ fun `clearing the text before the cursor saves a button that inserts after the cursor`() {
+ checkEditedButton(prefix = "", suffix = "
", expectedCursor = 0)
+ }
+
+ @Test
+ fun `clearing the text after the cursor saves a button that inserts before the cursor`() {
+ checkEditedButton(prefix = "
", suffix = "", expectedCursor = 4)
+ }
+
+ @Test
+ fun `editing requires text on at least one side of the cursor`() {
+ val toolbar = createToolbar()
+ toolbar.findViewWithTag("1").performLongClick()
+ val dialog = ShadowDialog.getLatestDialog() as AlertDialog
+ val binding = dialog.binding
+ val before = binding.noteEditorToolbarBefore
+ val after = binding.noteEditorToolbarAfter
+ val save = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
+
+ assertTrue(save.isEnabled)
+ before.setText("")
+ assertTrue(save.isEnabled)
+ after.setText("")
+ assertFalse(save.isEnabled, "Both fields empty must disable Save")
+
+ after.setText("
")
+ assertTrue(save.isEnabled)
+ after.setText(" ")
+ assertFalse(save.isEnabled, "Whitespace alone must not enable Save")
+ before.setText("
")
+ assertTrue(save.isEnabled)
+ dialog.dismiss()
+ }
+
+ private fun checkEditedButton(
+ prefix: String,
+ suffix: String,
+ expectedCursor: Int,
+ ) {
+ val toolbar = createToolbar()
+ toolbar.findViewWithTag("1").performLongClick()
+ val dialog = ShadowDialog.getLatestDialog() as AlertDialog
+ val binding = dialog.binding
+ binding.noteEditorToolbarBefore.setText(prefix)
+ binding.noteEditorToolbarAfter.setText(suffix)
+ clickAlertDialogButton { getButton(AlertDialog.BUTTON_POSITIVE) }
+
+ var formatted: Toolbar.StringFormat? = null
+ toolbar.formatListener = Toolbar.TextFormatListener { formatted = it.format("") }
+ toolbar.findViewWithTag("1").performClick()
+ val result = assertNotNull(formatted)
+ assertEquals("
", result.result)
+ assertEquals(expectedCursor, result.selectionStart)
+ assertEquals(expectedCursor, result.selectionEnd)
+
+ // Reopening reads the saved button from preferences.
+ toolbar.findViewWithTag("1").performLongClick()
+ val reopened = ShadowDialog.getLatestDialog() as AlertDialog
+ val reopenedBinding = reopened.binding
+ assertEquals(prefix, reopenedBinding.noteEditorToolbarBefore.text.toString())
+ assertEquals(suffix, reopenedBinding.noteEditorToolbarAfter.text.toString())
+ reopened.dismiss()
+ }
+
+ private fun createToolbar(): Toolbar {
+ // Start with both fields populated so saving must clear an existing value.
+ targetContext.sharedPrefs().edit {
+ putStringSet(
+ "note_editor_custom_buttons",
+ CustomToolbarButton.toStringSet(arrayListOf(CustomToolbarButton(0, "BR", "", ""))),
+ )
+ }
+ val editor =
+ startRegularActivity(NoteEditorDestination.AddNote().toIntent()).getNoteEditorFragment()
+ return editor.binding.editorToolbar
+ }
+
+ private val AlertDialog.binding: DialogNoteEditorToolbarEditCustomItemBinding
+ get() {
+ val customPanel = assertNotNull(findViewById(androidx.appcompat.R.id.custom))
+ return DialogNoteEditorToolbarEditCustomItemBinding.bind(customPanel.getChildAt(0))
+ }
+}