Fix Cards/Fields buttons

This commit is contained in:
Abdo 2025-06-20 22:55:02 +03:00
parent fa0c4b11ab
commit e0120309fa
3 changed files with 58 additions and 110 deletions

View file

@ -110,9 +110,6 @@ class AddCards(QMainWindow):
) )
def reopen(self, mw: AnkiQt) -> None: def reopen(self, mw: AnkiQt) -> None:
if not self.editor.fieldsAreBlank():
return
defaults = self.col.defaults_for_adding( defaults = self.col.defaults_for_adding(
current_review_card=self.mw.reviewer.card current_review_card=self.mw.reviewer.card
) )
@ -315,9 +312,6 @@ class AddCards(QMainWindow):
onOk() onOk()
def afterSave() -> None: def afterSave() -> None:
if self.editor.fieldsAreBlank(self._last_added_note):
return onOk()
ask_user_dialog( ask_user_dialog(
tr.adding_discard_current_input(), tr.adding_discard_current_input(),
callback=callback, callback=callback,

View file

@ -9,6 +9,7 @@ import json
import mimetypes import mimetypes
import os import os
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum from enum import Enum
from random import randrange from random import randrange
from typing import Any from typing import Any
@ -16,7 +17,7 @@ from typing import Any
from anki._legacy import deprecated from anki._legacy import deprecated
from anki.cards import Card from anki.cards import Card
from anki.hooks import runFilter from anki.hooks import runFilter
from anki.models import NotetypeDict, StockNotetype from anki.models import NotetypeDict, NotetypeId, StockNotetype
from anki.notes import Note, NoteId from anki.notes import Note, NoteId
from anki.utils import is_win from anki.utils import is_win
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
@ -81,6 +82,21 @@ def on_editor_ready(func: Callable) -> Callable:
return decorated return decorated
@dataclass
class NoteInfo:
"Used to hold partial note info fetched from the webview"
id: NoteId | None
mid: NotetypeId
fields: list[str]
def __post_init__(self) -> None:
if self.id is not None:
self.id = NoteId(int(self.id))
if self.mid is not None:
self.mid = NotetypeId(int(self.mid))
class Editor: class Editor:
"""The screen that embeds an editing widget should listen for changes via """The screen that embeds an editing widget should listen for changes via
the `operation_did_execute` hook, and call set_note() when the editor needs the `operation_did_execute` hook, and call set_note() when the editor needs
@ -103,7 +119,7 @@ class Editor:
self.mw = mw self.mw = mw
self.widget = widget self.widget = widget
self.parentWindow = parentWindow self.parentWindow = parentWindow
self.nid: NoteId | None = None self.mid: NotetypeId | None = None
# legacy argument provided? # legacy argument provided?
if addMode is not None: if addMode is not None:
editor_mode = EditorMode.ADD_CARDS if addMode else EditorMode.EDIT_CURRENT editor_mode = EditorMode.ADD_CARDS if addMode else EditorMode.EDIT_CURRENT
@ -118,8 +134,6 @@ class Editor:
# current card, for card layout # current card, for card layout
self.card: Card | None = None self.card: Card | None = None
self.state: EditorState = EditorState.INITIAL self.state: EditorState = EditorState.INITIAL
# used for the io mask editor's context menu
self.last_io_image_path: str | None = None
self._ready = False self._ready = False
self._ready_callbacks: list[Callable[[], None]] = [] self._ready_callbacks: list[Callable[[], None]] = []
self._init_links() self._init_links()
@ -127,7 +141,6 @@ class Editor:
self.add_webview() self.add_webview()
self.setupWeb() self.setupWeb()
self.setupShortcuts() self.setupShortcuts()
# gui_hooks.editor_did_init(self)
# Initial setup # Initial setup
############################################################ ############################################################
@ -327,7 +340,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _onFields(self) -> None: def _onFields(self) -> None:
from aqt.fields import FieldDialog from aqt.fields import FieldDialog
FieldDialog(self.mw, self.note_type(), parent=self.parentWindow) def on_note_info(note_info: NoteInfo) -> None:
FieldDialog(
self.mw, self.mw.col.models.get(note_info.mid), parent=self.parentWindow
)
self.get_note_info(on_note_info)
def onCardLayout(self) -> None: def onCardLayout(self) -> None:
self.call_after_note_saved(self._onCardLayout) self.call_after_note_saved(self._onCardLayout)
@ -340,10 +358,15 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
else: else:
ord = 0 ord = 0
assert self.note is not None def on_note_info(note_info: NoteInfo) -> None:
if note_info.id:
note = self.mw.col.get_note(note_info.id)
else:
note = Note(self.mw.col, note_info.mid)
note.fields = note_info.fields
CardLayout( CardLayout(
self.mw, self.mw,
self.note, note,
ord=ord, ord=ord,
parent=self.parentWindow, parent=self.parentWindow,
fill_empty=False, fill_empty=False,
@ -351,6 +374,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
if is_win: if is_win:
self.parentWindow.activateWindow() self.parentWindow.activateWindow()
self.get_note_info(on_note_info)
# JS->Python bridge # JS->Python bridge
###################################################################### ######################################################################
@ -361,62 +386,16 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
ord = int(ord_str) ord = int(ord_str)
if type == "blur": if type == "blur":
self.currentField = None self.currentField = None
# run any filters
if self.note and gui_hooks.editor_did_unfocus_field(
False, self.note, ord
):
# something updated the note; update it after a subsequent focus
# event has had time to fire
self.mw.progress.timer(
100, self.loadNoteKeepingFocus, False, parent=self.widget
)
else: else:
if self.note: pass
gui_hooks.editor_did_fire_typing_timer(self.note)
# focused into field? # focused into field?
elif cmd.startswith("focus"): elif cmd.startswith("focus"):
(type, num) = cmd.split(":", 1) (type, num) = cmd.split(":", 1)
self.last_field_index = self.currentField = int(num) self.last_field_index = self.currentField = int(num)
if self.note:
gui_hooks.editor_did_focus_field(self.note, self.currentField)
elif cmd.startswith("toggleStickyAll"):
model = self.note_type()
flds = model["flds"]
any_sticky = any([fld["sticky"] for fld in flds])
result = []
for fld in flds:
if not any_sticky or fld["sticky"]:
fld["sticky"] = not fld["sticky"]
result.append(fld["sticky"])
update_notetype_legacy(parent=self.mw, notetype=model).run_in_background(
initiator=self
)
return result
elif cmd.startswith("toggleSticky"):
(type, num) = cmd.split(":", 1)
ord = int(num)
model = self.note_type()
fld = model["flds"][ord]
new_state = not fld["sticky"]
fld["sticky"] = new_state
update_notetype_legacy(parent=self.mw, notetype=model).run_in_background(
initiator=self
)
return new_state
elif cmd.startswith("saveTags"): elif cmd.startswith("saveTags"):
if self.note: pass
gui_hooks.editor_did_update_tags(self.note)
elif cmd.startswith("editorState"): elif cmd.startswith("editorState"):
(_, new_state_id, old_state_id) = cmd.split(":", 2) (_, new_state_id, old_state_id) = cmd.split(":", 2)
@ -496,17 +475,10 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
assert self.mw.pm.profile is not None assert self.mw.pm.profile is not None
js = f"loadNote({json.dumps(self.nid)}, {mid}, {json.dumps(focus_to)}, {json.dumps(self.orig_note_id)});" js = f"loadNote({json.dumps(self.nid)}, {mid}, {json.dumps(focus_to)}, {json.dumps(self.orig_note_id)});"
if self.note:
js = gui_hooks.editor_will_load_note(js, self.note, self)
self.web.evalWithCallback( self.web.evalWithCallback(
f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback
) )
@deprecated(replaced_by=load_note)
def loadNote(self, focusTo: int | None = None) -> None:
assert self.note is not None
self.load_note(self.note.mid, focus_to=focusTo)
def call_after_note_saved( def call_after_note_saved(
self, callback: Callable, keepFocus: bool = False self, callback: Callable, keepFocus: bool = False
) -> None: ) -> None:
@ -519,19 +491,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
saveNow = call_after_note_saved saveNow = call_after_note_saved
def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
if not self.note:
return True
m = self.note_type()
for c, f in enumerate(self.note.fields):
f = f.replace("<br>", "").strip()
notChangedvalues = {"", "<br>"}
if previousNote and m["flds"][c]["sticky"]:
notChangedvalues.add(previousNote.fields[c].replace("<br>", "").strip())
if f not in notChangedvalues:
return False
return True
def cleanup(self) -> None: def cleanup(self) -> None:
av_player.stop_and_clear_queue_if_caller(self.editorMode) av_player.stop_and_clear_queue_if_caller(self.editorMode)
self.set_note(None) self.set_note(None)
@ -557,21 +516,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# Image occlusion # Image occlusion
###################################################################### ######################################################################
def current_notetype_is_image_occlusion(self) -> bool:
if not self.note:
return False
return (
self.note_type().get("originalStockKind", None)
== StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION
)
def setup_mask_editor(self, image_path: str) -> None: def setup_mask_editor(self, image_path: str) -> None:
try: try:
if self.editorMode == EditorMode.ADD_CARDS: if self.editorMode == EditorMode.ADD_CARDS:
self.setup_mask_editor_for_new_note(image_path=image_path) self.setup_mask_editor_for_new_note(image_path=image_path)
else: else:
assert self.note is not None
self.setup_mask_editor_for_existing_note(image_path=image_path) self.setup_mask_editor_for_existing_note(image_path=image_path)
except Exception as e: except Exception as e:
showWarning(str(e)) showWarning(str(e))
@ -613,17 +562,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
copy=Editor.onCopy, copy=Editor.onCopy,
) )
@property def get_note_info(self, on_done: Callable[[NoteInfo], None]) -> None:
def note(self) -> Note | None: def wrapped_on_done(note_info: dict[str, Any]) -> None:
if self.nid is None: on_done(NoteInfo(**note_info))
return None
return self.mw.col.get_note(self.nid)
def note_type(self) -> NotetypeDict: self.web.evalWithCallback("getNoteInfo()", wrapped_on_done)
assert self.note is not None
note_type = self.note.note_type()
assert note_type is not None
return note_type
# Pasting, drag & drop, and keyboard layouts # Pasting, drag & drop, and keyboard layouts
@ -651,3 +594,4 @@ class EditorWebView(AnkiWebView):
def onPaste(self) -> None: def onPaste(self) -> None:
self.triggerPageAction(QWebEnginePage.WebAction.Paste) self.triggerPageAction(QWebEnginePage.WebAction.Paste)
self.triggerPageAction(QWebEnginePage.WebAction.Paste)

View file

@ -449,6 +449,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return true; return true;
} }
export function getNoteInfo() {
return {
id: note?.id.toString() ?? null,
mid: notetypeMeta.id.toString(),
fields: note?.fields ?? [],
};
}
let richTextInputs: RichTextInput[] = []; let richTextInputs: RichTextInput[] = [];
$: richTextInputs = richTextInputs.filter(Boolean); $: richTextInputs = richTextInputs.filter(Boolean);
@ -870,6 +879,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setPlainTexts, setPlainTexts,
setDescriptions, setDescriptions,
setFonts, setFonts,
getNoteInfo,
focusField, focusField,
setTags, setTags,
setTagsCollapsed, setTagsCollapsed,