Enable strict_optional for aqt/editor.py (#3500)

* Enable strict_optional for aqt/editor.py

* Fix mypy errors in EditorWebView class

* Fix mypy errors in Editor class

* DRY

* Match same short circuiting behavior

* Convention
This commit is contained in:
Ben Nguyen 2024-10-15 08:08:24 -07:00 committed by GitHub
parent 694a5089a9
commit a537d349b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 51 deletions

View file

@ -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]

View file

@ -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("<br>", "").strip()
notChangedvalues = {"", "<br>"}
@ -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 '<a href="{}">{}</a>'.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")'