add some typing to addcards and main

This commit is contained in:
Damien Elmes 2019-12-20 18:43:52 +10:00
parent ddac1dd579
commit 638a8515bc
3 changed files with 113 additions and 100 deletions

View file

@ -14,9 +14,12 @@ from anki.hooks import addHook, remHook, runHook
from anki.utils import htmlToTextLine, isMac from anki.utils import htmlToTextLine, isMac
import aqt.editor, aqt.modelchooser, aqt.deckchooser import aqt.editor, aqt.modelchooser, aqt.deckchooser
from anki.notes import Note
from typing import Callable
class AddCards(QDialog): class AddCards(QDialog):
def __init__(self, mw: AnkiQt): def __init__(self, mw: AnkiQt) -> None:
QDialog.__init__(self, None, Qt.Window) QDialog.__init__(self, None, Qt.Window)
mw.setupDialogGC(self) mw.setupDialogGC(self)
self.mw = mw self.mw = mw
@ -37,11 +40,11 @@ class AddCards(QDialog):
addCloseShortcut(self) addCloseShortcut(self)
self.show() self.show()
def setupEditor(self): def setupEditor(self) -> None:
self.editor = aqt.editor.Editor( self.editor = aqt.editor.Editor(
self.mw, self.form.fieldsArea, self, True) self.mw, self.form.fieldsArea, self, True)
def setupChoosers(self): def setupChoosers(self) -> None:
self.modelChooser = aqt.modelchooser.ModelChooser( self.modelChooser = aqt.modelchooser.ModelChooser(
self.mw, self.form.modelArea) self.mw, self.form.modelArea)
self.deckChooser = aqt.deckchooser.DeckChooser( self.deckChooser = aqt.deckchooser.DeckChooser(
@ -50,7 +53,7 @@ class AddCards(QDialog):
def helpRequested(self): def helpRequested(self):
openHelp("addingnotes") openHelp("addingnotes")
def setupButtons(self): def setupButtons(self) -> None:
bb = self.form.buttonBox bb = self.form.buttonBox
ar = QDialogButtonBox.ActionRole ar = QDialogButtonBox.ActionRole
# add # add
@ -63,7 +66,7 @@ class AddCards(QDialog):
self.closeButton.setAutoDefault(False) self.closeButton.setAutoDefault(False)
bb.addButton(self.closeButton, QDialogButtonBox.RejectRole) bb.addButton(self.closeButton, QDialogButtonBox.RejectRole)
# help # help
self.helpButton = QPushButton(_("Help"), clicked=self.helpRequested) self.helpButton = QPushButton(_("Help"), clicked=self.helpRequested) # type: ignore
self.helpButton.setAutoDefault(False) self.helpButton.setAutoDefault(False)
bb.addButton(self.helpButton, bb.addButton(self.helpButton,
QDialogButtonBox.HelpRole) QDialogButtonBox.HelpRole)
@ -80,10 +83,10 @@ class AddCards(QDialog):
b.setEnabled(False) b.setEnabled(False)
self.historyButton = b self.historyButton = b
def setAndFocusNote(self, note): def setAndFocusNote(self, note: Note) -> None:
self.editor.setNote(note, focusTo=0) self.editor.setNote(note, focusTo=0)
def onModelChange(self): def onModelChange(self) -> None:
oldNote = self.editor.note oldNote = self.editor.note
note = self.mw.col.newNote() note = self.mw.col.newNote()
self.previousNote = None self.previousNote = None
@ -108,7 +111,7 @@ class AddCards(QDialog):
self.removeTempNote(oldNote) self.removeTempNote(oldNote)
self.editor.setNote(note) self.editor.setNote(note)
def onReset(self, model=None, keep=False): def onReset(self, model: None = None, keep: bool = False) -> None:
oldNote = self.editor.note oldNote = self.editor.note
note = self.mw.col.newNote() note = self.mw.col.newNote()
flds = note.model()['flds'] flds = note.model()['flds']
@ -126,7 +129,7 @@ class AddCards(QDialog):
break break
self.setAndFocusNote(note) self.setAndFocusNote(note)
def removeTempNote(self, note): def removeTempNote(self, note: Note) -> None:
if not note or not note.id: if not note or not note.id:
return return
# we don't have to worry about cards; just the note # we don't have to worry about cards; just the note
@ -205,10 +208,10 @@ question on all cards."""), help="AddItems")
return return
return QDialog.keyPressEvent(self, evt) return QDialog.keyPressEvent(self, evt)
def reject(self): def reject(self) -> None:
self.ifCanClose(self._reject) self.ifCanClose(self._reject)
def _reject(self): def _reject(self) -> None:
remHook('reset', self.onReset) remHook('reset', self.onReset)
remHook('currentModelChanged', self.onModelChange) remHook('currentModelChanged', self.onModelChange)
clearAudioQueue() clearAudioQueue()
@ -221,7 +224,7 @@ question on all cards."""), help="AddItems")
aqt.dialogs.markClosed("AddCards") aqt.dialogs.markClosed("AddCards")
QDialog.reject(self) QDialog.reject(self)
def ifCanClose(self, onOk): def ifCanClose(self, onOk: Callable) -> None:
def afterSave(): def afterSave():
ok = (self.editor.fieldsAreBlank(self.previousNote) or ok = (self.editor.fieldsAreBlank(self.previousNote) or
askUser(_("Close and lose current input?"), defaultno=True)) askUser(_("Close and lose current input?"), defaultno=True))

