add types to editor.py

This commit is contained in:
Damien Elmes 2021-02-01 17:28:35 +10:00
parent e7483edee7
commit d13762bd32
4 changed files with 87 additions and 81 deletions

View file

@ -806,8 +806,8 @@ QTableView {{ gridline-color: {grid} }}
qconnect(hh.sectionMoved, self.onColumnMoved) qconnect(hh.sectionMoved, self.onColumnMoved)
def onSortChanged(self, idx: int, ord: int) -> None: def onSortChanged(self, idx: int, ord: int) -> None:
ord = bool(ord) ord_bool = bool(ord)
self.editor.saveNow(lambda: self._onSortChanged(idx, ord)) self.editor.saveNow(lambda: self._onSortChanged(idx, ord_bool))
def _onSortChanged(self, idx: int, ord: bool) -> None: def _onSortChanged(self, idx: int, ord: bool) -> None:
type = self.model.activeCols[idx] type = self.model.activeCols[idx]

View file

@ -12,7 +12,7 @@ import urllib.parse
import urllib.request import urllib.request
import warnings import warnings
from random import randrange from random import randrange
from typing import Callable, List, Optional, Tuple from typing import Any, Callable, Dict, List, Match, Optional, Tuple
import bs4 import bs4
import requests import requests
@ -92,7 +92,9 @@ _html = """
# caller is responsible for resetting note on reset # caller is responsible for resetting note on reset
class Editor: class Editor:
def __init__(self, mw: AnkiQt, widget, parentWindow, addMode=False) -> None: def __init__(
self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False
) -> None:
self.mw = mw self.mw = mw
self.widget = widget self.widget = widget
self.parentWindow = parentWindow self.parentWindow = parentWindow
@ -110,7 +112,7 @@ class Editor:
# Initial setup # Initial setup
############################################################ ############################################################
def setupOuter(self): def setupOuter(self) -> None:
l = QVBoxLayout() l = QVBoxLayout()
l.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
l.setSpacing(0) l.setSpacing(0)
@ -229,7 +231,7 @@ class Editor:
# Top buttons # Top buttons
###################################################################### ######################################################################
def resourceToData(self, path): def resourceToData(self, path: str) -> str:
"""Convert a file (specified by a path) into a data URI.""" """Convert a file (specified by a path) into a data URI."""
if not os.path.exists(path): if not os.path.exists(path):
raise FileNotFoundError raise FileNotFoundError
@ -251,21 +253,21 @@ class Editor:
keys: str = None, keys: str = None,
disables: bool = True, disables: bool = True,
rightside: bool = True, rightside: bool = True,
): ) -> str:
"""Assign func to bridge cmd, register shortcut, return button""" """Assign func to bridge cmd, register shortcut, return button"""
if func: if func:
self._links[cmd] = func self._links[cmd] = func
if keys: if keys:
def on_activated(): def on_activated() -> None:
func(self) func(self)
if toggleable: if toggleable:
# generate a random id for triggering toggle # generate a random id for triggering toggle
id = id or str(randrange(1_000_000)) id = id or str(randrange(1_000_000))
def on_hotkey(): def on_hotkey() -> None:
on_activated() on_activated()
self.web.eval(f'toggleEditorButton("#{id}");') self.web.eval(f'toggleEditorButton("#{id}");')
@ -383,26 +385,26 @@ class Editor:
keys, fn, _ = row keys, fn, _ = row
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
def _addFocusCheck(self, fn): def _addFocusCheck(self, fn: Callable) -> Callable:
def checkFocus(): def checkFocus() -> None:
if self.currentField is None: if self.currentField is None:
return return
fn() fn()
return checkFocus return checkFocus
def onFields(self): def onFields(self) -> None:
self.saveNow(self._onFields) self.saveNow(self._onFields)
def _onFields(self): def _onFields(self) -> None:
from aqt.fields import FieldDialog from aqt.fields import FieldDialog
FieldDialog(self.mw, self.note.model(), parent=self.parentWindow) FieldDialog(self.mw, self.note.model(), parent=self.parentWindow)
def onCardLayout(self): def onCardLayout(self) -> None:
self.saveNow(self._onCardLayout) self.saveNow(self._onCardLayout)
def _onCardLayout(self): def _onCardLayout(self) -> None:
from aqt.clayout import CardLayout from aqt.clayout import CardLayout
if self.card: if self.card:
@ -422,16 +424,16 @@ class Editor:
# JS->Python bridge # JS->Python bridge
###################################################################### ######################################################################
def onBridgeCmd(self, cmd) -> None: def onBridgeCmd(self, cmd: str) -> None:
if not self.note: if not self.note:
# shutdown # shutdown
return return
# focus lost or key/button pressed? # focus lost or key/button pressed?
if cmd.startswith("blur") or cmd.startswith("key"): if cmd.startswith("blur") or cmd.startswith("key"):
(type, ord, nid, txt) = cmd.split(":", 3) (type, ord_str, nid_str, txt) = cmd.split(":", 3)
ord = int(ord) ord = int(ord_str)
try: try:
nid = int(nid) nid = int(nid_str)
except ValueError: except ValueError:
nid = 0 nid = 0
if nid != self.note.id: if nid != self.note.id:
@ -465,13 +467,15 @@ class Editor:
else: else:
print("uncaught cmd", cmd) print("uncaught cmd", cmd)
def mungeHTML(self, txt): def mungeHTML(self, txt: str) -> str:
return gui_hooks.editor_will_munge_html(txt, self) return gui_hooks.editor_will_munge_html(txt, self)
# Setting/unsetting the current note # Setting/unsetting the current note
###################################################################### ######################################################################
def setNote(self, note, hide=True, focusTo=None): def setNote(
self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None
) -> None:
"Make NOTE the current note." "Make NOTE the current note."
self.note = note self.note = note
self.currentField = None self.currentField = None
@ -482,10 +486,10 @@ class Editor:
if hide: if hide:
self.widget.hide() self.widget.hide()
def loadNoteKeepingFocus(self): def loadNoteKeepingFocus(self) -> None:
self.loadNote(self.currentField) self.loadNote(self.currentField)
def loadNote(self, focusTo=None) -> None: def loadNote(self, focusTo: Optional[int] = None) -> None:
if not self.note: if not self.note:
return return
@ -496,7 +500,7 @@ class Editor:
self.widget.show() self.widget.show()
self.updateTags() self.updateTags()
def oncallback(arg): def oncallback(arg: Any) -> None:
if not self.note: if not self.note:
return return
self.setupForegroundButton() self.setupForegroundButton()
@ -520,7 +524,7 @@ class Editor:
for f in self.note.model()["flds"] for f in self.note.model()["flds"]
] ]
def saveNow(self, callback, keepFocus=False): def saveNow(self, callback: Callable, keepFocus: bool = False) -> None:
"Save unsaved edits then call callback()." "Save unsaved edits then call callback()."
if not self.note: if not self.note:
# calling code may not expect the callback to fire immediately # calling code may not expect the callback to fire immediately
@ -529,7 +533,7 @@ class Editor:
self.saveTags() self.saveTags()
self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback()) self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback())
def checkValid(self): def checkValid(self) -> None:
cols = [""] * len(self.note.fields) cols = [""] * len(self.note.fields)
err = self.note.dupeOrEmpty() err = self.note.dupeOrEmpty()
if err == 2: if err == 2:
@ -537,7 +541,7 @@ class Editor:
self.web.eval("setBackgrounds(%s);" % json.dumps(cols)) self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
def showDupes(self): def showDupes(self) -> None:
self.mw.browser_search( self.mw.browser_search(
SearchTerm( SearchTerm(
dupe=SearchTerm.Dupe( dupe=SearchTerm.Dupe(
@ -546,7 +550,7 @@ class Editor:
) )
) )
def fieldsAreBlank(self, previousNote=None): def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool:
if not self.note: if not self.note:
return True return True
m = self.note.model() m = self.note.model()
@ -559,7 +563,7 @@ class Editor:
return False return False
return True return True
def cleanup(self): def cleanup(self) -> None:
self.setNote(None) self.setNote(None)
# prevent any remaining evalWithCallback() events from firing after C++ object deleted # prevent any remaining evalWithCallback() events from firing after C++ object deleted
self.web = None self.web = None
@ -567,11 +571,11 @@ class Editor:
# HTML editing # HTML editing
###################################################################### ######################################################################
def onHtmlEdit(self): def onHtmlEdit(self) -> None:
field = self.currentField field = self.currentField
self.saveNow(lambda: self._onHtmlEdit(field)) self.saveNow(lambda: self._onHtmlEdit(field))
def _onHtmlEdit(self, field): def _onHtmlEdit(self, field: int) -> None:
d = QDialog(self.widget, Qt.Window) d = QDialog(self.widget, Qt.Window)
form = aqt.forms.edithtml.Ui_Dialog() form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d) form.setupUi(d)
@ -604,7 +608,7 @@ class Editor:
# Tag handling # Tag handling
###################################################################### ######################################################################
def setupTags(self): def setupTags(self) -> None:
import aqt.tagedit import aqt.tagedit
g = QGroupBox(self.widget) g = QGroupBox(self.widget)
@ -626,7 +630,7 @@ class Editor:
g.setLayout(tb) g.setLayout(tb)
self.outerLayout.addWidget(g) self.outerLayout.addWidget(g)
def updateTags(self): def updateTags(self) -> None:
if self.tags.col != self.mw.col: if self.tags.col != self.mw.col:
self.tags.setCol(self.mw.col) self.tags.setCol(self.mw.col)
if not self.tags.text() or not self.addMode: if not self.tags.text() or not self.addMode:
@ -640,44 +644,44 @@ class Editor:
self.note.flush() self.note.flush()
gui_hooks.editor_did_update_tags(self.note) gui_hooks.editor_did_update_tags(self.note)
def saveAddModeVars(self): def saveAddModeVars(self) -> None:
if self.addMode: if self.addMode:
# save tags to model # save tags to model
m = self.note.model() m = self.note.model()
m["tags"] = self.note.tags m["tags"] = self.note.tags
self.mw.col.models.save(m, updateReqs=False) self.mw.col.models.save(m, updateReqs=False)
def hideCompleters(self): def hideCompleters(self) -> None:
self.tags.hideCompleter() self.tags.hideCompleter()
def onFocusTags(self): def onFocusTags(self) -> None:
self.tags.setFocus() self.tags.setFocus()
# Format buttons # Format buttons
###################################################################### ######################################################################
def toggleBold(self): def toggleBold(self) -> None:
self.web.eval("setFormat('bold');") self.web.eval("setFormat('bold');")
def toggleItalic(self): def toggleItalic(self) -> None:
self.web.eval("setFormat('italic');") self.web.eval("setFormat('italic');")
def toggleUnderline(self): def toggleUnderline(self) -> None:
self.web.eval("setFormat('underline');") self.web.eval("setFormat('underline');")
def toggleSuper(self): def toggleSuper(self) -> None:
self.web.eval("setFormat('superscript');") self.web.eval("setFormat('superscript');")
def toggleSub(self): def toggleSub(self) -> None:
self.web.eval("setFormat('subscript');") self.web.eval("setFormat('subscript');")
def removeFormat(self): def removeFormat(self) -> None:
self.web.eval("setFormat('removeFormat');") self.web.eval("setFormat('removeFormat');")
def onCloze(self): def onCloze(self) -> None:
self.saveNow(self._onCloze, keepFocus=True) self.saveNow(self._onCloze, keepFocus=True)
def _onCloze(self): def _onCloze(self) -> None:
# check that the model is set up for cloze deletion # check that the model is set up for cloze deletion
if self.note.model()["type"] != MODEL_CLOZE: if self.note.model()["type"] != MODEL_CLOZE:
if self.addMode: if self.addMode:
@ -701,16 +705,16 @@ class Editor:
# Foreground colour # Foreground colour
###################################################################### ######################################################################
def setupForegroundButton(self): def setupForegroundButton(self) -> None:
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f") self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
self.onColourChanged() self.onColourChanged()
# use last colour # use last colour
def onForeground(self): def onForeground(self) -> None:
self._wrapWithColour(self.fcolour) self._wrapWithColour(self.fcolour)
# choose new colour # choose new colour
def onChangeCol(self): def onChangeCol(self) -> None:
if isLin: if isLin:
new = QColorDialog.getColor( new = QColorDialog.getColor(
QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog
@ -724,32 +728,32 @@ class Editor:
self.onColourChanged() self.onColourChanged()
self._wrapWithColour(self.fcolour) self._wrapWithColour(self.fcolour)
def _updateForegroundButton(self): def _updateForegroundButton(self) -> None:
self.web.eval("setFGButton('%s')" % self.fcolour) self.web.eval("setFGButton('%s')" % self.fcolour)
def onColourChanged(self): def onColourChanged(self) -> None:
self._updateForegroundButton() self._updateForegroundButton()
self.mw.pm.profile["lastColour"] = self.fcolour self.mw.pm.profile["lastColour"] = self.fcolour
def _wrapWithColour(self, colour): def _wrapWithColour(self, colour: str) -> None:
self.web.eval("setFormat('forecolor', '%s')" % colour) self.web.eval("setFormat('forecolor', '%s')" % colour)
# Audio/video/images # Audio/video/images
###################################################################### ######################################################################
def onAddMedia(self): def onAddMedia(self) -> None:
extension_filter = " ".join( extension_filter = " ".join(
"*." + extension for extension in sorted(itertools.chain(pics, audio)) "*." + extension for extension in sorted(itertools.chain(pics, audio))
) )
key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")" key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")"
def accept(file): def accept(file: str) -> None:
self.addMedia(file, canDelete=True) self.addMedia(file, canDelete=True)
file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media") file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media")
self.parentWindow.activateWindow() self.parentWindow.activateWindow()
def addMedia(self, path, canDelete=False): def addMedia(self, path: str, canDelete: bool = False) -> None:
try: try:
html = self._addMedia(path, canDelete) html = self._addMedia(path, canDelete)
except Exception as e: except Exception as e:
@ -757,7 +761,7 @@ class Editor:
return return
self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html)) self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html))
def _addMedia(self, path, canDelete=False): def _addMedia(self, path: str, canDelete: bool = False) -> str:
"Add to media folder and return local img or sound tag." "Add to media folder and return local img or sound tag."
# copy to media folder # copy to media folder
fname = self.mw.col.media.addFile(path) fname = self.mw.col.media.addFile(path)
@ -774,7 +778,7 @@ class Editor:
def _addMediaFromData(self, fname: str, data: bytes) -> str: def _addMediaFromData(self, fname: str, data: bytes) -> str:
return self.mw.col.media.writeData(fname, data) return self.mw.col.media.writeData(fname, data)
def onRecSound(self): def onRecSound(self) -> None:
aqt.sound.record_audio( aqt.sound.record_audio(
self.parentWindow, self.parentWindow,
self.mw, self.mw,
@ -808,7 +812,7 @@ class Editor:
# not a supported type # not a supported type
return None return None
def isURL(self, s): def isURL(self, s: str) -> bool:
s = s.lower() s = s.lower()
return ( return (
s.startswith("http://") s.startswith("http://")
@ -957,23 +961,23 @@ class Editor:
) )
def doDrop(self, html: str, internal: bool, extended: bool = False) -> None: def doDrop(self, html: str, internal: bool, extended: bool = False) -> None:
def pasteIfField(ret): def pasteIfField(ret: bool) -> None:
if ret: if ret:
self.doPaste(html, internal, extended) self.doPaste(html, internal, extended)
p = self.web.mapFromGlobal(QCursor.pos()) p = self.web.mapFromGlobal(QCursor.pos())
self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField) self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField)
def onPaste(self): def onPaste(self) -> None:
self.web.onPaste() self.web.onPaste()
def onCutOrCopy(self): def onCutOrCopy(self) -> None:
self.web.flagAnkiText() self.web.flagAnkiText()
# Advanced menu # Advanced menu
###################################################################### ######################################################################
def onAdvanced(self): def onAdvanced(self) -> None:
m = QMenu(self.mw) m = QMenu(self.mw)
for text, handler, shortcut in ( for text, handler, shortcut in (
@ -1000,28 +1004,28 @@ class Editor:
# LaTeX # LaTeX
###################################################################### ######################################################################
def insertLatex(self): def insertLatex(self) -> None:
self.web.eval("wrap('[latex]', '[/latex]');") self.web.eval("wrap('[latex]', '[/latex]');")
def insertLatexEqn(self): def insertLatexEqn(self) -> None:
self.web.eval("wrap('[$]', '[/$]');") self.web.eval("wrap('[$]', '[/$]');")
def insertLatexMathEnv(self): def insertLatexMathEnv(self) -> None:
self.web.eval("wrap('[$$]', '[/$$]');") self.web.eval("wrap('[$$]', '[/$$]');")
def insertMathjaxInline(self): def insertMathjaxInline(self) -> None:
self.web.eval("wrap('\\\\(', '\\\\)');") self.web.eval("wrap('\\\\(', '\\\\)');")
def insertMathjaxBlock(self): def insertMathjaxBlock(self) -> None:
self.web.eval("wrap('\\\\[', '\\\\]');") self.web.eval("wrap('\\\\[', '\\\\]');")
def insertMathjaxChemistry(self): def insertMathjaxChemistry(self) -> None:
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');") self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
# Links from HTML # Links from HTML
###################################################################### ######################################################################
_links = dict( _links: Dict[str, Callable] = dict(
fields=onFields, fields=onFields,
cards=onCardLayout, cards=onCardLayout,
bold=toggleBold, bold=toggleBold,
@ -1047,7 +1051,7 @@ class Editor:
class EditorWebView(AnkiWebView): class EditorWebView(AnkiWebView):
def __init__(self, parent, editor): def __init__(self, parent: QWidget, editor: Editor) -> None:
AnkiWebView.__init__(self, title="editor") AnkiWebView.__init__(self, title="editor")
self.editor = editor self.editor = editor
self.strip = self.editor.mw.pm.profile["stripHTML"] self.strip = self.editor.mw.pm.profile["stripHTML"]
@ -1057,15 +1061,15 @@ class EditorWebView(AnkiWebView):
qconnect(clip.dataChanged, self._onClipboardChange) qconnect(clip.dataChanged, self._onClipboardChange)
gui_hooks.editor_web_view_did_init(self) gui_hooks.editor_web_view_did_init(self)
def _onClipboardChange(self): def _onClipboardChange(self) -> None:
if self._markInternal: if self._markInternal:
self._markInternal = False self._markInternal = False
self._flagAnkiText() self._flagAnkiText()
def onCut(self): def onCut(self) -> None:
self.triggerPageAction(QWebEnginePage.Cut) self.triggerPageAction(QWebEnginePage.Cut)
def onCopy(self): def onCopy(self) -> None:
self.triggerPageAction(QWebEnginePage.Copy) self.triggerPageAction(QWebEnginePage.Copy)
def _wantsExtendedPaste(self) -> bool: def _wantsExtendedPaste(self) -> bool:
@ -1088,10 +1092,10 @@ class EditorWebView(AnkiWebView):
def onMiddleClickPaste(self) -> None: def onMiddleClickPaste(self) -> None:
self._onPaste(QClipboard.Selection) self._onPaste(QClipboard.Selection)
def dragEnterEvent(self, evt): def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
evt.accept() evt.accept()
def dropEvent(self, evt): def dropEvent(self, evt: QDropEvent) -> None:
extended = self._wantsExtendedPaste() extended = self._wantsExtendedPaste()
mime = evt.mimeData() mime = evt.mimeData()
@ -1172,7 +1176,7 @@ class EditorWebView(AnkiWebView):
token = html.escape(token).replace("\t", " " * 4) token = html.escape(token).replace("\t", " " * 4)
# if there's more than one consecutive space, # if there's more than one consecutive space,
# use non-breaking spaces for the second one on # use non-breaking spaces for the second one on
def repl(match): def repl(match: Match) -> None:
return match.group(1).replace(" ", " ") + " " return match.group(1).replace(" ", " ") + " "
token = re.sub(" ( +)", repl, token) token = re.sub(" ( +)", repl, token)
@ -1218,11 +1222,11 @@ class EditorWebView(AnkiWebView):
return self.editor.fnameToLink(fname) return self.editor.fnameToLink(fname)
return None return None
def flagAnkiText(self): def flagAnkiText(self) -> None:
# be ready to adjust when clipboard event fires # be ready to adjust when clipboard event fires
self._markInternal = True self._markInternal = True
def _flagAnkiText(self): def _flagAnkiText(self) -> None:
# add a comment in the clipboard html so we can tell text is copied # add a comment in the clipboard html so we can tell text is copied
# from us and doesn't need to be stripped # from us and doesn't need to be stripped
clip = self.editor.mw.app.clipboard() clip = self.editor.mw.app.clipboard()
@ -1250,20 +1254,20 @@ class EditorWebView(AnkiWebView):
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light" # 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 # - there may be other cases like a trailing 'Bold' that need fixing, but will
# wait for further reports first. # wait for further reports first.
def fontMungeHack(font): def fontMungeHack(font: str) -> str:
return re.sub(" L$", " Light", font) return re.sub(" L$", " Light", font)
def munge_html(txt, editor): def munge_html(txt: str, editor: Editor) -> str:
return "" if txt in ("<br>", "<div><br></div>") else txt return "" if txt in ("<br>", "<div><br></div>") else txt
def remove_null_bytes(txt, editor): def remove_null_bytes(txt: str, editor: Editor) -> str:
# misbehaving apps may include a null byte in the text # misbehaving apps may include a null byte in the text
return txt.replace("\x00", "") return txt.replace("\x00", "")
def reverse_url_quoting(txt, editor): def reverse_url_quoting(txt: str, editor: Editor) -> str:
# reverse the url quoting we added to get images to display # reverse the url quoting we added to get images to display
return editor.mw.col.media.escape_media_filenames(txt, unescape=True) return editor.mw.col.media.escape_media_filenames(txt, unescape=True)

View file

@ -525,7 +525,7 @@ if isWin:
id: Any id: Any
class WindowsRTTTSFilePlayer(TTSProcessPlayer): class WindowsRTTTSFilePlayer(TTSProcessPlayer):
voice_list = None voice_list: List[Any] = []
tmppath = os.path.join(tmpdir(), "tts.wav") tmppath = os.path.join(tmpdir(), "tts.wav")
def import_voices(self) -> None: def import_voices(self) -> None:

View file

@ -12,6 +12,8 @@ strict_equality = true
disallow_untyped_defs=true disallow_untyped_defs=true
[mypy-aqt.sidebar] [mypy-aqt.sidebar]
disallow_untyped_defs=true disallow_untyped_defs=true
[mypy-aqt.editor]
disallow_untyped_defs=true
[mypy-aqt.mpv] [mypy-aqt.mpv]