Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
import com.ichi2.anki.snackbar.SnackbarBuilder
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.ui.setupNoteTypeSpinner
import com.ichi2.anki.utils.ext.getLongOrNull
import com.ichi2.anki.utils.ext.indexOfOrNull
import com.ichi2.anki.utils.ext.sharedPrefs
import com.ichi2.anki.utils.ext.showDialogFragment
import com.ichi2.anki.utils.ext.window
Expand Down Expand Up @@ -874,6 +876,12 @@ class NoteEditorFragment :
if (addNote) {
noteTypeSpinner!!.onItemSelectedListener = SetNoteTypeListener()
requireAnkiActivity().setToolbarTitle(R.string.menu_add)

// if a specific note type id was provided in the arguments select it in the spinner
val noteTypeId: Long? = requireArguments().getLongOrNull(EXTRA_NOTE_TYPE_ID)
val noteTypeIdIndex: Int? = noteTypeId?.let { allNoteTypeIds?.indexOfOrNull(it) }
noteTypeIdIndex?.let { noteTypeSpinner?.setSelection(it) }

// set information transferred by intent
var contents: String? = null
val tags = requireArguments().getStringArray(EXTRA_TAGS)
Expand Down Expand Up @@ -1317,6 +1325,8 @@ class NoteEditorFragment :

// treat add new note and edit existing note independently
if (addNote) {
// truncate the fields to the actual size the notetype supports
editorNote!!.fields = editorNote!!.fields.take(editorNote!!.notetype.fields.length()).toMutableList()
// load all of the fields into the note
for (f in editFields!!) {
updateField(f)
Expand Down Expand Up @@ -1620,7 +1630,8 @@ class NoteEditorFragment :
}

fun copyNote() {
launchNoteEditor(NoteEditorLauncher.CopyNote(deckId, fieldsText, selectedTags)) { }
val noteTypeId = currentlySelectedNotetype?.id ?: editorNote?.notetype?.id
launchNoteEditor(NoteEditorLauncher.CopyNote(deckId, fieldsText, selectedTags, noteTypeId)) { }
}

private fun launchNoteEditor(
Expand Down Expand Up @@ -2483,7 +2494,17 @@ class NoteEditorFragment :
if (note == null || addNote) {
getColUnsafe.run {
val notetype = notetypes.current()
Note.fromNotetypeId(this@run, notetype.id)
val newNote = Note.fromNotetypeId(this@run, notetype.id)
editorNote?.let {
if (editorNote!!.fields.size > newNote.fields.size) {
newNote.fields = editorNote!!.fields.toMutableList()
} else {
editorNote!!.fields.forEachIndexed { i, field ->
newNote.fields[i] = field
}
}
}
newNote
}
} else {
note
Expand Down Expand Up @@ -2882,7 +2903,9 @@ class NoteEditorFragment :
if (!getColUnsafe.config.getBool(ConfigKey.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK)) {
deckId = getColUnsafe.defaultsForAdding().deckId
}

for (f in editFields!!) {
updateField(f)
}
refreshNoteData(FieldChangeType.changeFieldCount(shouldReplaceNewlines()))
setDuplicateFieldStyles()
}
Expand Down Expand Up @@ -3092,6 +3115,7 @@ class NoteEditorFragment :
const val RELOAD_REQUIRED_EXTRA_KEY = "reloadRequired"
const val EXTRA_IMG_OCCLUSION = "image_uri"
const val IN_CARD_BROWSER_ACTIVITY = "inCardBrowserActivity"
const val EXTRA_NOTE_TYPE_ID = "NOTE_TYPE_ID"

// calling activity
enum class NoteEditorCaller(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import com.ichi2.anki.NoteEditorFragment.Companion.NoteEditorCaller
import com.ichi2.anki.browser.CardBrowserViewModel
import com.ichi2.anki.libanki.CardId
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.libanki.NoteTypeId
import com.ichi2.anki.utils.Destination
import com.ichi2.anki.utils.ext.bundleOfNotNull

/**
* Defines various configurations for opening the NoteEditor fragment with specific data or actions.
Expand Down Expand Up @@ -206,12 +208,14 @@ sealed interface NoteEditorLauncher : Destination {
val deckId: DeckId,
val fieldsText: String,
val tags: List<String>? = null,
val noteTypeId: NoteTypeId? = null,
) : NoteEditorLauncher {
override fun toBundle(): Bundle =
bundleOf(
bundleOfNotNull(
NoteEditorFragment.EXTRA_CALLER to NoteEditorCaller.NOTEEDITOR.value,
NoteEditorFragment.EXTRA_DID to deckId,
NoteEditorFragment.EXTRA_CONTENTS to fieldsText,
noteTypeId?.let { NoteEditorFragment.EXTRA_NOTE_TYPE_ID to it },
).also { bundle ->
tags?.let { tags -> bundle.putStringArray(NoteEditorFragment.EXTRA_TAGS, tags.toTypedArray()) }
}
Expand Down
28 changes: 28 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/List.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2026 David Allison <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki.utils.ext

/**
* Returns the index of the first occurrence of the specified element in the list, or `null` if
* the specified element is not contained in the list.
*
* For lists containing more than [Int.MAX_VALUE] elements, a result of this function is unspecified.
*/
fun <T> List<T>.indexOfOrNull(value: T): Int? {
val index = indexOf(value)
return if (index >= 0) index else null
}
48 changes: 47 additions & 1 deletion AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,24 @@ class NoteEditorTest : RobolectricTest() {
assertThat("Deck ID in the new note should be the ID provided in the intent", newNoteEditor.deckId, equalTo(currentDid))
}

@Test
fun copyNoteCopiesNoteType() {
val originalNote = addBasicAndReversedNote()

// update the last selected note type so the test does not pass by accident
addBasicNote()

val editor = openNoteEditorWithArgs(NoteEditorLauncher.EditCard(originalNote.firstCard().id, DEFAULT).toBundle())
val copyNoteBundle = getCopyNoteIntent(editor)
val newNoteEditor = openNoteEditorWithArgs(copyNoteBundle)

assertThat(
"the note type of the copied note should be the same as the original",
newNoteEditor.editorNote!!.notetype.name,
equalTo(col.notetypes.basicAndReversed.name),
)
}

@Test
fun stickyFieldsAreUnchangedAfterAdd() =
runTest {
Expand Down Expand Up @@ -282,6 +300,33 @@ class NoteEditorTest : RobolectricTest() {
assertThat("newlines should be preserved, second field should be blanked", actual, contains(newFirstField, ""))
}

@Test
fun `changing note type preserves and restores field contents`() =
runTest {
val threeFieldNoteTypeName = "ThreeFieldType"
val fieldNames = arrayOf("F1", "F2", "F3")
addStandardNoteType(threeFieldNoteTypeName, fieldNames, "{{F1}}", "{{F2}}")
val threeFieldNoteType = col.notetypes.byName(threeFieldNoteTypeName)!!
val basicNoteType = col.notetypes.basic

withNoteEditorAdding {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't the bug I listed

Copy link
Member

@david-allison david-allison Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still isn't the bug I listed, could you also add a test for the bug I provided.

This test is fine if there's an additional bug while adding

Copy link
Contributor Author

@Aryan171 Aryan171 Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test checks for field text retention in the add mode by switching from 3 fields -> 2 fields -> 3 fields. I have verified that:

When switching 3 -> 2, the first two fields are preserved.

When switching back 2 -> 3, the third field is restored from memory: assertThat("Field ${i + 1} should be restored from memory", fields[i], equalTo(text))

Since the bug report mentions the final field being blanked during these steps, this test was designed to catch that exact bug. I might be missing something small in how the bug was reported, could you clarify if you're looking for a specific test case for the edit mode as well even if the failure is in the add mode only?

edit: I’ve confirmed this test passes with my fix and fails on the current codebase

noteType = threeFieldNoteType
val originalTexts = fieldNames.map { "Text for $it" }
originalTexts.forEachIndexed { i, text -> fields[i] = text }

noteType = basicNoteType

assertThat("Field 1 should be copied", fields[0], equalTo(originalTexts[0]))
assertThat("Field 2 should be copied", fields[1], equalTo(originalTexts[1]))

noteType = threeFieldNoteType

originalTexts.forEachIndexed { i, text ->
assertThat("Field ${i + 1} should be restored from memory", fields[i], equalTo(text))
}
}
}

@Test
fun `sticky fields do not impact current values`() =
runTest {
Expand Down Expand Up @@ -792,9 +837,10 @@ class NoteEditorTest : RobolectricTest() {
}

fun buildInternal(): NoteEditorFragment {
col.notetypes.setCurrent(notetype)
val noteEditor = getNoteEditorAddingNote(REVIEWER)
advanceRobolectricLooper()
noteEditor.setCurrentlySelectedNoteType(notetype.id)
advanceRobolectricLooper()
// image occlusion does not need a first field
if (this.firstField != null) {
noteEditor.setFieldValueFromUi(0, firstField)
Expand Down