diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt index be4042816cea..b887b20e3a0d 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) @@ -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) @@ -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( @@ -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 @@ -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() } @@ -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( 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..4d629f1de712 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 { @@ -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 { + 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 { @@ -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)