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)) + } +}