View file

@ -8,7 +8,7 @@ import time
import re import re
import unicodedata import unicodedata
from operator import itemgetter from operator import itemgetter
from typing import Callable, List, Dict, Optional from typing import Callable, List, Optional
from anki.collection import _Collection from anki.collection import _Collection
from anki.lang import ngettext from anki.lang import ngettext

View file

@ -10,7 +10,7 @@ import time
import faulthandler import faulthandler
import platform import platform
from threading import Thread from threading import Thread
from typing import Optional from typing import Sequence
from send2trash import send2trash from send2trash import send2trash
from anki.collection import _Collection from anki.collection import _Collection
from aqt.profiles import ProfileManager as ProfileManagerType from aqt.profiles import ProfileManager as ProfileManagerType
@ -33,8 +33,15 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
from aqt.qt import sip from aqt.qt import sip
from anki.lang import _, ngettext from anki.lang import _, ngettext
from argparse import Namespace
from typing import Any, Callable, Dict, List, Optional, Tuple
class AnkiQt(QMainWindow): class AnkiQt(QMainWindow):
def __init__(self, app: QApplication, profileManager: ProfileManagerType, opts, args): col: _Collection
pm: ProfileManagerType
web: aqt.webview.AnkiWebView
def __init__(self, app: QApplication, profileManager: ProfileManagerType, opts: Namespace, args: List[Any]) -> None:
QMainWindow.__init__(self) QMainWindow.__init__(self)
self.state = "startup" self.state = "startup"
self.opts = opts self.opts = opts
@ -65,7 +72,7 @@ class AnkiQt(QMainWindow):
fn = self.setupProfile fn = self.setupProfile
self.progress.timer(10, fn, False, requiresCollection=False) self.progress.timer(10, fn, False, requiresCollection=False)
def setupUI(self): def setupUI(self) -> None:
self.col = None self.col = None
self.setupCrashLog() self.setupCrashLog()
self.disableGC() self.disableGC()
@ -118,14 +125,14 @@ class AnkiQt(QMainWindow):
self.close() self.close()
self.closeFires = True self.closeFires = True
def setupProfile(self): def setupProfile(self) -> None:
if self.pm.meta['firstRun']: if self.pm.meta['firstRun']:
# load the new deck user profile # load the new deck user profile
self.pm.load(self.pm.profiles()[0]) self.pm.load(self.pm.profiles()[0])
self.pm.meta['firstRun'] = False self.pm.meta['firstRun'] = False
self.pm.save() self.pm.save()
self.pendingImport = None self.pendingImport: Optional[str] = None
self.restoringBackup = False self.restoringBackup = False
# profile not provided on command line? # profile not provided on command line?
if not self.pm.name: if not self.pm.name:
@ -138,7 +145,7 @@ class AnkiQt(QMainWindow):
else: else:
self.loadProfile() self.loadProfile()
def showProfileManager(self): def showProfileManager(self) -> None:
self.pm.profile = None self.pm.profile = None
self.state = "profileManager" self.state = "profileManager"
d = self.profileDiag = self.ProfileManager() d = self.profileDiag = self.ProfileManager()
@ -155,14 +162,14 @@ class AnkiQt(QMainWindow):
f.profiles.currentRowChanged.connect(self.onProfileRowChange) f.profiles.currentRowChanged.connect(self.onProfileRowChange)
f.statusbar.setVisible(False) f.statusbar.setVisible(False)
# enter key opens profile # enter key opens profile
QShortcut(QKeySequence("Return"), d, activated=self.onOpenProfile) QShortcut(QKeySequence("Return"), d, activated=self.onOpenProfile) # type: ignore
self.refreshProfilesList() self.refreshProfilesList()
# raise first, for osx testing # raise first, for osx testing
d.show() d.show()
d.activateWindow() d.activateWindow()
d.raise_() d.raise_()
def refreshProfilesList(self): def refreshProfilesList(self) -> None:
f = self.profileForm f = self.profileForm
f.profiles.clear() f.profiles.clear()
profs = self.pm.profiles() profs = self.pm.profiles()
@ -173,7 +180,7 @@ class AnkiQt(QMainWindow):
idx = 0 idx = 0
f.profiles.setCurrentRow(idx) f.profiles.setCurrentRow(idx)
def onProfileRowChange(self, n): def onProfileRowChange(self, n: int) -> None:
if n < 0: if n < 0:
# called on .clear() # called on .clear()
return return
@ -185,7 +192,7 @@ class AnkiQt(QMainWindow):
name = self.pm.profiles()[self.profileForm.profiles.currentRow()] name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
return self.pm.load(name) return self.pm.load(name)
def onOpenProfile(self): def onOpenProfile(self) -> None:
self.loadProfile(self.profileDiag.closeWithoutQuitting) self.loadProfile(self.profileDiag.closeWithoutQuitting)
def profileNameOk(self, str): def profileNameOk(self, str):
@ -255,7 +262,7 @@ close the profile or restart Anki."""))
self.onOpenProfile() self.onOpenProfile()
def loadProfile(self, onsuccess=None): def loadProfile(self, onsuccess: Optional[Callable] = None) -> None:
self.maybeAutoSync() self.maybeAutoSync()
if not self.loadCollection(): if not self.loadCollection():
@ -280,7 +287,7 @@ close the profile or restart Anki."""))
if onsuccess: if onsuccess:
onsuccess() onsuccess()
def unloadProfile(self, onsuccess): def unloadProfile(self, onsuccess: Callable) -> None:
def callback(): def callback():
self._unloadProfile() self._unloadProfile()
onsuccess() onsuccess()
@ -288,7 +295,7 @@ close the profile or restart Anki."""))
runHook("unloadProfile") runHook("unloadProfile")
self.unloadCollection(callback) self.unloadCollection(callback)
def _unloadProfile(self): def _unloadProfile(self) -> None:
self.pm.profile['mainWindowGeom'] = self.saveGeometry() self.pm.profile['mainWindowGeom'] = self.saveGeometry()
self.pm.profile['mainWindowState'] = self.saveState() self.pm.profile['mainWindowState'] = self.saveState()
self.pm.save() self.pm.save()
@ -301,7 +308,7 @@ close the profile or restart Anki."""))
self.maybeAutoSync() self.maybeAutoSync()
def _checkForUnclosedWidgets(self): def _checkForUnclosedWidgets(self) -> None:
for w in self.app.topLevelWidgets(): for w in self.app.topLevelWidgets():
if w.isVisible(): if w.isVisible():
# windows with this property are safe to close immediately # windows with this property are safe to close immediately
@ -310,13 +317,13 @@ close the profile or restart Anki."""))
else: else:
print("Window should have been closed: {}".format(w)) print("Window should have been closed: {}".format(w))
def unloadProfileAndExit(self): def unloadProfileAndExit(self) -> None:
self.unloadProfile(self.cleanupAndExit) self.unloadProfile(self.cleanupAndExit)
def unloadProfileAndShowProfileManager(self): def unloadProfileAndShowProfileManager(self):
self.unloadProfile(self.showProfileManager) self.unloadProfile(self.showProfileManager)
def cleanupAndExit(self): def cleanupAndExit(self) -> None:
self.errorHandler.unload() self.errorHandler.unload()
self.mediaServer.shutdown() self.mediaServer.shutdown()
anki.sound.cleanupMPV() anki.sound.cleanupMPV()
@ -325,7 +332,7 @@ close the profile or restart Anki."""))
# Sound/video # Sound/video
########################################################################## ##########################################################################
def setupSound(self): def setupSound(self) -> None:
if isWin: if isWin:
return return
try: try:
@ -338,7 +345,7 @@ close the profile or restart Anki."""))
# Collection load/unload # Collection load/unload
########################################################################## ##########################################################################
def loadCollection(self): def loadCollection(self) -> bool:
try: try:
return self._loadCollection() return self._loadCollection()
except Exception as e: except Exception as e:
@ -362,7 +369,7 @@ Debug info:
self.showProfileManager() self.showProfileManager()
return False return False
def _loadCollection(self): def _loadCollection(self) -> bool:
cpath = self.pm.collectionPath() cpath = self.pm.collectionPath()
self.col = Collection(cpath, log=True) self.col = Collection(cpath, log=True)
@ -373,7 +380,7 @@ Debug info:
self.moveToState("deckBrowser") self.moveToState("deckBrowser")
return True return True
def unloadCollection(self, onsuccess): def unloadCollection(self, onsuccess: Callable) -> None:
def callback(): def callback():
self.setEnabled(False) self.setEnabled(False)
self._unloadCollection() self._unloadCollection()
@ -381,7 +388,7 @@ Debug info:
self.closeAllWindows(callback) self.closeAllWindows(callback)
def _unloadCollection(self): def _unloadCollection(self) -> None:
if not self.col: if not self.col:
return return
if self.restoringBackup: if self.restoringBackup:
@ -431,7 +438,7 @@ from the profile screen."))
z.writestr("media", "{}") z.writestr("media", "{}")
z.close() z.close()
def backup(self): def backup(self) -> None:
nbacks = self.pm.profile['numBackups'] nbacks = self.pm.profile['numBackups']
if not nbacks or devMode: if not nbacks or devMode:
return return
@ -462,7 +469,7 @@ from the profile screen."))
path = os.path.join(dir, fname) path = os.path.join(dir, fname)
os.unlink(path) os.unlink(path)
def maybeOptimize(self): def maybeOptimize(self) -> None:
# have two weeks passed? # have two weeks passed?
if (intTime() - self.pm.profile['lastOptimize']) < 86400*14: if (intTime() - self.pm.profile['lastOptimize']) < 86400*14:
return return
@ -475,7 +482,7 @@ from the profile screen."))
# State machine # State machine
########################################################################## ##########################################################################
def moveToState(self, state, *args): def moveToState(self, state: str, *args) -> None:
#print("-> move from", self.state, "to", state) #print("-> move from", self.state, "to", state)
oldState = self.state or "dummy" oldState = self.state or "dummy"
cleanup = getattr(self, "_"+oldState+"Cleanup", None) cleanup = getattr(self, "_"+oldState+"Cleanup", None)
@ -490,7 +497,7 @@ from the profile screen."))
self.bottomWeb.show() self.bottomWeb.show()
runHook('afterStateChange', state, oldState, *args) runHook('afterStateChange', state, oldState, *args)
def _deckBrowserState(self, oldState): def _deckBrowserState(self, oldState: str) -> None:
self.deckBrowser.show() self.deckBrowser.show()
def _colLoadingState(self, oldState): def _colLoadingState(self, oldState):
@ -501,14 +508,14 @@ from the profile screen."))
runHook("colLoading", self.col) runHook("colLoading", self.col)
self.moveToState("overview") self.moveToState("overview")
def _selectedDeck(self): def _selectedDeck(self) -> Optional[Dict[str, Any]]:
did = self.col.decks.selected() did = self.col.decks.selected()
if not self.col.decks.nameOrNone(did): if not self.col.decks.nameOrNone(did):
showInfo(_("Please select a deck.")) showInfo(_("Please select a deck."))
return return None
return self.col.decks.get(did) return self.col.decks.get(did)
def _overviewState(self, oldState): def _overviewState(self, oldState: str) -> None:
if not self._selectedDeck(): if not self._selectedDeck():
return self.moveToState("deckBrowser") return self.moveToState("deckBrowser")
self.col.reset() self.col.reset()
@ -528,7 +535,7 @@ from the profile screen."))
# Resetting state # Resetting state
########################################################################## ##########################################################################
def reset(self, guiOnly=False): def reset(self, guiOnly: bool = False) -> None:
"Called for non-trivial edits. Rebuilds queue and updates UI." "Called for non-trivial edits. Rebuilds queue and updates UI."
if self.col: if self.col:
if not guiOnly: if not guiOnly:
@ -548,7 +555,7 @@ from the profile screen."))
"True if not in profile manager, syncing, etc." "True if not in profile manager, syncing, etc."
return self.state in ("overview", "review", "deckBrowser") return self.state in ("overview", "review", "deckBrowser")
def maybeReset(self): def maybeReset(self) -> None:
self.autosave() self.autosave()
if self.state == "resetRequired": if self.state == "resetRequired":
self.state = self.returnState self.state = self.returnState
@ -582,7 +589,7 @@ from the profile screen."))
# HTML helpers # HTML helpers
########################################################################## ##########################################################################
def button(self, link, name, key=None, class_="", id="", extra=""): def button(self, link: str, name: str, key: Optional[str] = None, class_: str = "", id: str = "", extra: str = "") -> str:
class_ = "but "+ class_ class_ = "but "+ class_
if key: if key:
key = _("Shortcut key: %s") % key key = _("Shortcut key: %s") % key
@ -596,7 +603,7 @@ title="%s" %s>%s</button>''' % (
# Main window setup # Main window setup
########################################################################## ##########################################################################
def setupMainWindow(self): def setupMainWindow(self) -> None:
# main window # main window
self.form = aqt.forms.main.Ui_MainWindow() self.form = aqt.forms.main.Ui_MainWindow()
self.form.setupUi(self) self.form.setupUi(self)
@ -631,13 +638,13 @@ title="%s" %s>%s</button>''' % (
o._domReady = False o._domReady = False
o._page.setContent(bytes("", "ascii")) o._page.setContent(bytes("", "ascii"))
def closeAllWindows(self, onsuccess): def closeAllWindows(self, onsuccess: Callable) -> None:
aqt.dialogs.closeAll(onsuccess) aqt.dialogs.closeAll(onsuccess)
# Components # Components
########################################################################## ##########################################################################
def setupSignals(self): def setupSignals(self) -> None:
signal.signal(signal.SIGINT, self.onSigInt) signal.signal(signal.SIGINT, self.onSigInt)
def onSigInt(self, signum, frame): def onSigInt(self, signum, frame):
@ -649,38 +656,38 @@ title="%s" %s>%s</button>''' % (
self.close() self.close()
self.progress.timer(100, quit, False) self.progress.timer(100, quit, False)
def setupProgress(self): def setupProgress(self) -> None:
self.progress = aqt.progress.ProgressManager(self) self.progress = aqt.progress.ProgressManager(self)
def setupErrorHandler(self): def setupErrorHandler(self) -> None:
import aqt.errors import aqt.errors
self.errorHandler = aqt.errors.ErrorHandler(self) self.errorHandler = aqt.errors.ErrorHandler(self)
def setupAddons(self): def setupAddons(self) -> None:
import aqt.addons import aqt.addons
self.addonManager = aqt.addons.AddonManager(self) self.addonManager = aqt.addons.AddonManager(self)
if not self.safeMode: if not self.safeMode:
self.addonManager.loadAddons() self.addonManager.loadAddons()
def setupSpellCheck(self): def setupSpellCheck(self) -> None:
os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = ( os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = (
os.path.join(self.pm.base, "dictionaries")) os.path.join(self.pm.base, "dictionaries"))
def setupThreads(self): def setupThreads(self) -> None:
self._mainThread = QThread.currentThread() self._mainThread = QThread.currentThread()
def inMainThread(self): def inMainThread(self) -> bool:
return self._mainThread == QThread.currentThread() return self._mainThread == QThread.currentThread()
def setupDeckBrowser(self): def setupDeckBrowser(self) -> None:
from aqt.deckbrowser import DeckBrowser from aqt.deckbrowser import DeckBrowser
self.deckBrowser = DeckBrowser(self) self.deckBrowser = DeckBrowser(self)
def setupOverview(self): def setupOverview(self) -> None:
from aqt.overview import Overview from aqt.overview import Overview
self.overview = Overview(self) self.overview = Overview(self)
def setupReviewer(self): def setupReviewer(self) -> None:
from aqt.reviewer import Reviewer from aqt.reviewer import Reviewer
self.reviewer = Reviewer(self) self.reviewer = Reviewer(self)
@ -698,7 +705,7 @@ title="%s" %s>%s</button>''' % (
return return
# expects a current profile, but no collection loaded # expects a current profile, but no collection loaded
def maybeAutoSync(self): def maybeAutoSync(self) -> None:
if (not self.pm.profile['syncKey'] if (not self.pm.profile['syncKey']
or not self.pm.profile['autoSync'] or not self.pm.profile['autoSync']
or self.safeMode or self.safeMode
@ -723,7 +730,7 @@ title="%s" %s>%s</button>''' % (
self.setWindowState(self.windowState() & ~Qt.WindowMinimized) self.setWindowState(self.windowState() & ~Qt.WindowMinimized)
return True return True
def setupStyle(self): def setupStyle(self) -> None:
buf = "" buf = ""
if isWin and platform.release() == '10': if isWin and platform.release() == '10':
@ -755,7 +762,7 @@ QTreeWidget {
# Key handling # Key handling
########################################################################## ##########################################################################
def setupKeys(self): def setupKeys(self) -> None:
globalShortcuts = [ globalShortcuts = [
("Ctrl+:", self.onDebug), ("Ctrl+:", self.onDebug),
("d", lambda: self.moveToState("deckBrowser")), ("d", lambda: self.moveToState("deckBrowser")),
@ -767,26 +774,26 @@ QTreeWidget {
] ]
self.applyShortcuts(globalShortcuts) self.applyShortcuts(globalShortcuts)
self.stateShortcuts = [] self.stateShortcuts: Sequence[Tuple[str, Callable]] = []
def applyShortcuts(self, shortcuts): def applyShortcuts(self, shortcuts: Sequence[Tuple[str, Callable]]) -> List[QShortcut]:
qshortcuts = [] qshortcuts = []
for key, fn in shortcuts: for key, fn in shortcuts:
scut = QShortcut(QKeySequence(key), self, activated=fn) scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore
scut.setAutoRepeat(False) scut.setAutoRepeat(False)
qshortcuts.append(scut) qshortcuts.append(scut)
return qshortcuts return qshortcuts
def setStateShortcuts(self, shortcuts): def setStateShortcuts(self, shortcuts: List[Tuple[str, Callable]]) -> None:
runHook(self.state+"StateShortcuts", shortcuts) runHook(self.state+"StateShortcuts", shortcuts)
self.stateShortcuts = self.applyShortcuts(shortcuts) self.stateShortcuts = self.applyShortcuts(shortcuts)
def clearStateShortcuts(self): def clearStateShortcuts(self) -> None:
for qs in self.stateShortcuts: for qs in self.stateShortcuts:
sip.delete(qs) sip.delete(qs)
self.stateShortcuts = [] self.stateShortcuts = []
def onStudyKey(self): def onStudyKey(self) -> None:
if self.state == "overview": if self.state == "overview":
self.col.startTimebox() self.col.startTimebox()
self.moveToState("review") self.moveToState("review")
@ -796,7 +803,7 @@ QTreeWidget {
# App exit # App exit
########################################################################## ##########################################################################
def closeEvent(self, event): def closeEvent(self, event: QCloseEvent) -> None:
if self.state == "profileManager": if self.state == "profileManager":
# if profile manager active, this event may fire via OS X menu bar's # if profile manager active, this event may fire via OS X menu bar's
# quit option # quit option
@ -827,7 +834,7 @@ QTreeWidget {
runHook("revertedState", n) runHook("revertedState", n)
self.maybeEnableUndo() self.maybeEnableUndo()
def maybeEnableUndo(self): def maybeEnableUndo(self) -> None:
if self.col and self.col.undoName(): if self.col and self.col.undoName():
self.form.actionUndo.setText(_("Undo %s") % self.form.actionUndo.setText(_("Undo %s") %
self.col.undoName()) self.col.undoName())
@ -842,7 +849,7 @@ QTreeWidget {
self.col.save(name) self.col.save(name)
self.maybeEnableUndo() self.maybeEnableUndo()
def autosave(self): def autosave(self) -> None:
saved = self.col.autosave() saved = self.col.autosave()
self.maybeEnableUndo() self.maybeEnableUndo()
if saved: if saved:
@ -851,10 +858,10 @@ QTreeWidget {
# Other menu operations # Other menu operations
########################################################################## ##########################################################################
def onAddCard(self): def onAddCard(self) -> None:
aqt.dialogs.open("AddCards", self) aqt.dialogs.open("AddCards", self)
def onBrowse(self): def onBrowse(self) -> None:
aqt.dialogs.open("Browser", self) aqt.dialogs.open("Browser", self)
def onEditCurrent(self): def onEditCurrent(self):
@ -899,12 +906,14 @@ QTreeWidget {
# Importing & exporting # Importing & exporting
########################################################################## ##########################################################################
def handleImport(self, path): def handleImport(self, path: str) -> None:
import aqt.importing import aqt.importing
if not os.path.exists(path): if not os.path.exists(path):
return showInfo(_("Please use File>Import to import this file.")) showInfo(_("Please use File>Import to import this file."))
return None
aqt.importing.importFile(self, path) aqt.importing.importFile(self, path)
return None
def onImport(self): def onImport(self):
import aqt.importing import aqt.importing
@ -938,7 +947,7 @@ QTreeWidget {
# Menu, title bar & status # Menu, title bar & status
########################################################################## ##########################################################################
def setupMenus(self): def setupMenus(self) -> None:
m = self.form m = self.form
m.actionSwitchProfile.triggered.connect( m.actionSwitchProfile.triggered.connect(
self.unloadProfileAndShowProfileManager) self.unloadProfileAndShowProfileManager)
@ -959,18 +968,18 @@ QTreeWidget {
m.actionEmptyCards.triggered.connect(self.onEmptyCards) m.actionEmptyCards.triggered.connect(self.onEmptyCards)
m.actionNoteTypes.triggered.connect(self.onNoteTypes) m.actionNoteTypes.triggered.connect(self.onNoteTypes)
def updateTitleBar(self): def updateTitleBar(self) -> None:
self.setWindowTitle("Anki") self.setWindowTitle("Anki")
# Auto update # Auto update
########################################################################## ##########################################################################
def setupAutoUpdate(self): def setupAutoUpdate(self) -> None:
import aqt.update import aqt.update
self.autoUpdate = aqt.update.LatestVersionFinder(self) self.autoUpdate = aqt.update.LatestVersionFinder(self)
self.autoUpdate.newVerAvail.connect(self.newVerAvail) self.autoUpdate.newVerAvail.connect(self.newVerAvail) # type: ignore
self.autoUpdate.newMsg.connect(self.newMsg) self.autoUpdate.newMsg.connect(self.newMsg) # type: ignore
self.autoUpdate.clockIsOff.connect(self.clockIsOff) self.autoUpdate.clockIsOff.connect(self.clockIsOff) # type: ignore
self.autoUpdate.start() self.autoUpdate.start()
def newVerAvail(self, ver): def newVerAvail(self, ver):
@ -1003,7 +1012,7 @@ Difference to correct time: %s.""") % diffText
# Count refreshing # Count refreshing
########################################################################## ##########################################################################
def setupRefreshTimer(self): def setupRefreshTimer(self) -> None:
# every 10 minutes # every 10 minutes
self.progress.timer(10*60*1000, self.onRefreshTimer, True) self.progress.timer(10*60*1000, self.onRefreshTimer, True)
@ -1016,7 +1025,7 @@ Difference to correct time: %s.""") % diffText
# Permanent libanki hooks # Permanent libanki hooks
########################################################################## ##########################################################################
def setupHooks(self): def setupHooks(self) -> None:
addHook("modSchema", self.onSchemaMod) addHook("modSchema", self.onSchemaMod)
addHook("remNotes", self.onRemNotes) addHook("remNotes", self.onRemNotes)
addHook("odueInvalid", self.onOdueInvalid) addHook("odueInvalid", self.onOdueInvalid)
@ -1050,7 +1059,7 @@ and if the problem comes up again, please ask on the support site."""))
# Log note deletion # Log note deletion
########################################################################## ##########################################################################
def onRemNotes(self, col, nids): def onRemNotes(self, col: _Collection, nids: List[int]) -> None:
path = os.path.join(self.pm.profileFolder(), "deleted.txt") path = os.path.join(self.pm.profileFolder(), "deleted.txt")
existed = os.path.exists(path) existed = os.path.exists(path)
with open(path, "ab") as f: with open(path, "ab") as f:
@ -1299,22 +1308,22 @@ will be lost. Continue?"""))
# System specific code # System specific code
########################################################################## ##########################################################################
def setupSystemSpecific(self): def setupSystemSpecific(self) -> None:
self.hideMenuAccels = False self.hideMenuAccels = False
if isMac: if isMac:
# mac users expect a minimize option # mac users expect a minimize option
self.minimizeShortcut = QShortcut("Ctrl+M", self) self.minimizeShortcut = QShortcut("Ctrl+M", self)
self.minimizeShortcut.activated.connect(self.onMacMinimize) self.minimizeShortcut.activated.connect(self.onMacMinimize) # type: ignore
self.hideMenuAccels = True self.hideMenuAccels = True
self.maybeHideAccelerators() self.maybeHideAccelerators()
self.hideStatusTips() self.hideStatusTips()
elif isWin: elif isWin:
# make sure ctypes is bundled # make sure ctypes is bundled
from ctypes import windll, wintypes from ctypes import windll, wintypes # type: ignore
_dummy = windll _dummy = windll
_dummy = wintypes _dummy = wintypes
def maybeHideAccelerators(self, tgt=None): def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None:
if not self.hideMenuAccels: if not self.hideMenuAccels:
return return
tgt = tgt or self tgt = tgt or self
@ -1324,7 +1333,7 @@ will be lost. Continue?"""))
if m: if m:
action.setText(m.group(1) + (m.group(2) or "")) action.setText(m.group(1) + (m.group(2) or ""))
def hideStatusTips(self): def hideStatusTips(self) -> None:
for action in self.findChildren(QAction): for action in self.findChildren(QAction):
action.setStatusTip("") action.setStatusTip("")
@ -1334,10 +1343,10 @@ will be lost. Continue?"""))
# Single instance support # Single instance support
########################################################################## ##########################################################################
def setupAppMsg(self): def setupAppMsg(self) -> None:
self.app.appMsg.connect(self.onAppMsg) self.app.appMsg.connect(self.onAppMsg)
def onAppMsg(self, buf): def onAppMsg(self, buf: str) -> Optional[QTimer]:
if self.state == "startup": if self.state == "startup":
# try again in a second # try again in a second
return self.progress.timer(1000, lambda: self.onAppMsg(buf), False, return self.progress.timer(1000, lambda: self.onAppMsg(buf), False,
@ -1345,7 +1354,7 @@ will be lost. Continue?"""))
elif self.state == "profileManager": elif self.state == "profileManager":
# can't raise window while in profile manager # can't raise window while in profile manager
if buf == "raise": if buf == "raise":
return return None
self.pendingImport = buf self.pendingImport = buf
return tooltip(_("Deck will be imported when a profile is opened.")) return tooltip(_("Deck will be imported when a profile is opened."))
if not self.interactiveState() or self.progress.busy(): if not self.interactiveState() or self.progress.busy():
@ -1354,7 +1363,7 @@ will be lost. Continue?"""))
showInfo(_("""\ showInfo(_("""\
Please ensure a profile is open and Anki is not busy, then try again."""), Please ensure a profile is open and Anki is not busy, then try again."""),
parent=None) parent=None)
return return None
# raise window # raise window
if isWin: if isWin:
# on windows we can raise the window by minimizing and restoring # on windows we can raise the window by minimizing and restoring
@ -1366,33 +1375,34 @@ Please ensure a profile is open and Anki is not busy, then try again."""),
self.activateWindow() self.activateWindow()
self.raise_() self.raise_()
if buf == "raise": if buf == "raise":
return return None
# import # import
self.handleImport(buf) self.handleImport(buf)
return None
# GC # GC
########################################################################## ##########################################################################
# ensure gc runs in main thread # ensure gc runs in main thread
def setupDialogGC(self, obj): def setupDialogGC(self, obj: Any) -> None:
obj.finished.connect(lambda: self.gcWindow(obj)) obj.finished.connect(lambda: self.gcWindow(obj)) # type: ignore
def gcWindow(self, obj): def gcWindow(self, obj: Any) -> None:
obj.deleteLater() obj.deleteLater()
self.progress.timer(1000, self.doGC, False, requiresCollection=False) self.progress.timer(1000, self.doGC, False, requiresCollection=False)
def disableGC(self): def disableGC(self) -> None:
gc.collect() gc.collect()
gc.disable() gc.disable()
def doGC(self): def doGC(self) -> None:
assert not self.progress.inDB assert not self.progress.inDB
gc.collect() gc.collect()
# Crash log # Crash log
########################################################################## ##########################################################################
def setupCrashLog(self): def setupCrashLog(self) -> None:
p = os.path.join(self.pm.base, "crash.log") p = os.path.join(self.pm.base, "crash.log")
self._crashLog = open(p, "ab", 0) self._crashLog = open(p, "ab", 0)
faulthandler.enable(self._crashLog) faulthandler.enable(self._crashLog)
@ -1400,12 +1410,12 @@ Please ensure a profile is open and Anki is not busy, then try again."""),
# Media server # Media server
########################################################################## ##########################################################################
def setupMediaServer(self): def setupMediaServer(self) -> None:
self.mediaServer = aqt.mediasrv.MediaServer(self) self.mediaServer = aqt.mediasrv.MediaServer(self)
self.mediaServer.start() self.mediaServer.start()
def baseHTML(self): def baseHTML(self) -> str:
return '<base href="%s">' % self.serverURL() return '<base href="%s">' % self.serverURL()
def serverURL(self): def serverURL(self) -> str:
return "http://127.0.0.1:%d/" % self.mediaServer.getPort() return "http://127.0.0.1:%d/" % self.mediaServer.getPort()