From f447b0e07d813e5f7fcfa45e71872d499913ef4e Mon Sep 17 00:00:00 2001 From: Aryan171 <62366692+Aryan171@users.noreply.github.com> Date: Thu, 8 Jan 2026 02:33:09 +0530 Subject: [PATCH 1/3] fix(note-editor): retain note type when copying Co-authored-by: Aryan171 62366692+Aryan171@users.noreply.github.com Co-authored-by: David Allison 62114487+david-allison@users.noreply.github.com source: card type retention when copying #19384 --- .../java/com/ichi2/anki/NoteEditorFragment.kt | 12 +++++++- .../anki/noteeditor/NoteEditorLauncher.kt | 6 +++- .../java/com/ichi2/anki/utils/ext/List.kt | 28 +++++++++++++++++++ .../java/com/ichi2/anki/NoteEditorTest.kt | 21 +++++++++++++- 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/List.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt index be4042816cea..0fb9e09e8068 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt @@ -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 @@ -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) @@ -1620,7 +1628,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( @@ -3092,6 +3101,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( diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/noteeditor/NoteEditorLauncher.kt b/AnkiDroid/src/main/java/com/ichi2/anki/noteeditor/NoteEditorLauncher.kt index 29da99535877..06a27002b118 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/noteeditor/NoteEditorLauncher.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/noteeditor/NoteEditorLauncher.kt @@ -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. @@ -206,12 +208,14 @@ sealed interface NoteEditorLauncher : Destination { val deckId: DeckId, val fieldsText: String, val tags: List? = 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()) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/List.kt b/AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/List.kt new file mode 100644 index 000000000000..7ea9837fc617 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/List.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 David Allison + * + * 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 . + */ + +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 List.indexOfOrNull(value: T): Int? { + val index = indexOf(value) + return if (index >= 0) index else null +} diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt index 47fdb49993c4..9882c8348efd 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt @@ -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 { @@ -792,9 +810,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) From d9c823586b8d0d9e480e3e32339bc4e32fc8fbb9 Mon Sep 17 00:00:00 2001 From: Aryan171 <62366692+Aryan171@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:00:19 +0530 Subject: [PATCH 2/3] test(NoteEditor): Add test for note type change field restoration --- .../java/com/ichi2/anki/NoteEditorTest.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt index 9882c8348efd..4d629f1de712 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt @@ -300,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 { + 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 { From 5cf2848d52eb41f85b204a77dad95ebf5cea6c18 Mon Sep 17 00:00:00 2001 From: Aryan171 <62366692+Aryan171@users.noreply.github.com> Date: Thu, 8 Jan 2026 03:30:30 +0530 Subject: [PATCH 3/3] Retain field content when changing note type --- .../java/com/ichi2/anki/NoteEditorFragment.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt index 0fb9e09e8068..b887b20e3a0d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt @@ -1325,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) @@ -2492,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 @@ -2891,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() }