Skip to content

Integrate image occlusion editor#14884

Merged
david-allison merged 21 commits intoankidroid:mainfrom
abdnh:image-occlusion
Dec 16, 2023
Merged

Integrate image occlusion editor#14884
david-allison merged 21 commits intoankidroid:mainfrom
abdnh:image-occlusion

Conversation

@abdnh
Copy link
Copy Markdown
Contributor

@abdnh abdnh commented Dec 6, 2023

Purpose / Description

This integrates the image occlusion editor with NoteEditor.

Fixes

Fixes #6980

Approach

The IO editor is accessed from the normal editor via a button.

image
image
image
image

How Has This Been Tested?

Manual testing in the emulator. Android 14.0, API level 34

Checklist

Please, go through these checks before submitting the PR.

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

Issues

  • Clipboard support
  • Clicking the save button in the normal button in IO edit mode triggers NullPointerException
  • IO editor should be closed when Add/Update button is clicked. The normal editor should probably also be closed in edit mode
  • Add the "Reset Image Occlusion" button
  • No way to edit the Comments field since it's not exposed in the IO editor. @dae is that because it's not a protected field?
  • The "x cards added" string counts text labels and other non-mask shapes - looks like an upstream issue

@abdnh abdnh marked this pull request as draft December 6, 2023 14:18
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 6, 2023

Message to maintainers, this PR contains strings changes.

  1. Before merging this PR, it is best to run the "Sync Translations" GitHub action, then make and merge a PR from the i18n_sync branch to get translations cleaned out.
  2. Then merge this PR, and immediately do another translation PR so the huge change made by this PR's key changes are all by themselves.

Read more about updating strings on the wiki,

else -> {}
}

col.backend.addImageOcclusionNotetype()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I assume it's fine not to call this in a background task. The computer version doesn't bother using background task for this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Technically, we probably should be using a background task for the desktop as well. If the user has a large number of notetypes + slow disk, the operation to fetch all the notetypes could potentially block for some time.

For methods that access the collection, standard practice is to write a little wrapper function in the Collection class and call that inside withCol, instead of using the backend directly. We only access the backend directly for operations that don't require/want the collection lock (such as checking progress). One problem this avoids is the collection is guaranteed not to be closed in the middle of an operation running inside withCol.

Copy link
Copy Markdown
Contributor

@dae dae left a comment

Choose a reason for hiding this comment

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

Thanks for looking into this Abdo!


class ImageOcclusion : PageFragment() {

override val title = R.string.image_occlusion
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can use TR.notetypesImageOcclusionName() to avoid having to add a duplicate string.

else -> {}
}

col.backend.addImageOcclusionNotetype()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Technically, we probably should be using a background task for the desktop as well. If the user has a large number of notetypes + slow disk, the operation to fetch all the notetypes could potentially block for some time.

For methods that access the collection, standard practice is to write a little wrapper function in the Collection class and call that inside withCol, instead of using the backend directly. We only access the backend directly for operations that don't require/want the collection lock (such as checking progress). One problem this avoids is the collection is guaranteed not to be closed in the middle of an operation running inside withCol.

"getImageOcclusionNote" -> CollectionManager.getBackend().getImageOcclusionNoteRaw(bytes)
"getImageForOcclusionFields" -> CollectionManager.getBackend().getImageOcclusionFieldsRaw(bytes)
"addImageOcclusionNote" -> CollectionManager.getBackend().addImageOcclusionNoteRaw(bytes)
"updateImageOcclusionNote" -> CollectionManager.getBackend().updateImageOcclusionNoteRaw(bytes)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These would be better implemented as wrappers inside a withCol block.

@dae
Copy link
Copy Markdown
Contributor

dae commented Dec 10, 2023

No way to edit the Comments field since it's not exposed in the IO editor. @dae is that because it's not a protected field?

