From be8f93b6b838e401c1548a5059137c9552adaf4a Mon Sep 17 00:00:00 2001
From: krmanik <12841290+krmanik@users.noreply.github.com>
Date: Fri, 9 Jun 2023 13:13:43 +0800
Subject: [PATCH] setup mask editor in note editor - add image on mask button
click (only one time) - show hide add button for io on notetype change
---
ftl/core/notetypes.ftl | 2 +
pylib/anki/collection.py | 4 +
pylib/anki/models.py | 1 +
qt/aqt/addcards.py | 42 +-
qt/aqt/editor.py | 63 +++
ts/editor/NoteEditor.svelte | 358 +++++++++++-------
ts/editor/base.ts | 2 +
ts/editor/editor-toolbar/EditorToolbar.svelte | 5 +
.../ImageOcclusionButton.svelte | 57 +++
ts/editor/tsconfig.json | 3 +-
ts/image-occlusion/Notes.svelte | 2 +-
ts/image-occlusion/TopToolbar.svelte | 3 +-
ts/image-occlusion/store.ts | 2 +
13 files changed, 407 insertions(+), 137 deletions(-)
create mode 100644 ts/editor/editor-toolbar/ImageOcclusionButton.svelte
diff --git a/ftl/core/notetypes.ftl b/ftl/core/notetypes.ftl
index 59036f9f1f8..1e92bcda4c8 100644
--- a/ftl/core/notetypes.ftl
+++ b/ftl/core/notetypes.ftl
@@ -51,3 +51,5 @@ notetypes-error-generating-cloze = An error occurred when generating an image oc
notetypes-error-getting-imagecloze = An error occurred while fetching an image occlusion note
notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date?
notetype-error-no-image-to-show = No image to show.
+notetype-create-edit-io-message = Tap the two squares to create or edit an occlusion.
+notetype-io-switch-type = Tap 'Type: Image Occlusion' to switch to normal text/media cards.
diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index 4b924fe176c..da9bf813417 100644
--- a/pylib/anki/collection.py
+++ b/pylib/anki/collection.py
@@ -466,6 +466,10 @@ def import_json_string(self, json: str) -> ImportLogWithChanges:
def get_image_for_occlusion(self, path: str | None) -> GetImageForOcclusionResponse:
return self._backend.get_image_for_occlusion(path=path)
+ def add_image_occlusion_notetype(self) -> None:
+ "Add notetype if missing."
+ self._backend.add_image_occlusion_notetype()
+
def add_image_occlusion_note(
self,
notetype_id: int,
diff --git a/pylib/anki/models.py b/pylib/anki/models.py
index 7fe27dba379..fe9c9b5f27f 100644
--- a/pylib/anki/models.py
+++ b/pylib/anki/models.py
@@ -27,6 +27,7 @@
NotetypeNames = notetypes_pb2.NotetypeNames
ChangeNotetypeInfo = notetypes_pb2.ChangeNotetypeInfo
ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest
+StockNotetype = notetypes_pb2.StockNotetype
# legacy types
NotetypeDict = dict[str, Any]
diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py
index ef8a1150060..96e4ec5903b 100644
--- a/qt/aqt/addcards.py
+++ b/qt/aqt/addcards.py
@@ -10,7 +10,7 @@
from anki._legacy import deprecated
from anki.collection import OpChanges, SearchNode
from anki.decks import DeckId
-from anki.models import NotetypeId
+from anki.models import NotetypeId, StockNotetype
from anki.notes import Note, NoteFieldsCheckResult, NoteId
from anki.utils import html_to_text_line, is_mac
from aqt import AnkiQt, gui_hooks
@@ -48,13 +48,16 @@ def __init__(self, mw: AnkiQt) -> None:
self.setMinimumWidth(400)
self.setup_choosers()
self.setupEditor()
- self.setupButtons()
add_close_shortcut(self)
self._load_new_note()
+ self.setupButtons()
self.history: list[NoteId] = []
self._last_added_note: Optional[Note] = None
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
restoreGeom(self, "add")
+ self.col.add_image_occlusion_notetype()
+ # hide io buttons for note type other than image occlusion
+ self.show_hide_add_buttons()
gui_hooks.add_cards_did_init(self)
self.show()
@@ -112,6 +115,17 @@ def setupButtons(self) -> None:
self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self)
qconnect(self.compat_add_shorcut.activated, self.addButton.click)
self.addButton.setToolTip(shortcut(tr.adding_add_shortcut_ctrlandenter()))
+
+ # add io hide all button
+ self.addButtonHideAll = bb.addButton(tr.notetypes_hide_all_guess_one(), ar)
+ qconnect(self.addButtonHideAll.clicked, self.add_io_hide_all_note)
+ self.addButtonHideAll.setShortcut(QKeySequence("Ctrl+Return+A"))
+ self.addButtonHideAll.setVisible(False)
+ # add io hide one button
+ self.addButtonHideOne = bb.addButton(tr.notetypes_hide_one_guess_one(), ar)
+ qconnect(self.addButtonHideOne.clicked, self.add_io_hide_one_note)
+ self.addButtonHideOne.setShortcut(QKeySequence("Ctrl+Return+O"))
+
# close
self.closeButton = QPushButton(tr.actions_close())
self.closeButton.setAutoDefault(False)
@@ -133,6 +147,19 @@ def setupButtons(self) -> None:
b.setEnabled(False)
self.historyButton = b
+ def show_hide_add_buttons(self) -> None:
+ if (
+ self.editor.note.note_type()["originalStockKind"]
+ == StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION
+ ):
+ self.addButton.setVisible(False)
+ self.addButtonHideAll.setVisible(True)
+ self.addButtonHideOne.setVisible(True)
+ else:
+ self.addButton.setVisible(True)
+ self.addButtonHideAll.setVisible(False)
+ self.addButtonHideOne.setVisible(False)
+
def setAndFocusNote(self, note: Note) -> None:
self.editor.set_note(note, focusTo=0)
@@ -192,6 +219,9 @@ def on_notetype_change(self, notetype_id: NotetypeId) -> None:
old_note.note_type(), new_note.note_type()
)
+ # update buttons for image occlusion on note type change
+ self.show_hide_add_buttons()
+
def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None:
note = self._new_note()
if old_note := sticky_fields_from:
@@ -348,6 +378,14 @@ def doClose() -> None:
self.ifCanClose(doClose)
+ def add_io_hide_all_note(self) -> None:
+ self.editor.web.eval("setOcclusionField(true)")
+ self.add_current_note()
+
+ def add_io_hide_one_note(self) -> None:
+ self.editor.web.eval("setOcclusionField(false)")
+ self.add_current_note()
+
# legacy aliases
@property
diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index 3eed03c64e2..2b3d5be2328 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -31,6 +31,7 @@
from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter
from anki.httpclient import HttpClient
+from anki.models import StockNotetype
from anki.notes import Note, NoteFieldsCheckResult
from anki.utils import checksum, is_lin, is_win, namedtmp
from aqt import AnkiQt, colors, gui_hooks
@@ -475,6 +476,11 @@ def onBridgeCmd(self, cmd: str) -> Any:
elif cmd in self._links:
return self._links[cmd](self)
+ elif cmd.startswith("toggleMaskEditor"):
+ (_, show_str) = cmd.split(":", 1)
+ show = show_str == "true"
+ self.onToggleMaskEditor(show)
+
else:
print("uncaught cmd", cmd)
@@ -548,6 +554,7 @@ def oncallback(arg: Any) -> None:
setShrinkImages({json.dumps(self.mw.col.get_config("shrinkEditorImages", True))});
setCloseHTMLTags({json.dumps(self.mw.col.get_config("closeHTMLTags", True))});
triggerChanges();
+ setOriginalStockKind({json.dumps(self.note.note_type()["originalStockKind"])});
"""
if self.addMode:
@@ -1177,6 +1184,45 @@ def toggleCloseHTMLTags(self) -> None:
def setTagsCollapsed(self, collapsed: bool) -> None:
aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed)
+ def onAddImageForOcclusion(self) -> None:
+ """Show a file selection screen, then get selected image path."""
+ extension_filter = " ".join(
+ f"*.{extension}" for extension in sorted(itertools.chain(pics))
+ )
+ filter = f"{tr.editing_media()} ({extension_filter})"
+
+ def accept(file: str) -> None:
+ try:
+ html = self._addMedia(file)
+ options = {"kind": "add", "imagePath": file, "notetypeId": 0}
+ # pass both html and options
+ options = {"html": html, "mode": options}
+ self.web.eval(f"setupMaskEditor({options})")
+ except Exception as e:
+ showWarning(str(e))
+ return
+
+ if self.addMode:
+ file = getFile(
+ parent=self.widget,
+ title=tr.editing_add_media(),
+ cb=cast(Callable[[Any], None], accept),
+ filter=filter,
+ key="media",
+ )
+ else:
+ options = {"kind": "edit", "noteId": self.note.id}
+ options = {"mode": options}
+ self.web.eval(f"setupMaskEditor({options})")
+
+ self.parentWindow.activateWindow()
+
+ def onToggleMaskEditor(self, show) -> None:
+ if show:
+ self.web.eval("toggleMaskEditor(true)")
+ else:
+ self.web.eval("toggleMaskEditor(false)")
+
# Links from HTML
######################################################################
@@ -1206,6 +1252,8 @@ def _init_links(self) -> None:
toggleMathjax=Editor.toggleMathjax,
toggleShrinkImages=Editor.toggleShrinkImages,
toggleCloseHTMLTags=Editor.toggleCloseHTMLTags,
+ addImageForOcclusion=Editor.onAddImageForOcclusion,
+ toggleMaskEditor=Editor.onToggleMaskEditor,
)
@@ -1463,4 +1511,19 @@ def set_cloze_button(editor: Editor) -> None:
)
+def set_image_occlusion_button(editor: Editor) -> None:
+ action = (
+ "show"
+ if editor.note.note_type()["originalStockKind"]
+ == StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION
+ else "hide"
+ )
+ editor.web.eval(
+ 'require("anki/ui").loaded.then(() =>'
+ f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("image-occlusion-button")'
+ "); "
+ )
+
+
gui_hooks.editor_did_load_note.append(set_cloze_button)
+gui_hooks.editor_did_load_note.append(set_image_occlusion_button)
diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte
index a9b78624c05..6388e7261f5 100644
--- a/ts/editor/NoteEditor.svelte
+++ b/ts/editor/NoteEditor.svelte
@@ -42,6 +42,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+
+
+
+ {
+ bridgeCommand(`toggleMaskEditor:${false}`);
+ }}
+ >
+ {tr.notetypesOcclusionNote()}
+
+
+
+
+ {
+ bridgeCommand(`toggleMaskEditor:${true}`);
+ // run this bridge when image not added
+ if (get(ioImageLoaded) === false) {
+ bridgeCommand("addImageForOcclusion");
+ }
+ }}
+ >
+ {tr.notetypesOcclusionMask()}
+
+
+
+
diff --git a/ts/editor/tsconfig.json b/ts/editor/tsconfig.json
index e021013df1f..38bb3cccd86 100644
--- a/ts/editor/tsconfig.json
+++ b/ts/editor/tsconfig.json
@@ -7,7 +7,8 @@
"symbols-overlay/*",
"plain-text-input/*",
"rich-text-input/*",
- "editor-toolbar/*"
+ "editor-toolbar/*",
+ "../image-occlusion/*"
],
"references": [
{ "path": "../components" },
diff --git a/ts/image-occlusion/Notes.svelte b/ts/image-occlusion/Notes.svelte
index 9333348b12b..bbe8fe9ae26 100644
--- a/ts/image-occlusion/Notes.svelte
+++ b/ts/image-occlusion/Notes.svelte
@@ -85,7 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
.note-toolbar {
- margin-left: 98px;
+ margin-left: 106px;
margin-top: 2px;
display: flex;
overflow-x: auto;
diff --git a/ts/image-occlusion/TopToolbar.svelte b/ts/image-occlusion/TopToolbar.svelte
index 0461915eb69..cf89bc083e5 100644
--- a/ts/image-occlusion/TopToolbar.svelte
+++ b/ts/image-occlusion/TopToolbar.svelte
@@ -141,7 +141,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
display: flex;
overflow-y: scroll;
z-index: 99;
- margin-left: 98px;
+ margin-left: 106px;
margin-top: 2px;
}
@@ -171,6 +171,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
height: 32px;
margin: unset;
padding: 6px !important;
+ font-size: 16px !important;
}
.dropdown-content {
diff --git a/ts/image-occlusion/store.ts b/ts/image-occlusion/store.ts
index ce9b3be7d19..63111edab9e 100644
--- a/ts/image-occlusion/store.ts
+++ b/ts/image-occlusion/store.ts
@@ -9,3 +9,5 @@ export const notesDataStore = writable({ id: "", title: "", divValue: "", textar
export const zoomResetValue = writable(1);
// it stores the tags for the note in note editor
export const tagsWritable = writable([""]);
+// it stores the value of image loaded into mask editor or not
+export const ioImageLoaded = writable(false);