diff --git a/.mypy.ini b/.mypy.ini
index f9fdf42f5..5d6d2be5c 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -38,6 +38,8 @@ strict_optional = True
strict_optional = True
[mypy-aqt.operations.*]
strict_optional = True
+[mypy-aqt.editor]
+strict_optional = True
[mypy-anki.scheduler.base]
strict_optional = True
[mypy-anki._backend.rsbridge]
diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index 4f64c5f2f..4c2cdaf23 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -34,7 +34,7 @@ from anki.collection import Config, SearchNode
from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter
from anki.httpclient import HttpClient
-from anki.models import NotetypeId, StockNotetype
+from anki.models import NotetypeDict, NotetypeId, StockNotetype
from anki.notes import Note, NoteFieldsCheckResult, NoteId
from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp
from aqt import AnkiQt, colors, gui_hooks
@@ -242,38 +242,35 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
rightside: bool = True,
) -> str:
"""Assign func to bridge cmd, register shortcut, return button"""
- if func:
- def wrapped_func(editor: Editor) -> None:
- self.call_after_note_saved(
- functools.partial(func, editor), keepFocus=True
- )
+ def wrapped_func(editor: Editor) -> None:
+ self.call_after_note_saved(functools.partial(func, editor), keepFocus=True)
- self._links[cmd] = wrapped_func
+ self._links[cmd] = wrapped_func
- if keys:
+ if keys:
- def on_activated() -> None:
- wrapped_func(self)
+ def on_activated() -> None:
+ wrapped_func(self)
- if toggleable:
- # generate a random id for triggering toggle
- id = id or str(randrange(1_000_000))
+ if toggleable:
+ # generate a random id for triggering toggle
+ id = id or str(randrange(1_000_000))
- def on_hotkey() -> None:
- on_activated()
- self.web.eval(
- f'toggleEditorButton(document.getElementById("{id}"));'
- )
+ def on_hotkey() -> None:
+ on_activated()
+ self.web.eval(
+ f'toggleEditorButton(document.getElementById("{id}"));'
+ )
- else:
- on_hotkey = on_activated
+ else:
+ on_hotkey = on_activated
- QShortcut( # type: ignore
- QKeySequence(keys),
- self.widget,
- activated=on_hotkey,
- )
+ QShortcut( # type: ignore
+ QKeySequence(keys),
+ self.widget,
+ activated=on_hotkey,
+ )
btn = self._addButton(
icon,
@@ -363,7 +360,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _onFields(self) -> None:
from aqt.fields import FieldDialog
- FieldDialog(self.mw, self.note.note_type(), parent=self.parentWindow)
+ FieldDialog(self.mw, self.note_type(), parent=self.parentWindow)
def onCardLayout(self) -> None:
self.call_after_note_saved(self._onCardLayout)
@@ -375,6 +372,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
ord = self.card.ord
else:
ord = 0
+
+ assert self.note is not None
CardLayout(
self.mw,
self.note,
@@ -435,7 +434,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
gui_hooks.editor_did_focus_field(self.note, self.currentField)
elif cmd.startswith("toggleStickyAll"):
- model = self.note.note_type()
+ model = self.note_type()
flds = model["flds"]
any_sticky = any([fld["sticky"] for fld in flds])
@@ -456,7 +455,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
(type, num) = cmd.split(":", 1)
ord = int(num)
- model = self.note.note_type()
+ model = self.note_type()
fld = model["flds"][ord]
new_state = not fld["sticky"]
fld["sticky"] = new_state
@@ -469,10 +468,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
elif cmd.startswith("lastTextColor"):
(_, textColor) = cmd.split(":", 1)
+ assert self.mw.pm.profile is not None
self.mw.pm.profile["lastTextColor"] = textColor
elif cmd.startswith("lastHighlightColor"):
(_, highlightColor) = cmd.split(":", 1)
+ assert self.mw.pm.profile is not None
self.mw.pm.profile["lastHighlightColor"] = highlightColor
elif cmd.startswith("saveTags"):
@@ -545,11 +546,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
for fld, val in self.note.items()
]
- flds = self.note.note_type()["flds"]
+ note_type = self.note_type()
+ flds = note_type["flds"]
collapsed = [fld["collapsed"] for fld in flds]
plain_texts = [fld.get("plainText", False) for fld in flds]
descriptions = [fld.get("description", "") for fld in flds]
- notetype_meta = {"id": self.note.mid, "modTime": self.note.note_type()["mod"]}
+ notetype_meta = {"id": self.note.mid, "modTime": note_type["mod"]}
self.widget.show()
@@ -566,6 +568,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self.web.setFocus()
gui_hooks.editor_did_load_note(self)
+ assert self.mw.pm.profile is not None
text_color = self.mw.pm.profile.get("lastTextColor", "#0000ff")
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#0000ff")
@@ -590,7 +593,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
"""
if self.addMode:
- sticky = [field["sticky"] for field in self.note.note_type()["flds"]]
+ sticky = [field["sticky"] for field in self.note_type()["flds"]]
js += " setSticky(%s);" % json.dumps(sticky)
if (
@@ -607,6 +610,9 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _save_current_note(self) -> None:
"Call after note is updated with data from webview."
+ if not self.note:
+ return
+
update_note(parent=self.widget, note=self.note).run_in_background(
initiator=self
)
@@ -614,7 +620,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def fonts(self) -> list[tuple[str, int, bool]]:
return [
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
- for f in self.note.note_type()["flds"]
+ for f in self.note_type()["flds"]
]
def call_after_note_saved(
@@ -648,6 +654,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
checkValid = _check_and_update_duplicate_display_async
def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None:
+ assert self.note is not None
cols = [""] * len(self.note.fields)
cloze_hint = ""
if result == NoteFieldsCheckResult.DUPLICATE:
@@ -665,13 +672,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
)
def showDupes(self) -> None:
+ assert self.note is not None
aqt.dialogs.open(
"Browser",
self.mw,
search=(
SearchNode(
dupe=SearchNode.Dupe(
- notetype_id=self.note.note_type()["id"],
+ notetype_id=self.note_type()["id"],
first_field=self.note.fields[0],
)
),
@@ -681,7 +689,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
if not self.note:
return True
- m = self.note.note_type()
+ m = self.note_type()
for c, f in enumerate(self.note.fields):
f = f.replace("
", "").strip()
notChangedvalues = {"", "
"}
@@ -696,7 +704,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
if self.web:
self.web.cleanup()
- self.web = None
+ self.web = None # type: ignore
# legacy
@@ -729,9 +737,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
if self.tags.col != self.mw.col:
self.tags.setCol(self.mw.col)
if not self.tags.text() or not self.addMode:
+ assert self.note is not None
self.tags.setText(self.note.string_tags().strip())
def on_tag_focus_lost(self) -> None:
+ assert self.note is not None
self.note.tags = self.mw.col.tags.split(self.tags.text())
gui_hooks.editor_did_update_tags(self.note)
if not self.addMode:
@@ -826,7 +836,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# Media downloads
######################################################################
- def urlToLink(self, url: str) -> str | None:
+ def urlToLink(self, url: str) -> str:
fname = self.urlToFile(url)
if not fname:
return '{}'.format(
@@ -1037,8 +1047,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
######################################################################
def current_notetype_is_image_occlusion(self) -> bool:
- return bool(self.note) and (
- self.note.note_type().get("originalStockKind", None)
+ if not self.note:
+ return False
+
+ return (
+ self.note_type().get("originalStockKind", None)
== StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION
)
@@ -1049,6 +1062,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
image_path=image_path, notetype_id=0
)
else:
+ assert self.note is not None
self.setup_mask_editor_for_existing_note(
note_id=self.note.id, image_path=image_path
)
@@ -1075,8 +1089,10 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def select_image_from_clipboard_and_occlude(self) -> None:
"""Set up the mask editor for the image in the clipboard."""
- clipoard = self.mw.app.clipboard()
- mime = clipoard.mimeData()
+ clipboard = self.mw.app.clipboard()
+ assert clipboard is not None
+ mime = clipboard.mimeData()
+ assert mime is not None
if not mime.hasImage():
showWarning(tr.editing_no_image_found_on_clipboard())
return
@@ -1160,6 +1176,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy)
def _onHtmlEdit(self, field: int) -> None:
+ assert self.note is not None
d = QDialog(self.widget, Qt.WindowType.Window)
form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d)
@@ -1223,7 +1240,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy)
def _onCloze(self) -> None:
# check that the model is set up for cloze deletion
- if self.note.note_type()["type"] != MODEL_CLOZE:
+ if self.note_type()["type"] != MODEL_CLOZE:
if self.addMode:
tooltip(tr.editing_warning_cloze_deletions_will_not_work())
else:
@@ -1231,7 +1248,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
return
# find the highest existing cloze
highest = 0
- for name, val in list(self.note.items()):
+ assert self.note is not None
+ for _, val in list(self.note.items()):
m = re.findall(r"\{\{c(\d+)::", val)
if m:
highest = max(highest, sorted(int(x) for x in m)[-1])
@@ -1243,6 +1261,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self.web.eval("wrap('{{c%d::', '}}');" % highest)
def setupForegroundButton(self) -> None:
+ assert self.mw.pm.profile is not None
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
# use last colour
@@ -1276,6 +1295,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy)
def onColourChanged(self) -> None:
self._updateForegroundButton()
+ assert self.mw.pm.profile is not None
self.mw.pm.profile["lastColour"] = self.fcolour
@deprecated(info=_js_legacy)
@@ -1300,6 +1320,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
(tr.editing_edit_html(), self.onHtmlEdit, "Ctrl+Shift+X"),
):
a = m.addAction(text)
+ assert a is not None
qconnect(a.triggered, handler)
a.setShortcut(QKeySequence(shortcut))
@@ -1387,6 +1408,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
addImageForOcclusionFromClipboard=Editor.select_image_from_clipboard_and_occlude,
)
+ def note_type(self) -> NotetypeDict:
+ 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
######################################################################
@@ -1403,6 +1430,7 @@ class EditorWebView(AnkiWebView):
self._internal_field_text_for_paste: str | None = None
self._last_known_clipboard_mime: QMimeData | None = None
clip = self.editor.mw.app.clipboard()
+ assert clip is not None
clip.dataChanged.connect(self._on_clipboard_change)
gui_hooks.editor_web_view_did_init(self)
@@ -1411,7 +1439,7 @@ class EditorWebView(AnkiWebView):
self._internal_field_text_for_paste = None
def _on_clipboard_change(self) -> None:
- self._last_known_clipboard_mime = self.editor.mw.app.clipboard().mimeData()
+ self._last_known_clipboard_mime = self._clipboard().mimeData()
if self._store_field_content_on_next_clipboard_change:
# if the flag was set, save the field data
self._internal_field_text_for_paste = self._get_clipboard_html_for_field()
@@ -1423,8 +1451,9 @@ class EditorWebView(AnkiWebView):
self._internal_field_text_for_paste = None
def _get_clipboard_html_for_field(self):
- clip = self.editor.mw.app.clipboard()
+ clip = self._clipboard()
mime = clip.mimeData()
+ assert mime is not None
if not mime.hasHtml():
return
return mime.html()
@@ -1440,6 +1469,7 @@ class EditorWebView(AnkiWebView):
def _opened_context_menu_on_image(self) -> bool:
context_menu_request = self.lastContextMenuRequest()
+ assert context_menu_request is not None
return (
context_menu_request.mediaType()
== context_menu_request.MediaType.MediaTypeImage
@@ -1455,7 +1485,8 @@ class EditorWebView(AnkiWebView):
def _onPaste(self, mode: QClipboard.Mode) -> None:
# Since _on_clipboard_change doesn't always trigger properly on macOS, we do a double check if any changes were made before pasting
- if self._last_known_clipboard_mime != self.editor.mw.app.clipboard().mimeData():
+ clipboard = self._clipboard()
+ if self._last_known_clipboard_mime != clipboard.mimeData():
self._on_clipboard_change()
extended = self._wantsExtendedPaste()
if html := self._internal_field_text_for_paste:
@@ -1463,7 +1494,8 @@ class EditorWebView(AnkiWebView):
self.editor.doPaste(html, True, extended)
else:
print("use clipboard")
- mime = self.editor.mw.app.clipboard().mimeData(mode=mode)
+ mime = clipboard.mimeData(mode=mode)
+ assert mime is not None
html, internal = self._processMime(mime, extended)
if html:
self.editor.doPaste(html, internal, extended)
@@ -1474,12 +1506,15 @@ class EditorWebView(AnkiWebView):
def onMiddleClickPaste(self) -> None:
self._onPaste(QClipboard.Mode.Selection)
- def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
+ def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None:
+ assert evt is not None
evt.accept()
- def dropEvent(self, evt: QDropEvent) -> None:
+ def dropEvent(self, evt: QDropEvent | None) -> None:
+ assert evt is not None
extended = self._wantsExtendedPaste()
mime = evt.mimeData()
+ assert mime is not None
cursor_pos = self.mapFromGlobal(QCursor.pos())
if evt.source() and mime.hasHtml():
@@ -1585,12 +1620,13 @@ class EditorWebView(AnkiWebView):
return fname
- def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
+ def contextMenuEvent(self, evt: QContextMenuEvent | None) -> None:
m = QMenu(self)
if self.hasSelection():
self._add_cut_action(m)
self._add_copy_action(m)
a = m.addAction(tr.editing_paste())
+ assert a is not None
qconnect(a.triggered, self.onPaste)
if self._opened_context_menu_on_image():
self._add_image_menu(m)
@@ -1599,26 +1635,38 @@ class EditorWebView(AnkiWebView):
def _add_cut_action(self, menu: QMenu) -> None:
a = menu.addAction(tr.editing_cut())
+ assert a is not None
qconnect(a.triggered, self.onCut)
def _add_copy_action(self, menu: QMenu) -> None:
a = menu.addAction(tr.actions_copy())
+ assert a is not None
qconnect(a.triggered, self.onCopy)
def _add_image_menu(self, menu: QMenu) -> None:
a = menu.addAction(tr.editing_copy_image())
+ assert a is not None
qconnect(a.triggered, self.on_copy_image)
- url = self.lastContextMenuRequest().mediaUrl()
+ context_menu_request = self.lastContextMenuRequest()
+ assert context_menu_request is not None
+ url = context_menu_request.mediaUrl()
file_name = url.fileName()
path = os.path.join(self.editor.mw.col.media.dir(), file_name)
a = menu.addAction(tr.editing_open_image())
+ assert a is not None
qconnect(a.triggered, lambda: openFolder(path))
if is_win or is_mac:
a = menu.addAction(tr.editing_show_in_folder())
+ assert a is not None
qconnect(a.triggered, lambda: show_in_folder(path))
+ def _clipboard(self) -> QClipboard:
+ clipboard = self.editor.mw.app.clipboard()
+ assert clipboard is not None
+ return clipboard
+
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light"
# - there may be other cases like a trailing 'Bold' that need fixing, but will
@@ -1648,7 +1696,7 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
def set_cloze_button(editor: Editor) -> None:
- action = "show" if editor.note.note_type()["type"] == MODEL_CLOZE else "hide"
+ action = "show" if editor.note_type()["type"] == MODEL_CLOZE else "hide"
editor.web.eval(
'require("anki/ui").loaded.then(() =>'
f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")'