Sorry, missed that comment. I think when @krmanik originally introduced this feature, we didn't have a comments field in the notetype. Presumably when it was later added, we forgot to update the mobile-only entrypoint. We should probably keep it hidden until we can transition to the desktop editor implementation, as I think the mobile-only entrypoint will fail if that field is missing, and we don't protect it. Once we've migrated all clients to the desktop editor, we'll be able to remove the protection on header/back extra as well, so users can remove them if they wish.

}
mPasteImaegOcclusionImageButton?.text = TR.notetypesIoPasteImageFromClipboard()
mPasteImaegOcclusionImageButton?.setOnClickListener {
if (ClipboardUtil.hasImage(clipboard)) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Clipboard support is not properly tested yet. I wrote this referencing similar features across the codebase. I've not managed to copy images to the clipboard for some reason (tested copying from Chrome).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just confirmed this is working. But maybe hasImage() should be extended to support all accepted types? https://github.com/ankitects/anki/blob/6f3550464d37aee1b8b784e431cbfce8382d3ce7/rslib/src/image_occlusion/imagedata.rs#L154

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can be left as a TODO if you want

"getImageForOcclusionFields" -> withCol { getImageOcclusionFieldsRaw(bytes) }
"addImageOcclusionNote" -> withCol { addImageOcclusionNoteRaw(bytes) }.also {
activity.launchCatchingTask {
// Allow time for toast message to appear before closing editor
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dae does AnkiMobile show the toast messages? If so, how is this handled there?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It 'cheats': the screen is closed immediately, and it prints something else:

showPopup(tr.importingNoteAdded(count: 1))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This may be somewhat related - a crash showing JS dialogs/confirms when AnkiPages tried to open an AlertDialog after the AnkiPages object was destroyed (if my hypothesis there is correct) #14920 - I see some other discussion in this area and I haven't read all comments so this comment should be considered as an isolated bit of context just in case, and it may not be useful

Copy link
Copy Markdown
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

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

Comments from a few days ago, things may have moved on

// nothing was changed by the note editor so just redraw the card
redrawCard()
} else if (resultCode == NoteEditor.RESULT_UPDATED_IO_NOTE) {
displayCardQuestion(true)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(question, not a request)

Is an OpChanges exposed which we can use inside an undoableOp instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both adding and updating I/O notes return OpChanges, so that should be possible, but as the calls are currently of the Raw variant, the return value would need to be decoded as OpChangesOnly.

get() = mEditorNote!!.items().map { it.requireNoNulls() }.toTypedArray()

private fun currentNotetypeIsImageOcclusion(): Boolean {
println("currentNotetypeIsImageOcclusion: ${currentlySelectedNotetype?.fieldsNames}")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Prefer Timber.d

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(if this is something you feel would be useful moving forward, and not temporary debug info that should be removed)

"getImageForOcclusionFields" -> withCol { getImageOcclusionFieldsRaw(bytes) }
"addImageOcclusionNote" -> withCol { addImageOcclusionNoteRaw(bytes) }.also {
activity.launchCatchingTask {
// Allow time for toast message to appear before closing editor
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Android toasts persist globally, as a future extension, we can move this to a native call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For context David, this current screen is a 'temporary' one: the desktop editor integrates the I/O editor directly, and this separate screen is provided just for the mobile clients. Hopefully they'll be able to switch to the desktop editor in the future, making this entrypoint obsolete.

private const val ARG_KEY_PATH = "path"

fun getIntent(context: Context, kind: String, noteOrNotetypeId: Long, imagePath: String?): Intent {
val arguments = Bundle().apply {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

bundleOf exists in Kotlin

// nothing was changed by the note editor so just redraw the card
redrawCard()
} else if (resultCode == NoteEditor.RESULT_UPDATED_IO_NOTE) {
displayCardQuestion(true)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be rebased on main, this line conflicts (parameter was removed in API changes)

)

private val requestIOEditorLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@abdnh
Copy link
Copy Markdown
Contributor Author

abdnh commented Dec 10, 2023

@dae Does AnkiMobile have the "Reset Image Occlusion" button? It probably makes more sense as part of the IO page on mobile.

@abdnh abdnh marked this pull request as ready for review December 10, 2023 12:02
@dae
Copy link
Copy Markdown
Contributor

dae commented Dec 11, 2023

It doesn't - you need to close and re-open the window if you wish to select a different image.

@krmanik
Copy link
Copy Markdown
Member

krmanik commented Dec 11, 2023

we didn't have a comments field in the notetype. Presumably when it was later added, we forgot to update the mobile-only entrypoint.

The comments field added later, the header and back extra fields are currently basic implementation and enough for most users.

@abdnh
Copy link
Copy Markdown
Contributor Author

abdnh commented Dec 11, 2023

It doesn't - you need to close and re-open the window if you wish to select a different image.

OK, will skip that then.

This PR is ready for another review if someone has the time.

@dae
Copy link
Copy Markdown
Contributor

dae commented Dec 12, 2023

This will update the interface to show the change in undo status, and avoids the need for manually refreshing the review screen:

diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
index a56e7ac178..aad2eb588c 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
@@ -297,9 +297,6 @@ abstract class AbstractFlashcardViewer :
             } else if (result.resultCode == RESULT_CANCELED && !reloadRequired) {
                 // nothing was changed by the note editor so just redraw the card
                 redrawCard()
-            } else if (result.resultCode == NoteEditor.RESULT_UPDATED_IO_NOTE) {
-                reloadWebViewContent()
-                onEditedNoteChanged()
             }
         }
     )
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
index 183867c0cc..b75925708a 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
@@ -19,6 +19,7 @@ package com.ichi2.anki.pages
 
 import android.app.Activity
 import androidx.fragment.app.FragmentActivity
+import anki.collection.OpChanges
 import com.ichi2.anki.CollectionManager
 import com.ichi2.anki.CollectionManager.withCol
 import com.ichi2.anki.NoteEditor
@@ -100,21 +101,31 @@ open class AnkiServer(
             "getImageForOcclusion" -> withCol { getImageForOcclusionRaw(bytes) }
             "getImageOcclusionNote" -> withCol { getImageOcclusionNoteRaw(bytes) }
             "getImageForOcclusionFields" -> withCol { getImageOcclusionFieldsRaw(bytes) }
-            "addImageOcclusionNote" -> withCol { addImageOcclusionNoteRaw(bytes) }.also {
+            "addImageOcclusionNote" -> {
+                val data = withCol {
+                    addImageOcclusionNoteRaw(bytes)
+                }
+                undoableOp { OpChanges.parseFrom(data) }
                 activity.launchCatchingTask {
                     // Allow time for toast message to appear before closing editor
                     delay(1000)
                     activity.setResult(Activity.RESULT_OK)
                     activity.finish()
                 }
+                data
             }
-            "updateImageOcclusionNote" -> withCol { updateImageOcclusionNoteRaw(bytes) }.also {
+            "updateImageOcclusionNote" -> {
+                val data = withCol {
+                    updateImageOcclusionNoteRaw(bytes)
+                }
+                undoableOp { OpChanges.parseFrom(data) }
                 activity.launchCatchingTask {
                     // Allow time for toast message to appear before closing editor
                     delay(1000)
                     activity.setResult(NoteEditor.RESULT_UPDATED_IO_NOTE)
                     activity.finish()
                 }
+                data
             }
             "congratsInfo" -> withCol { congratsInfoRaw(bytes) }
             else -> { throw Exception("unhandled request: $methodName") }

Apart from that, adding and editing seems to work well. The other AD devs are more qualified to comment on the Android-related changes here, but from my backend-focused perspective, it looks good. For your first non-documentation PR, you've done well to get up to speed with Android development so quickly!

@abdnh
Copy link
Copy Markdown
Contributor Author

abdnh commented Dec 12, 2023

For your first non-documentation PR, you've done well to get up to speed with Android development so quickly!

Thanks. I worked briefly with Android and Kotlin on a project a couple of years ago.

@david-allison david-allison added the Review High Priority Request for high priority review label Dec 13, 2023
Copy link
Copy Markdown
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

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

Looks good. Only optional nitpicks

Additional feedback: this could do with maybe 8dp spacing between the button and the 'tags' picker

Copy link
Copy Markdown
Member

@BrayanDSO BrayanDSO left a comment

Choose a reason for hiding this comment

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

LGTM. Really great having IO in AnkIDroid

@BrayanDSO BrayanDSO added Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) squash-merge A squash & force push is required. The PR author may do this to speed up the merge process. labels Dec 16, 2023
@david-allison
Copy link
Copy Markdown
Member

david-allison commented Dec 16, 2023

Pending Strings & squash-merge A squash & force push is required. The PR author may do this to speed up the merge process.

@david-allison david-allison merged commit b0d66d6 into ankidroid:main Dec 16, 2023
@github-actions github-actions bot removed Review High Priority Request for high priority review squash-merge A squash & force push is required. The PR author may do this to speed up the merge process. Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) labels Dec 16, 2023
@github-actions github-actions bot added this to the 2.17 release milestone Dec 16, 2023
@david-allison
Copy link
Copy Markdown
Member

Really glad to see this one in, thank you 🙏🙏

@mikehardy
Copy link
Copy Markdown
Member

This is ridiculous, we have image occlusion in AnkiDroid !?!?! @abdnh - champion work 👑 thank you

I literally never thought I'd see the day

@abdnh abdnh deleted the image-occlusion branch December 16, 2023 23:51
bkzhn pushed a commit to bkzhn/Anki-Android that referenced this pull request Jan 14, 2024
* 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

ankidroid#14884 (comment)

* Prefer structural equality

* Avoid Hungarian notation

* Add bottom padding to IO buttons container

* Add TODO about image formats

* Replace START animation
@Mohamed7AbdELhalim

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

Hi there @abdnh! This is the OpenCollective Notice for PRs merged from 2023-12-01 through 2023-12-31

If you are interested in compensation for this work, the process with details is here:

https://github.com/ankidroid/Anki-Android/wiki/OpenCollective-Payment-Process#how-to-get-paid

We only post one comment per person per month to avoid spamming you, regardless of the number of PRs merged, but this note applies to all PRs merged for this month

Please understand that our monthly budget is never guaranteed to cover all claims - the cap on payments-per-person may be lower, but we try to make our process as fair and transparent as possible, we just need your understanding.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image Occlusion in AnkiDroid!

7 participants