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")'