Skip to content

Commit b0d66d6

Browse files
authored
Integrate image occlusion editor (#14884)
* Integrate image occlusion editor * Remove duplicate translation * Reuse translation from upstream * Wrap in withCol * Run addImageOcclusionNotetype in background * Close IO editor after adding/updating note * Do not load field/tag changes for IO note * Add 'Paste Image from Clipboard' button * Use .apply * Remove print * Use bundleOf() * Use ActivityResultContracts.GetContent() * Fix Edit Occlusions button * Fix reviewer note not being updated after editing IO note * Use undoableOp #14884 (comment) * Prefer structural equality * Avoid Hungarian notation * Add bottom padding to IO buttons container * Add TODO about image formats * Replace START animation
1 parent 11576fc commit b0d66d6

File tree

15 files changed

+319
-31
lines changed

15 files changed

+319
-31
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt

Lines changed: 136 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ package com.ichi2.anki
2020

2121
import android.annotation.SuppressLint
2222
import android.content.BroadcastReceiver
23+
import android.content.ClipData
24+
import android.content.ClipboardManager
2325
import android.content.Context
2426
import android.content.Intent
2527
import android.content.IntentFilter
@@ -49,11 +51,13 @@ import androidx.core.content.edit
4951
import androidx.core.content.res.ResourcesCompat
5052
import androidx.core.text.HtmlCompat
5153
import anki.config.ConfigKey
54+
import anki.notetypes.StockNotetype
5255
import com.google.android.material.color.MaterialColors
5356
import com.google.android.material.snackbar.Snackbar
5457
import com.ichi2.anim.ActivityTransitionAnimation
5558
import com.ichi2.anim.ActivityTransitionAnimation.Direction.*
5659
import com.ichi2.anki.CollectionManager.TR
60+
import com.ichi2.anki.CollectionManager.withCol
5761
import com.ichi2.anki.dialogs.ConfirmationDialog
5862
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
5963
import com.ichi2.anki.dialogs.DeckSelectionDialog.SelectableDeck
@@ -74,6 +78,7 @@ import com.ichi2.anki.noteeditor.FieldState.FieldChangeType
7478
import com.ichi2.anki.noteeditor.Toolbar
7579
import com.ichi2.anki.noteeditor.Toolbar.TextFormatListener
7680
import com.ichi2.anki.noteeditor.Toolbar.TextWrapper
81+
import com.ichi2.anki.pages.ImageOcclusion
7782
import com.ichi2.anki.preferences.sharedPrefs
7883
import com.ichi2.anki.receiver.SdCardReceiver
7984
import com.ichi2.anki.servicelayer.LanguageHintService
@@ -94,6 +99,7 @@ import com.ichi2.libanki.Notetypes.Companion.NOT_FOUND_NOTE_TYPE
9499
import com.ichi2.utils.*
95100
import com.ichi2.widget.WidgetStatus
96101
import org.json.JSONArray
102+
import org.json.JSONException
97103
import org.json.JSONObject
98104
import timber.log.Timber
99105
import java.util.*
@@ -133,6 +139,10 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
133139
private var mCardsButton: AppCompatButton? = null
134140
private var mNoteTypeSpinner: Spinner? = null
135141
private var mDeckSpinnerSelection: DeckSpinnerSelection? = null
142+
private var imageOcclusionButtonsContainer: LinearLayout? = null
143+
private var selectImageForOcclusionButton: Button? = null
144+
private var editOcclusionsButton: Button? = null
145+
private var pasteOcclusionImageButton: Button? = null
136146

137147
// non-null after onCollectionLoaded
138148
private var mEditorNote: Note? = null
@@ -169,6 +179,8 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
169179
private var mToggleStickyText: HashMap<Int, String?> = HashMap()
170180
private val mOnboarding = Onboarding.NoteEditor(this)
171181

182+
var clipboard: ClipboardManager? = null
183+
172184
private val requestAddLauncher = registerForActivityResult(
173185
ActivityResultContracts.StartActivityForResult(),
174186
NoteEditorActivityResultCallback {
@@ -239,6 +251,29 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
239251
}
240252
)
241253

254+
private val ioEditorLauncher = registerForActivityResult(
255+
ActivityResultContracts.GetContent()
256+
) { uri ->
257+
if (uri != null) {
258+
ImportUtils.getFileCachedCopy(this@NoteEditor, uri)?.let { path ->
259+
setupImageOcclusionEditor(path)
260+
}
261+
}
262+
}
263+
264+
private val requestIOEditorCloser = registerForActivityResult(
265+
ActivityResultContracts.StartActivityForResult(),
266+
NoteEditorActivityResultCallback { result ->
267+
if (result.resultCode != RESULT_CANCELED) {
268+
changed = true
269+
if (!addNote) {
270+
mReloadRequired = true
271+
closeNoteEditor(RESULT_UPDATED_IO_NOTE, null)
272+
}
273+
}
274+
}
275+
)
276+
242277
private inner class NoteEditorActivityResultCallback(private val callback: (result: ActivityResult) -> Unit) : ActivityResultCallback<ActivityResult> {
243278
override fun onActivityResult(result: ActivityResult) {
244279
Timber.d("onActivityResult() with result: %s", result.resultCode)
@@ -402,6 +437,17 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
402437
Timber.i("NoteEditor:: Cards button pressed. Opening template editor")
403438
showCardTemplateEditor()
404439
}
440+
imageOcclusionButtonsContainer = findViewById(R.id.ImageOcclusionButtonsLayout)
441+
editOcclusionsButton = findViewById(R.id.EditOcclusionsButton)
442+
selectImageForOcclusionButton = findViewById(R.id.SelectImageForOcclusionButton)
443+
pasteOcclusionImageButton = findViewById(R.id.PasteImageForOcclusionButton)
444+
445+
try {
446+
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
447+
} catch (e: Exception) {
448+
Timber.w(e)
449+
}
450+
405451
aedictIntent = false
406452
mCurrentEditedCard = null
407453
when (caller) {
@@ -446,6 +492,44 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
446492
else -> {}
447493
}
448494

495+
launchCatchingTask {
496+
withCol {
497+
addImageOcclusionNotetype()
498+
}
499+
}
500+
501+
if (addNote) {
502+
editOcclusionsButton?.visibility = View.GONE
503+
selectImageForOcclusionButton?.setOnClickListener {
504+
ioEditorLauncher.launch("image/*")
505+
}
506+
pasteOcclusionImageButton?.text = TR.notetypesIoPasteImageFromClipboard()
507+
pasteOcclusionImageButton?.setOnClickListener {
508+
// TODO: Support all extensions
509+
// See https://github.com/ankitects/anki/blob/6f3550464d37aee1b8b784e431cbfce8382d3ce7/rslib/src/image_occlusion/imagedata.rs#L154
510+
if (ClipboardUtil.hasImage(clipboard)) {
511+
val uri = ClipboardUtil.getImageUri(clipboard)
512+
val i = Intent().apply {
513+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
514+
clipData = ClipData.newUri(contentResolver, uri.toString(), uri)
515+
}
516+
ImportUtils.getFileCachedCopy(this, i)?.let { path ->
517+
setupImageOcclusionEditor(path)
518+
}
519+
} else {
520+
showSnackbar(TR.editingNoImageFoundOnClipboard())
521+
}
522+
}
523+
} else {
524+
selectImageForOcclusionButton?.visibility = View.GONE
525+
pasteOcclusionImageButton?.visibility = View.GONE
526+
editOcclusionsButton?.visibility = View.VISIBLE
527+
editOcclusionsButton?.text = resources.getString(R.string.edit_occlusions)
528+
editOcclusionsButton?.setOnClickListener {
529+
setupImageOcclusionEditor()
530+
}
531+
}
532+
449533
// Note type Selector
450534
mNoteTypeSpinner = findViewById(R.id.note_type_spinner)
451535
mAllModelIds = setupNoteTypeSpinner(this, mNoteTypeSpinner!!, col)
@@ -845,19 +929,21 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
845929
mCurrentEditedCard!!.did = deckId
846930
modified = true
847931
}
848-
// now load any changes to the fields from the form
849-
for (f in mEditFields!!) {
850-
modified = modified or updateField(f)
851-
}
852-
// added tag?
853-
for (t in mSelectedTags!!) {
854-
modified = modified || !mEditorNote!!.hasTag(t)
855-
}
856-
// removed tag?
857-
modified = modified || mEditorNote!!.tags.size > mSelectedTags!!.size
858-
if (modified) {
859-
mEditorNote!!.setTagsFromStr(tagsAsString(mSelectedTags!!))
860-
changed = true
932+
if (!currentNotetypeIsImageOcclusion()) {
933+
// now load any changes to the fields from the form
934+
for (f in mEditFields!!) {
935+
modified = modified or updateField(f)
936+
}
937+
// added tag?
938+
for (t in mSelectedTags!!) {
939+
modified = modified || !mEditorNote!!.hasTag(t)
940+
}
941+
// removed tag?
942+
modified = modified || mEditorNote!!.tags.size > mSelectedTags!!.size
943+
if (modified) {
944+
mEditorNote!!.setTagsFromStr(tagsAsString(mSelectedTags!!))
945+
changed = true
946+
}
861947
}
862948
closeNoteEditor()
863949
}
@@ -1224,6 +1310,14 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
12241310
val editLines = mFieldState.loadFieldEditLines(type)
12251311
mFieldsLayoutContainer!!.removeAllViews()
12261312
mCustomViewIds.clear()
1313+
if (currentNotetypeIsImageOcclusion()) {
1314+
setImageOcclusionButton()
1315+
return
1316+
} else {
1317+
imageOcclusionButtonsContainer?.visibility = View.GONE
1318+
mFieldsLayoutContainer?.visibility = View.VISIBLE
1319+
}
1320+
12271321
mEditFields = LinkedList()
12281322

12291323
var previous: FieldEditLine? = null
@@ -1897,6 +1991,33 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
18971991
val fieldsFromSelectedNote: Array<Array<String>>
18981992
get() = mEditorNote!!.items()
18991993

1994+
private fun currentNotetypeIsImageOcclusion(): Boolean {
1995+
try {
1996+
return currentlySelectedNotetype?.getInt("originalStockKind") == StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION_VALUE
1997+
} catch (j: JSONException) {
1998+
return false
1999+
}
2000+
}
2001+
2002+
private fun setImageOcclusionButton() {
2003+
imageOcclusionButtonsContainer?.visibility = View.VISIBLE
2004+
mFieldsLayoutContainer?.visibility = View.GONE
2005+
}
2006+
2007+
private fun setupImageOcclusionEditor(imagePath: String = "") {
2008+
val kind: String
2009+
val id: Long
2010+
if (addNote) {
2011+
kind = "add"
2012+
id = 0
2013+
} else {
2014+
kind = "edit"
2015+
id = mEditorNote?.id!!
2016+
}
2017+
val intent = ImageOcclusion.getIntent(this@NoteEditor, kind, id, imagePath)
2018+
requestIOEditorCloser.launch(intent)
2019+
}
2020+
19002021
// ----------------------------------------------------------------------------
19012022
// INNER CLASSES
19022023
// ----------------------------------------------------------------------------
@@ -2163,6 +2284,8 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
21632284
const val CALLER_NOTEEDITOR = 8
21642285
const val CALLER_NOTEEDITOR_INTENT_ADD = 10
21652286

2287+
const val RESULT_UPDATED_IO_NOTE = 11
2288+
21662289
// preferences keys
21672290
const val PREF_NOTE_EDITOR_SCROLL_TOOLBAR = "noteEditorScrollToolbar"
21682291
private const val PREF_NOTE_EDITOR_SHOW_TOOLBAR = "noteEditorShowToolbar"

AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiPackageImporterFragment.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import com.ichi2.anki.R
2424
import com.ichi2.anki.hideShowButtonCss
2525

2626
class AnkiPackageImporterFragment : PageFragment() {
27-
override val title: Int
28-
get() = R.string.menu_import
27+
override val title: String
28+
get() = resources.getString(R.string.menu_import)
2929
override val pageName: String
3030
get() = "import-page"
3131
override lateinit var webViewClient: PageWebViewClient

AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717

1818
package com.ichi2.anki.pages
1919

20+
import android.app.Activity
2021
import androidx.fragment.app.FragmentActivity
22+
import anki.collection.OpChanges
2123
import com.ichi2.anki.CollectionManager
2224
import com.ichi2.anki.CollectionManager.withCol
25+
import com.ichi2.anki.NoteEditor
2326
import com.ichi2.anki.importCsvRaw
2427
import com.ichi2.anki.importJsonFileRaw
28+
import com.ichi2.anki.launchCatchingTask
2529
import com.ichi2.anki.searchInBrowser
2630
import com.ichi2.libanki.*
2731
import com.ichi2.libanki.sched.computeFsrsWeightsRaw
@@ -30,6 +34,7 @@ import com.ichi2.libanki.sched.evaluateWeightsRaw
3034
import com.ichi2.libanki.stats.*
3135
import fi.iki.elonen.NanoHTTPD
3236
import kotlinx.coroutines.CoroutineScope
37+
import kotlinx.coroutines.delay
3338
import kotlinx.coroutines.runBlocking
3439
import timber.log.Timber
3540
import java.io.ByteArrayInputStream
@@ -93,6 +98,35 @@ open class AnkiServer(
9398
"setWantsAbort" -> CollectionManager.getBackend().setWantsAbortRaw(bytes)
9499
"evaluateWeights" -> withCol { evaluateWeightsRaw(bytes) }
95100
"latestProgress" -> CollectionManager.getBackend().latestProgressRaw(bytes)
101+
"getImageForOcclusion" -> withCol { getImageForOcclusionRaw(bytes) }
102+
"getImageOcclusionNote" -> withCol { getImageOcclusionNoteRaw(bytes) }
103+
"getImageForOcclusionFields" -> withCol { getImageOcclusionFieldsRaw(bytes) }
104+
"addImageOcclusionNote" -> {
105+
val data = withCol {
106+
addImageOcclusionNoteRaw(bytes)
107+
}
108+
undoableOp { OpChanges.parseFrom(data) }
109+
activity.launchCatchingTask {
110+
// Allow time for toast message to appear before closing editor
111+
delay(1000)
112+
activity.setResult(Activity.RESULT_OK)
113+
activity.finish()
114+
}
115+
data
116+
}
117+
"updateImageOcclusionNote" -> {
118+
val data = withCol {
119+
updateImageOcclusionNoteRaw(bytes)
120+
}
121+
undoableOp { OpChanges.parseFrom(data) }
122+
activity.launchCatchingTask {
123+
// Allow time for toast message to appear before closing editor
124+
delay(1000)
125+
activity.setResult(NoteEditor.RESULT_UPDATED_IO_NOTE)
126+
activity.finish()
127+
}
128+
data
129+
}
96130
"congratsInfo" -> withCol { congratsInfoRaw(bytes) }
97131
else -> { throw Exception("unhandled request: $methodName") }
98132
}

AnkiDroid/src/main/java/com/ichi2/anki/pages/CardInfo.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import com.ichi2.anki.R
2424
import com.ichi2.libanki.CardId
2525

2626
class CardInfo : PageFragment() {
27-
override val title = R.string.card_info_title
27+
override val title: String
28+
get() = resources.getString(R.string.card_info_title)
29+
2830
override val pageName = "card-info"
2931
override lateinit var webViewClient: PageWebViewClient
3032
override var webChromeClient = PageChromeClient()

AnkiDroid/src/main/java/com/ichi2/anki/pages/CongratsPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import android.content.Context
1919
import android.content.Intent
2020

2121
class CongratsPage : PageFragment() {
22-
override val title: Int? = null
22+
override val title: String = ""
2323
override val pageName = "congrats"
2424
override var webViewClient: PageWebViewClient = PageWebViewClient()
2525
override var webChromeClient: PageChromeClient = PageChromeClient()

AnkiDroid/src/main/java/com/ichi2/anki/pages/CsvImporter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import com.ichi2.anki.R
2525
* Anki page used to import text/csv files
2626
*/
2727
class CsvImporter : PageFragment() {
28-
override val title = R.string.menu_import
28+
override val title: String
29+
get() = resources.getString(R.string.menu_import)
30+
2931
override val pageName = "import-csv"
3032
override lateinit var webViewClient: PageWebViewClient
3133
override var webChromeClient = PageChromeClient()

AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import kotlinx.coroutines.Dispatchers
2929
import kotlinx.coroutines.withContext
3030

3131
class DeckOptions : PageFragment() {
32-
override val title = R.string.menu__deck_options
32+
override val title: String
33+
get() = resources.getString(R.string.menu__deck_options)
3334
override val pageName = "deck-options"
3435
override lateinit var webViewClient: PageWebViewClient
3536
override var webChromeClient = PageChromeClient()

0 commit comments

Comments
 (0)