mirror of
https://github.com/ankitects/anki.git
synced 2025-11-07 13:17:12 -05:00
Merge remote-tracking branch 'danielelmes/master' into allow_to_define_python_bin
# Conflicts: # rspy/Makefile
This commit is contained in:
commit
f13d9dfb8a
17 changed files with 236 additions and 150 deletions
2
Makefile
2
Makefile
|
|
@ -51,7 +51,7 @@ pyenv:
|
||||||
# update build hash
|
# update build hash
|
||||||
.PHONY: buildhash
|
.PHONY: buildhash
|
||||||
buildhash:
|
buildhash:
|
||||||
oldhash=$$(test -f meta/buildhash && cat meta/buildhash || true); \
|
@oldhash=$$(test -f meta/buildhash && cat meta/buildhash || true); \
|
||||||
newhash=$$(git rev-parse --short=8 HEAD || echo dev); \
|
newhash=$$(git rev-parse --short=8 HEAD || echo dev); \
|
||||||
if [ "$$oldhash" != "$$newhash" ]; then \
|
if [ "$$oldhash" != "$$newhash" ]; then \
|
||||||
echo $$newhash > meta/buildhash; \
|
echo $$newhash > meta/buildhash; \
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@ For more information on building, please see README.development
|
||||||
|
|
||||||
If you'd like to contribute code, please see README.contributing
|
If you'd like to contribute code, please see README.contributing
|
||||||
|
|
||||||

|
[](../../actions)
|
||||||
|
|
|
||||||
|
|
@ -657,6 +657,7 @@ where c.nid = n.id and c.id in %s group by nid"""
|
||||||
self._startTime = time.time()
|
self._startTime = time.time()
|
||||||
self._startReps = self.sched.reps
|
self._startReps = self.sched.reps
|
||||||
|
|
||||||
|
# FIXME: Use Literal[False] when on Python 3.8
|
||||||
def timeboxReached(self) -> Union[bool, Tuple[Any, int]]:
|
def timeboxReached(self) -> Union[bool, Tuple[Any, int]]:
|
||||||
"Return (elapsedTime, reps) if timebox reached, or False."
|
"Return (elapsedTime, reps) if timebox reached, or False."
|
||||||
if not self.conf["timeLim"]:
|
if not self.conf["timeLim"]:
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,8 @@ class Scheduler:
|
||||||
counts = [self.newCount, self.lrnCount, self.revCount]
|
counts = [self.newCount, self.lrnCount, self.revCount]
|
||||||
if card:
|
if card:
|
||||||
idx = self.countIdx(card)
|
idx = self.countIdx(card)
|
||||||
if idx == 1:
|
if idx == QUEUE_TYPE_LRN:
|
||||||
counts[1] += card.left // 1000
|
counts[QUEUE_TYPE_LRN] += card.left // 1000
|
||||||
else:
|
else:
|
||||||
counts[idx] += 1
|
counts[idx] += 1
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ order by due"""
|
||||||
|
|
||||||
def countIdx(self, card: Card) -> int:
|
def countIdx(self, card: Card) -> int:
|
||||||
if card.queue == QUEUE_TYPE_DAY_LEARN_RELEARN:
|
if card.queue == QUEUE_TYPE_DAY_LEARN_RELEARN:
|
||||||
return 1
|
return QUEUE_TYPE_LRN
|
||||||
return card.queue
|
return card.queue
|
||||||
|
|
||||||
def answerButtons(self, card: Card) -> int:
|
def answerButtons(self, card: Card) -> int:
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ order by due"""
|
||||||
|
|
||||||
def countIdx(self, card: Card) -> int:
|
def countIdx(self, card: Card) -> int:
|
||||||
if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW):
|
if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW):
|
||||||
return 1
|
return QUEUE_TYPE_LRN
|
||||||
return card.queue
|
return card.queue
|
||||||
|
|
||||||
def answerButtons(self, card: Card) -> int:
|
def answerButtons(self, card: Card) -> int:
|
||||||
|
|
|
||||||
|
|
@ -1258,6 +1258,12 @@ class ConfigEditor(QDialog):
|
||||||
self.updateText(self.conf)
|
self.updateText(self.conf)
|
||||||
restoreGeom(self, "addonconf")
|
restoreGeom(self, "addonconf")
|
||||||
restoreSplitter(self.form.splitter, "addonconf")
|
restoreSplitter(self.form.splitter, "addonconf")
|
||||||
|
self.setWindowTitle(
|
||||||
|
tr(
|
||||||
|
TR.ADDONS_CONFIG_WINDOW_TITLE,
|
||||||
|
name=self.mgr.addon_meta(addon).human_name(),
|
||||||
|
)
|
||||||
|
)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def onRestoreDefaults(self):
|
def onRestoreDefaults(self):
|
||||||
|
|
|
||||||
|
|
@ -596,6 +596,7 @@ class Browser(QMainWindow):
|
||||||
self.updateFont()
|
self.updateFont()
|
||||||
self.onUndoState(self.mw.form.actionUndo.isEnabled())
|
self.onUndoState(self.mw.form.actionUndo.isEnabled())
|
||||||
self.setupSearch()
|
self.setupSearch()
|
||||||
|
gui_hooks.browser_will_show(self)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def setupMenus(self) -> None:
|
def setupMenus(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ class CardLayout(QDialog):
|
||||||
v1.addLayout(self.buttons)
|
v1.addLayout(self.buttons)
|
||||||
v1.setContentsMargins(12, 12, 12, 12)
|
v1.setContentsMargins(12, 12, 12, 12)
|
||||||
self.setLayout(v1)
|
self.setLayout(v1)
|
||||||
|
gui_hooks.card_layout_will_show(self)
|
||||||
self.redraw()
|
self.redraw()
|
||||||
restoreGeom(self, "CardLayout")
|
restoreGeom(self, "CardLayout")
|
||||||
self.setWindowModality(Qt.ApplicationModal)
|
self.setWindowModality(Qt.ApplicationModal)
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,30 @@ class _BrowserWillBuildTreeFilter:
|
||||||
browser_will_build_tree = _BrowserWillBuildTreeFilter()
|
browser_will_build_tree = _BrowserWillBuildTreeFilter()
|
||||||
|
|
||||||
|
|
||||||
|
class _BrowserWillShowHook:
|
||||||
|
_hooks: List[Callable[["aqt.browser.Browser"], None]] = []
|
||||||
|
|
||||||
|
def append(self, cb: Callable[["aqt.browser.Browser"], None]) -> None:
|
||||||
|
"""(browser: aqt.browser.Browser)"""
|
||||||
|
self._hooks.append(cb)
|
||||||
|
|
||||||
|
def remove(self, cb: Callable[["aqt.browser.Browser"], None]) -> None:
|
||||||
|
if cb in self._hooks:
|
||||||
|
self._hooks.remove(cb)
|
||||||
|
|
||||||
|
def __call__(self, browser: aqt.browser.Browser) -> None:
|
||||||
|
for hook in self._hooks:
|
||||||
|
try:
|
||||||
|
hook(browser)
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
self._hooks.remove(hook)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
browser_will_show = _BrowserWillShowHook()
|
||||||
|
|
||||||
|
|
||||||
class _BrowserWillShowContextMenuHook:
|
class _BrowserWillShowContextMenuHook:
|
||||||
_hooks: List[Callable[["aqt.browser.Browser", QMenu], None]] = []
|
_hooks: List[Callable[["aqt.browser.Browser", QMenu], None]] = []
|
||||||
|
|
||||||
|
|
@ -328,6 +352,33 @@ class _BrowserWillShowContextMenuHook:
|
||||||
browser_will_show_context_menu = _BrowserWillShowContextMenuHook()
|
browser_will_show_context_menu = _BrowserWillShowContextMenuHook()
|
||||||
|
|
||||||
|
|
||||||
|
class _CardLayoutWillShowHook:
|
||||||
|
"""Allow to change the display of the card layout. After most values are
|
||||||
|
set and before the window is actually shown."""
|
||||||
|
|
||||||
|
_hooks: List[Callable[["aqt.clayout.CardLayout"], None]] = []
|
||||||
|
|
||||||
|
def append(self, cb: Callable[["aqt.clayout.CardLayout"], None]) -> None:
|
||||||
|
"""(clayout: aqt.clayout.CardLayout)"""
|
||||||
|
self._hooks.append(cb)
|
||||||
|
|
||||||
|
def remove(self, cb: Callable[["aqt.clayout.CardLayout"], None]) -> None:
|
||||||
|
if cb in self._hooks:
|
||||||
|
self._hooks.remove(cb)
|
||||||
|
|
||||||
|
def __call__(self, clayout: aqt.clayout.CardLayout) -> None:
|
||||||
|
for hook in self._hooks:
|
||||||
|
try:
|
||||||
|
hook(clayout)
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
self._hooks.remove(hook)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
card_layout_will_show = _CardLayoutWillShowHook()
|
||||||
|
|
||||||
|
|
||||||
class _CardWillShowFilter:
|
class _CardWillShowFilter:
|
||||||
"""Can modify card text before review/preview."""
|
"""Can modify card text before review/preview."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import difflib
|
import difflib
|
||||||
import html
|
import html
|
||||||
import html.parser
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import unicodedata as ucd
|
import unicodedata as ucd
|
||||||
from typing import List, Optional
|
from typing import Callable, List, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
|
@ -25,7 +26,7 @@ from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, toolti
|
||||||
|
|
||||||
|
|
||||||
class ReviewerBottomBar:
|
class ReviewerBottomBar:
|
||||||
def __init__(self, reviewer: Reviewer):
|
def __init__(self, reviewer: Reviewer) -> None:
|
||||||
self.reviewer = reviewer
|
self.reviewer = reviewer
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,28 +40,29 @@ class Reviewer:
|
||||||
self.cardQueue: List[Card] = []
|
self.cardQueue: List[Card] = []
|
||||||
self.hadCardQueue = False
|
self.hadCardQueue = False
|
||||||
self._answeredIds: List[int] = []
|
self._answeredIds: List[int] = []
|
||||||
self._recordedAudio = None
|
self._recordedAudio: Optional[str] = None
|
||||||
self.typeCorrect = None # web init happens before this is set
|
self.typeCorrect: str = None # web init happens before this is set
|
||||||
self.state: Optional[str] = None
|
self.state: Optional[str] = None
|
||||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||||
hooks.card_did_leech.append(self.onLeech)
|
hooks.card_did_leech.append(self.onLeech)
|
||||||
|
|
||||||
def show(self):
|
def show(self) -> None:
|
||||||
self.mw.col.reset()
|
self.mw.col.reset()
|
||||||
self.mw.setStateShortcuts(self._shortcutKeys())
|
self.mw.setStateShortcuts(self._shortcutKeys()) # type: ignore
|
||||||
self.web.set_bridge_command(self._linkHandler, self)
|
self.web.set_bridge_command(self._linkHandler, self)
|
||||||
self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
|
self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
|
||||||
self._reps = None
|
self._reps: int = None
|
||||||
self.nextCard()
|
self.nextCard()
|
||||||
|
|
||||||
def lastCard(self):
|
def lastCard(self) -> Optional[Card]:
|
||||||
if self._answeredIds:
|
if self._answeredIds:
|
||||||
if not self.card or self._answeredIds[-1] != self.card.id:
|
if not self.card or self._answeredIds[-1] != self.card.id:
|
||||||
try:
|
try:
|
||||||
return self.mw.col.getCard(self._answeredIds[-1])
|
return self.mw.col.getCard(self._answeredIds[-1])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# id was deleted
|
# id was deleted
|
||||||
return
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
gui_hooks.reviewer_will_end()
|
gui_hooks.reviewer_will_end()
|
||||||
|
|
@ -68,9 +70,10 @@ class Reviewer:
|
||||||
# Fetching a card
|
# Fetching a card
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def nextCard(self):
|
def nextCard(self) -> None:
|
||||||
elapsed = self.mw.col.timeboxReached()
|
elapsed = self.mw.col.timeboxReached()
|
||||||
if elapsed:
|
if elapsed:
|
||||||
|
assert not isinstance(elapsed, bool)
|
||||||
part1 = (
|
part1 = (
|
||||||
ngettext("%d card studied in", "%d cards studied in", elapsed[1])
|
ngettext("%d card studied in", "%d cards studied in", elapsed[1])
|
||||||
% elapsed[1]
|
% elapsed[1]
|
||||||
|
|
@ -125,7 +128,7 @@ class Reviewer:
|
||||||
# Initializing the webview
|
# Initializing the webview
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def revHtml(self):
|
def revHtml(self) -> str:
|
||||||
extra = self.mw.col.conf.get("reviewExtra", "")
|
extra = self.mw.col.conf.get("reviewExtra", "")
|
||||||
fade = ""
|
fade = ""
|
||||||
if self.mw.pm.glMode() == "software":
|
if self.mw.pm.glMode() == "software":
|
||||||
|
|
@ -140,7 +143,7 @@ class Reviewer:
|
||||||
fade, extra
|
fade, extra
|
||||||
)
|
)
|
||||||
|
|
||||||
def _initWeb(self):
|
def _initWeb(self) -> None:
|
||||||
self._reps = 0
|
self._reps = 0
|
||||||
# main window
|
# main window
|
||||||
self.web.stdHtml(
|
self.web.stdHtml(
|
||||||
|
|
@ -167,13 +170,13 @@ class Reviewer:
|
||||||
# Showing the question
|
# Showing the question
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _mungeQA(self, buf):
|
def _mungeQA(self, buf: str) -> str:
|
||||||
return self.typeAnsFilter(self.mw.prepare_card_text_for_display(buf))
|
return self.typeAnsFilter(self.mw.prepare_card_text_for_display(buf))
|
||||||
|
|
||||||
def _showQuestion(self) -> None:
|
def _showQuestion(self) -> None:
|
||||||
self._reps += 1
|
self._reps += 1
|
||||||
self.state = "question"
|
self.state = "question"
|
||||||
self.typedAnswer = None
|
self.typedAnswer: str = None
|
||||||
c = self.card
|
c = self.card
|
||||||
# grab the question and play audio
|
# grab the question and play audio
|
||||||
if c.isEmpty():
|
if c.isEmpty():
|
||||||
|
|
@ -206,17 +209,17 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
# user hook
|
# user hook
|
||||||
gui_hooks.reviewer_did_show_question(c)
|
gui_hooks.reviewer_did_show_question(c)
|
||||||
|
|
||||||
def autoplay(self, card):
|
def autoplay(self, card: Card) -> bool:
|
||||||
return self.mw.col.decks.confForDid(card.odid or card.did)["autoplay"]
|
return self.mw.col.decks.confForDid(card.odid or card.did)["autoplay"]
|
||||||
|
|
||||||
def _replayq(self, card, previewer=None):
|
def _replayq(self, card, previewer=None):
|
||||||
s = previewer if previewer else self
|
s = previewer if previewer else self
|
||||||
return s.mw.col.decks.confForDid(s.card.odid or s.card.did).get("replayq", True)
|
return s.mw.col.decks.confForDid(s.card.odid or s.card.did).get("replayq", True)
|
||||||
|
|
||||||
def _drawFlag(self):
|
def _drawFlag(self) -> None:
|
||||||
self.web.eval("_drawFlag(%s);" % self.card.userFlag())
|
self.web.eval("_drawFlag(%s);" % self.card.userFlag())
|
||||||
|
|
||||||
def _drawMark(self):
|
def _drawMark(self) -> None:
|
||||||
self.web.eval("_drawMark(%s);" % json.dumps(self.card.note().hasTag("marked")))
|
self.web.eval("_drawMark(%s);" % json.dumps(self.card.note().hasTag("marked")))
|
||||||
|
|
||||||
# Showing the answer
|
# Showing the answer
|
||||||
|
|
@ -246,7 +249,7 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
# Answering a card
|
# Answering a card
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
def _answerCard(self, ease):
|
def _answerCard(self, ease: int) -> None:
|
||||||
"Reschedule card and show next."
|
"Reschedule card and show next."
|
||||||
if self.mw.state != "review":
|
if self.mw.state != "review":
|
||||||
# showing resetRequired screen; ignore key
|
# showing resetRequired screen; ignore key
|
||||||
|
|
@ -269,7 +272,9 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
# Handlers
|
# Handlers
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
def _shortcutKeys(self):
|
def _shortcutKeys(
|
||||||
|
self,
|
||||||
|
) -> List[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]:
|
||||||
return [
|
return [
|
||||||
("e", self.mw.onEditCurrent),
|
("e", self.mw.onEditCurrent),
|
||||||
(" ", self.onEnterKey),
|
(" ", self.onEnterKey),
|
||||||
|
|
@ -299,18 +304,18 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
("7", self.on_seek_forward),
|
("7", self.on_seek_forward),
|
||||||
]
|
]
|
||||||
|
|
||||||
def on_pause_audio(self):
|
def on_pause_audio(self) -> None:
|
||||||
av_player.toggle_pause()
|
av_player.toggle_pause()
|
||||||
|
|
||||||
seek_secs = 5
|
seek_secs = 5
|
||||||
|
|
||||||
def on_seek_backward(self):
|
def on_seek_backward(self) -> None:
|
||||||
av_player.seek_relative(-self.seek_secs)
|
av_player.seek_relative(-self.seek_secs)
|
||||||
|
|
||||||
def on_seek_forward(self):
|
def on_seek_forward(self) -> None:
|
||||||
av_player.seek_relative(self.seek_secs)
|
av_player.seek_relative(self.seek_secs)
|
||||||
|
|
||||||
def onEnterKey(self):
|
def onEnterKey(self) -> None:
|
||||||
if self.state == "question":
|
if self.state == "question":
|
||||||
self._getTypedAnswer()
|
self._getTypedAnswer()
|
||||||
elif self.state == "answer":
|
elif self.state == "answer":
|
||||||
|
|
@ -318,14 +323,14 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
"selectedAnswerButton()", self._onAnswerButton
|
"selectedAnswerButton()", self._onAnswerButton
|
||||||
)
|
)
|
||||||
|
|
||||||
def _onAnswerButton(self, val):
|
def _onAnswerButton(self, val: str) -> None:
|
||||||
# button selected?
|
# button selected?
|
||||||
if val and val in "1234":
|
if val and val in "1234":
|
||||||
self._answerCard(int(val))
|
self._answerCard(int(val))
|
||||||
else:
|
else:
|
||||||
self._answerCard(self._defaultEase())
|
self._answerCard(self._defaultEase())
|
||||||
|
|
||||||
def _linkHandler(self, url):
|
def _linkHandler(self, url: str) -> None:
|
||||||
if url == "ans":
|
if url == "ans":
|
||||||
self._getTypedAnswer()
|
self._getTypedAnswer()
|
||||||
elif url.startswith("ease"):
|
elif url.startswith("ease"):
|
||||||
|
|
@ -344,13 +349,13 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||||
|
|
||||||
typeAnsPat = r"\[\[type:(.+?)\]\]"
|
typeAnsPat = r"\[\[type:(.+?)\]\]"
|
||||||
|
|
||||||
def typeAnsFilter(self, buf):
|
def typeAnsFilter(self, buf: str) -> str:
|
||||||
if self.state == "question":
|
if self.state == "question":
|
||||||
return self.typeAnsQuestionFilter(buf)
|
return self.typeAnsQuestionFilter(buf)
|
||||||
else:
|
else:
|
||||||
return self.typeAnsAnswerFilter(buf)
|
return self.typeAnsAnswerFilter(buf)
|
||||||
|
|
||||||
def typeAnsQuestionFilter(self, buf):
|
def typeAnsQuestionFilter(self, buf: str) -> str:
|
||||||
self.typeCorrect = None
|
self.typeCorrect = None
|
||||||
clozeIdx = None
|
clozeIdx = None
|
||||||
m = re.search(self.typeAnsPat, buf)
|
m = re.search(self.typeAnsPat, buf)
|
||||||
|
|
@ -397,20 +402,19 @@ Please run Tools>Empty Cards"""
|
||||||
buf,
|
buf,
|
||||||
)
|
)
|
||||||
|
|
||||||
def typeAnsAnswerFilter(self, buf):
|
def typeAnsAnswerFilter(self, buf: str) -> str:
|
||||||
if not self.typeCorrect:
|
if not self.typeCorrect:
|
||||||
return re.sub(self.typeAnsPat, "", buf)
|
return re.sub(self.typeAnsPat, "", buf)
|
||||||
origSize = len(buf)
|
origSize = len(buf)
|
||||||
buf = buf.replace("<hr id=answer>", "")
|
buf = buf.replace("<hr id=answer>", "")
|
||||||
hadHR = len(buf) != origSize
|
hadHR = len(buf) != origSize
|
||||||
# munge correct value
|
# munge correct value
|
||||||
parser = html.parser.HTMLParser()
|
|
||||||
cor = self.mw.col.media.strip(self.typeCorrect)
|
cor = self.mw.col.media.strip(self.typeCorrect)
|
||||||
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
|
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
|
||||||
cor = stripHTML(cor)
|
cor = stripHTML(cor)
|
||||||
# ensure we don't chomp multiple whitespace
|
# ensure we don't chomp multiple whitespace
|
||||||
cor = cor.replace(" ", " ")
|
cor = cor.replace(" ", " ")
|
||||||
cor = parser.unescape(cor)
|
cor = html.unescape(cor)
|
||||||
cor = cor.replace("\xa0", " ")
|
cor = cor.replace("\xa0", " ")
|
||||||
cor = cor.strip()
|
cor = cor.strip()
|
||||||
given = self.typedAnswer
|
given = self.typedAnswer
|
||||||
|
|
@ -434,7 +438,7 @@ Please run Tools>Empty Cards"""
|
||||||
|
|
||||||
return re.sub(self.typeAnsPat, repl, buf)
|
return re.sub(self.typeAnsPat, repl, buf)
|
||||||
|
|
||||||
def _contentForCloze(self, txt, idx):
|
def _contentForCloze(self, txt: str, idx) -> str:
|
||||||
matches = re.findall(r"\{\{c%s::(.+?)\}\}" % idx, txt, re.DOTALL)
|
matches = re.findall(r"\{\{c%s::(.+?)\}\}" % idx, txt, re.DOTALL)
|
||||||
if not matches:
|
if not matches:
|
||||||
return None
|
return None
|
||||||
|
|
@ -452,24 +456,28 @@ Please run Tools>Empty Cards"""
|
||||||
txt = ", ".join(matches)
|
txt = ", ".join(matches)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def tokenizeComparison(self, given, correct):
|
def tokenizeComparison(
|
||||||
|
self, given: str, correct: str
|
||||||
|
) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]:
|
||||||
# compare in NFC form so accents appear correct
|
# compare in NFC form so accents appear correct
|
||||||
given = ucd.normalize("NFC", given)
|
given = ucd.normalize("NFC", given)
|
||||||
correct = ucd.normalize("NFC", correct)
|
correct = ucd.normalize("NFC", correct)
|
||||||
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
|
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
|
||||||
givenElems = []
|
givenElems: List[Tuple[bool, str]] = []
|
||||||
correctElems = []
|
correctElems: List[Tuple[bool, str]] = []
|
||||||
givenPoint = 0
|
givenPoint = 0
|
||||||
correctPoint = 0
|
correctPoint = 0
|
||||||
offby = 0
|
offby = 0
|
||||||
|
|
||||||
def logBad(old, new, str, array):
|
def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None:
|
||||||
if old != new:
|
if old != new:
|
||||||
array.append((False, str[old:new]))
|
array.append((False, s[old:new]))
|
||||||
|
|
||||||
def logGood(start, cnt, str, array):
|
def logGood(
|
||||||
|
start: int, cnt: int, s: str, array: List[Tuple[bool, str]]
|
||||||
|
) -> None:
|
||||||
if cnt:
|
if cnt:
|
||||||
array.append((True, str[start : start + cnt]))
|
array.append((True, s[start : start + cnt]))
|
||||||
|
|
||||||
for x, y, cnt in s.get_matching_blocks():
|
for x, y, cnt in s.get_matching_blocks():
|
||||||
# if anything was missed in correct, pad given
|
# if anything was missed in correct, pad given
|
||||||
|
|
@ -486,17 +494,17 @@ Please run Tools>Empty Cards"""
|
||||||
logGood(y, cnt, correct, correctElems)
|
logGood(y, cnt, correct, correctElems)
|
||||||
return givenElems, correctElems
|
return givenElems, correctElems
|
||||||
|
|
||||||
def correct(self, given, correct, showBad=True):
|
def correct(self, given: str, correct: str, showBad: bool = True) -> str:
|
||||||
"Diff-corrects the typed-in answer."
|
"Diff-corrects the typed-in answer."
|
||||||
givenElems, correctElems = self.tokenizeComparison(given, correct)
|
givenElems, correctElems = self.tokenizeComparison(given, correct)
|
||||||
|
|
||||||
def good(s):
|
def good(s: str) -> str:
|
||||||
return "<span class=typeGood>" + html.escape(s) + "</span>"
|
return "<span class=typeGood>" + html.escape(s) + "</span>"
|
||||||
|
|
||||||
def bad(s):
|
def bad(s: str) -> str:
|
||||||
return "<span class=typeBad>" + html.escape(s) + "</span>"
|
return "<span class=typeBad>" + html.escape(s) + "</span>"
|
||||||
|
|
||||||
def missed(s):
|
def missed(s: str) -> str:
|
||||||
return "<span class=typeMissed>" + html.escape(s) + "</span>"
|
return "<span class=typeMissed>" + html.escape(s) + "</span>"
|
||||||
|
|
||||||
if given == correct:
|
if given == correct:
|
||||||
|
|
@ -519,24 +527,24 @@ Please run Tools>Empty Cards"""
|
||||||
res = "<div><code id=typeans>" + res + "</code></div>"
|
res = "<div><code id=typeans>" + res + "</code></div>"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _noLoneMarks(self, s):
|
def _noLoneMarks(self, s: str) -> str:
|
||||||
# ensure a combining character at the start does not join to
|
# ensure a combining character at the start does not join to
|
||||||
# previous text
|
# previous text
|
||||||
if s and ucd.category(s[0]).startswith("M"):
|
if s and ucd.category(s[0]).startswith("M"):
|
||||||
return "\xa0" + s
|
return "\xa0" + s
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def _getTypedAnswer(self):
|
def _getTypedAnswer(self) -> None:
|
||||||
self.web.evalWithCallback("typeans ? typeans.value : null", self._onTypedAnswer)
|
self.web.evalWithCallback("typeans ? typeans.value : null", self._onTypedAnswer)
|
||||||
|
|
||||||
def _onTypedAnswer(self, val):
|
def _onTypedAnswer(self, val: None) -> None:
|
||||||
self.typedAnswer = val or ""
|
self.typedAnswer = val or ""
|
||||||
self._showAnswer()
|
self._showAnswer()
|
||||||
|
|
||||||
# Bottom bar
|
# Bottom bar
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _bottomHTML(self):
|
def _bottomHTML(self) -> str:
|
||||||
return """
|
return """
|
||||||
<center id=outer>
|
<center id=outer>
|
||||||
<table id=innertable width=100%% cellspacing=0 cellpadding=0>
|
<table id=innertable width=100%% cellspacing=0 cellpadding=0>
|
||||||
|
|
@ -565,7 +573,7 @@ time = %(time)d;
|
||||||
time=self.card.timeTaken() // 1000,
|
time=self.card.timeTaken() // 1000,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _showAnswerButton(self):
|
def _showAnswerButton(self) -> None:
|
||||||
if not self.typeCorrect:
|
if not self.typeCorrect:
|
||||||
self.bottom.web.setFocus()
|
self.bottom.web.setFocus()
|
||||||
middle = """
|
middle = """
|
||||||
|
|
@ -587,17 +595,17 @@ time = %(time)d;
|
||||||
self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime))
|
self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime))
|
||||||
self.bottom.web.adjustHeightToFit()
|
self.bottom.web.adjustHeightToFit()
|
||||||
|
|
||||||
def _showEaseButtons(self):
|
def _showEaseButtons(self) -> None:
|
||||||
self.bottom.web.setFocus()
|
self.bottom.web.setFocus()
|
||||||
middle = self._answerButtons()
|
middle = self._answerButtons()
|
||||||
self.bottom.web.eval("showAnswer(%s);" % json.dumps(middle))
|
self.bottom.web.eval("showAnswer(%s);" % json.dumps(middle))
|
||||||
|
|
||||||
def _remaining(self):
|
def _remaining(self) -> str:
|
||||||
if not self.mw.col.conf["dueCounts"]:
|
if not self.mw.col.conf["dueCounts"]:
|
||||||
return ""
|
return ""
|
||||||
if self.hadCardQueue:
|
if self.hadCardQueue:
|
||||||
# if it's come from the undo queue, don't count it separately
|
# if it's come from the undo queue, don't count it separately
|
||||||
counts = list(self.mw.col.sched.counts())
|
counts: List[Union[int, str]] = list(self.mw.col.sched.counts())
|
||||||
else:
|
else:
|
||||||
counts = list(self.mw.col.sched.counts(self.card))
|
counts = list(self.mw.col.sched.counts(self.card))
|
||||||
idx = self.mw.col.sched.countIdx(self.card)
|
idx = self.mw.col.sched.countIdx(self.card)
|
||||||
|
|
@ -608,13 +616,13 @@ time = %(time)d;
|
||||||
ctxt += space + "<span class=review-count>%s</span>" % counts[2]
|
ctxt += space + "<span class=review-count>%s</span>" % counts[2]
|
||||||
return ctxt
|
return ctxt
|
||||||
|
|
||||||
def _defaultEase(self):
|
def _defaultEase(self) -> int:
|
||||||
if self.mw.col.sched.answerButtons(self.card) == 4:
|
if self.mw.col.sched.answerButtons(self.card) == 4:
|
||||||
return 3
|
return 3
|
||||||
else:
|
else:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def _answerButtonList(self):
|
def _answerButtonList(self) -> Sequence[Tuple[int, str]]:
|
||||||
l = ((1, _("Again")),)
|
l = ((1, _("Again")),)
|
||||||
cnt = self.mw.col.sched.answerButtons(self.card)
|
cnt = self.mw.col.sched.answerButtons(self.card)
|
||||||
if cnt == 2:
|
if cnt == 2:
|
||||||
|
|
@ -624,7 +632,7 @@ time = %(time)d;
|
||||||
else:
|
else:
|
||||||
return l + ((2, _("Hard")), (3, _("Good")), (4, _("Easy")))
|
return l + ((2, _("Hard")), (3, _("Good")), (4, _("Easy")))
|
||||||
|
|
||||||
def _answerButtons(self):
|
def _answerButtons(self) -> str:
|
||||||
default = self._defaultEase()
|
default = self._defaultEase()
|
||||||
|
|
||||||
def but(i, label):
|
def but(i, label):
|
||||||
|
|
@ -652,7 +660,7 @@ time = %(time)d;
|
||||||
<script>$(function () { $("#defease").focus(); });</script>"""
|
<script>$(function () { $("#defease").focus(); });</script>"""
|
||||||
return buf + script
|
return buf + script
|
||||||
|
|
||||||
def _buttonTime(self, i):
|
def _buttonTime(self, i: int) -> str:
|
||||||
if not self.mw.col.conf["estTimes"]:
|
if not self.mw.col.conf["estTimes"]:
|
||||||
return "<div class=spacer></div>"
|
return "<div class=spacer></div>"
|
||||||
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " "
|
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " "
|
||||||
|
|
@ -661,7 +669,7 @@ time = %(time)d;
|
||||||
# Leeches
|
# Leeches
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def onLeech(self, card):
|
def onLeech(self, card: Card) -> None:
|
||||||
# for now
|
# for now
|
||||||
s = _("Card was a leech.")
|
s = _("Card was a leech.")
|
||||||
if card.queue < 0:
|
if card.queue < 0:
|
||||||
|
|
@ -730,7 +738,7 @@ time = %(time)d;
|
||||||
qtMenuShortcutWorkaround(m)
|
qtMenuShortcutWorkaround(m)
|
||||||
m.exec_(QCursor.pos())
|
m.exec_(QCursor.pos())
|
||||||
|
|
||||||
def _addMenuItems(self, m, rows):
|
def _addMenuItems(self, m, rows) -> None:
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if not row:
|
if not row:
|
||||||
m.addSeparator()
|
m.addSeparator()
|
||||||
|
|
@ -753,10 +761,10 @@ time = %(time)d;
|
||||||
a.setChecked(True)
|
a.setChecked(True)
|
||||||
a.triggered.connect(func)
|
a.triggered.connect(func)
|
||||||
|
|
||||||
def onOptions(self):
|
def onOptions(self) -> None:
|
||||||
self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did))
|
self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did))
|
||||||
|
|
||||||
def setFlag(self, flag):
|
def setFlag(self, flag: int) -> None:
|
||||||
# need to toggle off?
|
# need to toggle off?
|
||||||
if self.card.userFlag() == flag:
|
if self.card.userFlag() == flag:
|
||||||
flag = 0
|
flag = 0
|
||||||
|
|
@ -764,7 +772,7 @@ time = %(time)d;
|
||||||
self.card.flush()
|
self.card.flush()
|
||||||
self._drawFlag()
|
self._drawFlag()
|
||||||
|
|
||||||
def onMark(self):
|
def onMark(self) -> None:
|
||||||
f = self.card.note()
|
f = self.card.note()
|
||||||
if f.hasTag("marked"):
|
if f.hasTag("marked"):
|
||||||
f.delTag("marked")
|
f.delTag("marked")
|
||||||
|
|
@ -773,19 +781,19 @@ time = %(time)d;
|
||||||
f.flush()
|
f.flush()
|
||||||
self._drawMark()
|
self._drawMark()
|
||||||
|
|
||||||
def onSuspend(self):
|
def onSuspend(self) -> None:
|
||||||
self.mw.checkpoint(_("Suspend"))
|
self.mw.checkpoint(_("Suspend"))
|
||||||
self.mw.col.sched.suspendCards([c.id for c in self.card.note().cards()])
|
self.mw.col.sched.suspendCards([c.id for c in self.card.note().cards()])
|
||||||
tooltip(_("Note suspended."))
|
tooltip(_("Note suspended."))
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
|
|
||||||
def onSuspendCard(self):
|
def onSuspendCard(self) -> None:
|
||||||
self.mw.checkpoint(_("Suspend"))
|
self.mw.checkpoint(_("Suspend"))
|
||||||
self.mw.col.sched.suspendCards([self.card.id])
|
self.mw.col.sched.suspendCards([self.card.id])
|
||||||
tooltip(_("Card suspended."))
|
tooltip(_("Card suspended."))
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
|
|
||||||
def onDelete(self):
|
def onDelete(self) -> None:
|
||||||
# need to check state because the shortcut is global to the main
|
# need to check state because the shortcut is global to the main
|
||||||
# window
|
# window
|
||||||
if self.mw.state != "review" or not self.card:
|
if self.mw.state != "review" or not self.card:
|
||||||
|
|
@ -801,23 +809,24 @@ time = %(time)d;
|
||||||
% cnt
|
% cnt
|
||||||
)
|
)
|
||||||
|
|
||||||
def onBuryCard(self):
|
def onBuryCard(self) -> None:
|
||||||
self.mw.checkpoint(_("Bury"))
|
self.mw.checkpoint(_("Bury"))
|
||||||
self.mw.col.sched.buryCards([self.card.id])
|
self.mw.col.sched.buryCards([self.card.id])
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
tooltip(_("Card buried."))
|
tooltip(_("Card buried."))
|
||||||
|
|
||||||
def onBuryNote(self):
|
def onBuryNote(self) -> None:
|
||||||
self.mw.checkpoint(_("Bury"))
|
self.mw.checkpoint(_("Bury"))
|
||||||
self.mw.col.sched.buryNote(self.card.nid)
|
self.mw.col.sched.buryNote(self.card.nid)
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
tooltip(_("Note buried."))
|
tooltip(_("Note buried."))
|
||||||
|
|
||||||
def onRecordVoice(self):
|
def onRecordVoice(self) -> None:
|
||||||
self._recordedAudio = getAudio(self.mw, encode=False)
|
self._recordedAudio = getAudio(self.mw, encode=False)
|
||||||
self.onReplayRecorded()
|
self.onReplayRecorded()
|
||||||
|
|
||||||
def onReplayRecorded(self):
|
def onReplayRecorded(self) -> None:
|
||||||
if not self._recordedAudio:
|
if not self._recordedAudio:
|
||||||
return tooltip(_("You haven't recorded your voice yet."))
|
tooltip(_("You haven't recorded your voice yet."))
|
||||||
|
return
|
||||||
av_player.play_file(self._recordedAudio)
|
av_player.play_file(self._recordedAudio)
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class AVPlayer:
|
||||||
# audio be stopped?
|
# audio be stopped?
|
||||||
interrupt_current_audio = True
|
interrupt_current_audio = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._enqueued: List[AVTag] = []
|
self._enqueued: List[AVTag] = []
|
||||||
self.current_player: Optional[Player] = None
|
self.current_player: Optional[Player] = None
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ class AVPlayer:
|
||||||
self._enqueued.insert(0, SoundOrVideoTag(filename=filename))
|
self._enqueued.insert(0, SoundOrVideoTag(filename=filename))
|
||||||
self._play_next_if_idle()
|
self._play_next_if_idle()
|
||||||
|
|
||||||
def toggle_pause(self):
|
def toggle_pause(self) -> None:
|
||||||
if self.current_player:
|
if self.current_player:
|
||||||
self.current_player.toggle_pause()
|
self.current_player.toggle_pause()
|
||||||
|
|
||||||
|
|
@ -179,7 +179,7 @@ av_player = AVPlayer()
|
||||||
|
|
||||||
# return modified command array that points to bundled command, and return
|
# return modified command array that points to bundled command, and return
|
||||||
# required environment
|
# required environment
|
||||||
def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
|
def _packagedCmd(cmd: List[str]) -> Tuple[Any, Dict[str, str]]:
|
||||||
cmd = cmd[:]
|
cmd = cmd[:]
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
if "LD_LIBRARY_PATH" in env:
|
if "LD_LIBRARY_PATH" in env:
|
||||||
|
|
@ -205,7 +205,7 @@ def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
|
||||||
si = startup_info()
|
si = startup_info()
|
||||||
|
|
||||||
# osx throws interrupted system call errors frequently
|
# osx throws interrupted system call errors frequently
|
||||||
def retryWait(proc) -> Any:
|
def retryWait(proc: subprocess.Popen) -> int:
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
return proc.wait()
|
return proc.wait()
|
||||||
|
|
@ -227,7 +227,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
|
||||||
args: List[str] = []
|
args: List[str] = []
|
||||||
env: Optional[Dict[str, str]] = None
|
env: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
def __init__(self, taskman: TaskManager):
|
def __init__(self, taskman: TaskManager) -> None:
|
||||||
self._taskman = taskman
|
self._taskman = taskman
|
||||||
self._terminate_flag = False
|
self._terminate_flag = False
|
||||||
self._process: Optional[subprocess.Popen] = None
|
self._process: Optional[subprocess.Popen] = None
|
||||||
|
|
@ -238,7 +238,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
|
||||||
lambda: self._play(tag), lambda res: self._on_done(res, on_done)
|
lambda: self._play(tag), lambda res: self._on_done(res, on_done)
|
||||||
)
|
)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self) -> None:
|
||||||
self._terminate_flag = True
|
self._terminate_flag = True
|
||||||
# block until stopped
|
# block until stopped
|
||||||
t = time.time()
|
t = time.time()
|
||||||
|
|
@ -252,7 +252,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
|
||||||
)
|
)
|
||||||
self._wait_for_termination(tag)
|
self._wait_for_termination(tag)
|
||||||
|
|
||||||
def _wait_for_termination(self, tag: AVTag):
|
def _wait_for_termination(self, tag: AVTag) -> None:
|
||||||
self._taskman.run_on_main(
|
self._taskman.run_on_main(
|
||||||
lambda: gui_hooks.av_player_did_begin_playing(self, tag)
|
lambda: gui_hooks.av_player_did_begin_playing(self, tag)
|
||||||
)
|
)
|
||||||
|
|
@ -359,7 +359,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
|
||||||
def toggle_pause(self) -> None:
|
def toggle_pause(self) -> None:
|
||||||
self.set_property("pause", not self.get_property("pause"))
|
self.set_property("pause", not self.get_property("pause"))
|
||||||
|
|
||||||
def seek_relative(self, secs) -> None:
|
def seek_relative(self, secs: int) -> None:
|
||||||
self.command("seek", secs, "relative")
|
self.command("seek", secs, "relative")
|
||||||
|
|
||||||
def on_idle(self) -> None:
|
def on_idle(self) -> None:
|
||||||
|
|
@ -401,7 +401,7 @@ class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
|
||||||
)
|
)
|
||||||
self._wait_for_termination(tag)
|
self._wait_for_termination(tag)
|
||||||
|
|
||||||
def command(self, *args) -> None:
|
def command(self, *args: Any) -> None:
|
||||||
"""Send a command over the slave interface.
|
"""Send a command over the slave interface.
|
||||||
|
|
||||||
The trailing newline is automatically added."""
|
The trailing newline is automatically added."""
|
||||||
|
|
@ -412,7 +412,7 @@ class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
|
||||||
def seek_relative(self, secs: int) -> None:
|
def seek_relative(self, secs: int) -> None:
|
||||||
self.command("seek", secs, 0)
|
self.command("seek", secs, 0)
|
||||||
|
|
||||||
def toggle_pause(self):
|
def toggle_pause(self) -> None:
|
||||||
self.command("pause")
|
self.command("pause")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -458,12 +458,12 @@ class _Recorder:
|
||||||
|
|
||||||
|
|
||||||
class PyAudioThreadedRecorder(threading.Thread):
|
class PyAudioThreadedRecorder(threading.Thread):
|
||||||
def __init__(self, startupDelay) -> None:
|
def __init__(self, startupDelay: float) -> None:
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
self.startupDelay = startupDelay
|
self.startupDelay = startupDelay
|
||||||
self.finish = False
|
self.finish = False
|
||||||
|
|
||||||
def run(self) -> Any:
|
def run(self) -> None:
|
||||||
chunk = 1024
|
chunk = 1024
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
|
|
||||||
|
|
@ -499,7 +499,7 @@ class PyAudioRecorder(_Recorder):
|
||||||
# discard first 250ms which may have pops/cracks
|
# discard first 250ms which may have pops/cracks
|
||||||
startupDelay = 0.25
|
startupDelay = 0.25
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
for t in recFiles + [processingSrc, processingDst]:
|
for t in recFiles + [processingSrc, processingDst]:
|
||||||
try:
|
try:
|
||||||
os.unlink(t)
|
os.unlink(t)
|
||||||
|
|
@ -507,15 +507,15 @@ class PyAudioRecorder(_Recorder):
|
||||||
pass
|
pass
|
||||||
self.encode = False
|
self.encode = False
|
||||||
|
|
||||||
def start(self):
|
def start(self) -> None:
|
||||||
self.thread = PyAudioThreadedRecorder(startupDelay=self.startupDelay)
|
self.thread = PyAudioThreadedRecorder(startupDelay=self.startupDelay)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self) -> None:
|
||||||
self.thread.finish = True
|
self.thread.finish = True
|
||||||
self.thread.join()
|
self.thread.join()
|
||||||
|
|
||||||
def file(self):
|
def file(self) -> str:
|
||||||
if self.encode:
|
if self.encode:
|
||||||
tgt = "rec%d.mp3" % time.time()
|
tgt = "rec%d.mp3" % time.time()
|
||||||
os.rename(processingDst, tgt)
|
os.rename(processingDst, tgt)
|
||||||
|
|
@ -530,7 +530,7 @@ Recorder = PyAudioRecorder
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
def getAudio(parent, encode=True):
|
def getAudio(parent: QWidget, encode: bool = True) -> Optional[str]:
|
||||||
"Record and return filename"
|
"Record and return filename"
|
||||||
# record first
|
# record first
|
||||||
r = Recorder()
|
r = Recorder()
|
||||||
|
|
@ -547,16 +547,16 @@ def getAudio(parent, encode=True):
|
||||||
t = time.time()
|
t = time.time()
|
||||||
r.start()
|
r.start()
|
||||||
time.sleep(r.startupDelay)
|
time.sleep(r.startupDelay)
|
||||||
QApplication.instance().processEvents()
|
QApplication.instance().processEvents() # type: ignore
|
||||||
while not mb.clickedButton():
|
while not mb.clickedButton():
|
||||||
txt = _("Recording...<br>Time: %0.1f")
|
txt = _("Recording...<br>Time: %0.1f")
|
||||||
mb.setText(txt % (time.time() - t))
|
mb.setText(txt % (time.time() - t))
|
||||||
mb.show()
|
mb.show()
|
||||||
QApplication.instance().processEvents()
|
QApplication.instance().processEvents() # type: ignore
|
||||||
if mb.clickedButton() == mb.escapeButton():
|
if mb.clickedButton() == mb.escapeButton():
|
||||||
r.stop()
|
r.stop()
|
||||||
r.cleanup()
|
r.cleanup()
|
||||||
return
|
return None
|
||||||
saveGeom(mb, "audioRecorder")
|
saveGeom(mb, "audioRecorder")
|
||||||
# ensure at least a second captured
|
# ensure at least a second captured
|
||||||
while time.time() - t < 1:
|
while time.time() - t < 1:
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ QTabWidget { background-color: %s; }
|
||||||
|
|
||||||
app.setPalette(palette)
|
app.setPalette(palette)
|
||||||
|
|
||||||
def _update_stat_colors(self):
|
def _update_stat_colors(self) -> None:
|
||||||
import anki.stats as s
|
import anki.stats as s
|
||||||
|
|
||||||
s.colLearn = self.str_color("new-count")
|
s.colLearn = self.str_color("new-count")
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ from aqt.webview import AnkiWebView
|
||||||
|
|
||||||
# wrapper class for set_bridge_command()
|
# wrapper class for set_bridge_command()
|
||||||
class TopToolbar:
|
class TopToolbar:
|
||||||
def __init__(self, toolbar: Toolbar):
|
def __init__(self, toolbar: Toolbar) -> None:
|
||||||
self.toolbar = toolbar
|
self.toolbar = toolbar
|
||||||
|
|
||||||
|
|
||||||
# wrapper class for set_bridge_command()
|
# wrapper class for set_bridge_command()
|
||||||
class BottomToolbar:
|
class BottomToolbar:
|
||||||
def __init__(self, toolbar: Toolbar):
|
def __init__(self, toolbar: Toolbar) -> None:
|
||||||
self.toolbar = toolbar
|
self.toolbar = toolbar
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ class Toolbar:
|
||||||
buf: str = "",
|
buf: str = "",
|
||||||
web_context: Optional[Any] = None,
|
web_context: Optional[Any] = None,
|
||||||
link_handler: Optional[Callable[[str], Any]] = None,
|
link_handler: Optional[Callable[[str], Any]] = None,
|
||||||
):
|
) -> None:
|
||||||
web_context = web_context or TopToolbar(self)
|
web_context = web_context or TopToolbar(self)
|
||||||
link_handler = link_handler or self._linkHandler
|
link_handler = link_handler or self._linkHandler
|
||||||
self.web.set_bridge_command(link_handler, web_context)
|
self.web.set_bridge_command(link_handler, web_context)
|
||||||
|
|
@ -90,7 +90,7 @@ class Toolbar:
|
||||||
f"""{label}</a>"""
|
f"""{label}</a>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def _centerLinks(self):
|
def _centerLinks(self) -> str:
|
||||||
links = [
|
links = [
|
||||||
self.create_link(
|
self.create_link(
|
||||||
"decks",
|
"decks",
|
||||||
|
|
@ -149,15 +149,15 @@ class Toolbar:
|
||||||
# Link handling
|
# Link handling
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def _linkHandler(self, link):
|
def _linkHandler(self, link: str) -> bool:
|
||||||
if link in self.link_handlers:
|
if link in self.link_handlers:
|
||||||
self.link_handlers[link]()
|
self.link_handlers[link]()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _deckLinkHandler(self):
|
def _deckLinkHandler(self) -> None:
|
||||||
self.mw.moveToState("deckBrowser")
|
self.mw.moveToState("deckBrowser")
|
||||||
|
|
||||||
def _studyLinkHandler(self):
|
def _studyLinkHandler(self) -> None:
|
||||||
# if overview already shown, switch to review
|
# if overview already shown, switch to review
|
||||||
if self.mw.state == "overview":
|
if self.mw.state == "overview":
|
||||||
self.mw.col.startTimebox()
|
self.mw.col.startTimebox()
|
||||||
|
|
@ -165,16 +165,16 @@ class Toolbar:
|
||||||
else:
|
else:
|
||||||
self.mw.onOverview()
|
self.mw.onOverview()
|
||||||
|
|
||||||
def _addLinkHandler(self):
|
def _addLinkHandler(self) -> None:
|
||||||
self.mw.onAddCard()
|
self.mw.onAddCard()
|
||||||
|
|
||||||
def _browseLinkHandler(self):
|
def _browseLinkHandler(self) -> None:
|
||||||
self.mw.onBrowse()
|
self.mw.onBrowse()
|
||||||
|
|
||||||
def _statsLinkHandler(self):
|
def _statsLinkHandler(self) -> None:
|
||||||
self.mw.onStats()
|
self.mw.onStats()
|
||||||
|
|
||||||
def _syncLinkHandler(self):
|
def _syncLinkHandler(self) -> None:
|
||||||
self.mw.onSync()
|
self.mw.onSync()
|
||||||
|
|
||||||
# HTML & CSS
|
# HTML & CSS
|
||||||
|
|
@ -206,7 +206,7 @@ class BottomBar(Toolbar):
|
||||||
buf: str = "",
|
buf: str = "",
|
||||||
web_context: Optional[Any] = None,
|
web_context: Optional[Any] = None,
|
||||||
link_handler: Optional[Callable[[str], Any]] = None,
|
link_handler: Optional[Callable[[str], Any]] = None,
|
||||||
):
|
) -> None:
|
||||||
# note: some screens may override this
|
# note: some screens may override this
|
||||||
web_context = web_context or BottomToolbar(self)
|
web_context = web_context or BottomToolbar(self)
|
||||||
link_handler = link_handler or self._linkHandler
|
link_handler = link_handler or self._linkHandler
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import dataclasses
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any, Callable, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.utils import isLin, isMac, isWin
|
from anki.utils import isLin, isMac, isWin
|
||||||
|
|
@ -176,7 +176,7 @@ class AnkiWebView(QWebEngineView): # type: ignore
|
||||||
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
||||||
|
|
||||||
self._domDone = True
|
self._domDone = True
|
||||||
self._pendingActions: List[Tuple[str, List[Any]]] = []
|
self._pendingActions: List[Tuple[str, Sequence[Any]]] = []
|
||||||
self.requiresCol = True
|
self.requiresCol = True
|
||||||
self.setPage(self._page)
|
self.setPage(self._page)
|
||||||
|
|
||||||
|
|
@ -258,13 +258,13 @@ class AnkiWebView(QWebEngineView): # type: ignore
|
||||||
def dropEvent(self, evt):
|
def dropEvent(self, evt):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setHtml(self, html):
|
def setHtml(self, html: str) -> None:
|
||||||
# discard any previous pending actions
|
# discard any previous pending actions
|
||||||
self._pendingActions = []
|
self._pendingActions = []
|
||||||
self._domDone = True
|
self._domDone = True
|
||||||
self._queueAction("setHtml", html)
|
self._queueAction("setHtml", html)
|
||||||
|
|
||||||
def _setHtml(self, html):
|
def _setHtml(self, html: str) -> None:
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
oldFocus = app.focusWidget()
|
oldFocus = app.focusWidget()
|
||||||
self._domDone = False
|
self._domDone = False
|
||||||
|
|
@ -273,7 +273,7 @@ class AnkiWebView(QWebEngineView): # type: ignore
|
||||||
if oldFocus:
|
if oldFocus:
|
||||||
oldFocus.setFocus()
|
oldFocus.setFocus()
|
||||||
|
|
||||||
def zoomFactor(self):
|
def zoomFactor(self) -> float:
|
||||||
# overridden scale factor?
|
# overridden scale factor?
|
||||||
webscale = os.environ.get("ANKI_WEBSCALE")
|
webscale = os.environ.get("ANKI_WEBSCALE")
|
||||||
if webscale:
|
if webscale:
|
||||||
|
|
@ -295,7 +295,7 @@ class AnkiWebView(QWebEngineView): # type: ignore
|
||||||
newFactor = desiredScale / qtIntScale
|
newFactor = desiredScale / qtIntScale
|
||||||
return max(1, newFactor)
|
return max(1, newFactor)
|
||||||
|
|
||||||
def _getQtIntScale(self, screen):
|
def _getQtIntScale(self, screen) -> int:
|
||||||
# try to detect if Qt has scaled the screen
|
# try to detect if Qt has scaled the screen
|
||||||
# - qt will round the scale factor to a whole number, so a dpi of 125% = 1x,
|
# - qt will round the scale factor to a whole number, so a dpi of 125% = 1x,
|
||||||
# and a dpi of 150% = 2x
|
# and a dpi of 150% = 2x
|
||||||
|
|
@ -430,13 +430,13 @@ body {{ zoom: {}; background: {}; {} }}
|
||||||
fname
|
fname
|
||||||
)
|
)
|
||||||
|
|
||||||
def eval(self, js):
|
def eval(self, js: str) -> None:
|
||||||
self.evalWithCallback(js, None)
|
self.evalWithCallback(js, None)
|
||||||
|
|
||||||
def evalWithCallback(self, js, cb):
|
def evalWithCallback(self, js: str, cb: Callable) -> None:
|
||||||
self._queueAction("eval", js, cb)
|
self._queueAction("eval", js, cb)
|
||||||
|
|
||||||
def _evalWithCallback(self, js, cb):
|
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None:
|
||||||
if cb:
|
if cb:
|
||||||
|
|
||||||
def handler(val):
|
def handler(val):
|
||||||
|
|
@ -449,11 +449,11 @@ body {{ zoom: {}; background: {}; {} }}
|
||||||
else:
|
else:
|
||||||
self.page().runJavaScript(js)
|
self.page().runJavaScript(js)
|
||||||
|
|
||||||
def _queueAction(self, name, *args):
|
def _queueAction(self, name: str, *args: Any) -> None:
|
||||||
self._pendingActions.append((name, args))
|
self._pendingActions.append((name, args))
|
||||||
self._maybeRunActions()
|
self._maybeRunActions()
|
||||||
|
|
||||||
def _maybeRunActions(self):
|
def _maybeRunActions(self) -> None:
|
||||||
while self._pendingActions and self._domDone:
|
while self._pendingActions and self._domDone:
|
||||||
name, args = self._pendingActions.pop(0)
|
name, args = self._pendingActions.pop(0)
|
||||||
|
|
||||||
|
|
@ -464,10 +464,10 @@ body {{ zoom: {}; background: {}; {} }}
|
||||||
else:
|
else:
|
||||||
raise Exception("unknown action: {}".format(name))
|
raise Exception("unknown action: {}".format(name))
|
||||||
|
|
||||||
def _openLinksExternally(self, url):
|
def _openLinksExternally(self, url: str) -> None:
|
||||||
openLink(url)
|
openLink(url)
|
||||||
|
|
||||||
def _shouldIgnoreWebEvent(self):
|
def _shouldIgnoreWebEvent(self) -> bool:
|
||||||
# async web events may be received after the profile has been closed
|
# async web events may be received after the profile has been closed
|
||||||
# or the underlying webview has been deleted
|
# or the underlying webview has been deleted
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
|
@ -499,18 +499,18 @@ body {{ zoom: {}; background: {}; {} }}
|
||||||
else:
|
else:
|
||||||
return self.onBridgeCmd(cmd)
|
return self.onBridgeCmd(cmd)
|
||||||
|
|
||||||
def defaultOnBridgeCmd(self, cmd: str) -> Any:
|
def defaultOnBridgeCmd(self, cmd: str) -> None:
|
||||||
print("unhandled bridge cmd:", cmd)
|
print("unhandled bridge cmd:", cmd)
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
def resetHandlers(self):
|
def resetHandlers(self) -> None:
|
||||||
self.onBridgeCmd = self.defaultOnBridgeCmd
|
self.onBridgeCmd = self.defaultOnBridgeCmd
|
||||||
self._bridge_context = None
|
self._bridge_context = None
|
||||||
|
|
||||||
def adjustHeightToFit(self):
|
def adjustHeightToFit(self) -> None:
|
||||||
self.evalWithCallback("$(document.body).height()", self._onHeight)
|
self.evalWithCallback("$(document.body).height()", self._onHeight)
|
||||||
|
|
||||||
def _onHeight(self, qvar):
|
def _onHeight(self, qvar: Optional[int]) -> None:
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
|
||||||
if qvar is None:
|
if qvar is None:
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,5 @@ addons-failed-to-load =
|
||||||
|
|
||||||
When loading '{$name}':
|
When loading '{$name}':
|
||||||
{$traceback}
|
{$traceback}
|
||||||
|
# Shown in the add-on configuration screen (Tools>Add-ons>Config), in the title bar
|
||||||
|
addons-config-window-title = Configure '{$name}'
|
||||||
|
|
|
||||||
|
|
@ -45,31 +45,6 @@ hooks = [
|
||||||
content.table += "\n<div>my html</div>"
|
content.table += "\n<div>my html</div>"
|
||||||
""",
|
""",
|
||||||
),
|
),
|
||||||
Hook(
|
|
||||||
name="deck_browser_did_render",
|
|
||||||
args=["deck_browser: aqt.deckbrowser.DeckBrowser"],
|
|
||||||
doc="""Allow to update the deck browser window. E.g. change its title.""",
|
|
||||||
),
|
|
||||||
Hook(
|
|
||||||
name="deck_browser_will_render_content",
|
|
||||||
args=[
|
|
||||||
"deck_browser: aqt.deckbrowser.DeckBrowser",
|
|
||||||
"content: aqt.deckbrowser.DeckBrowserContent",
|
|
||||||
],
|
|
||||||
doc="""Used to modify HTML content sections in the deck browser body
|
|
||||||
|
|
||||||
'content' contains the sections of HTML content the deck browser body
|
|
||||||
will be updated with.
|
|
||||||
|
|
||||||
When modifying the content of a particular section, please make sure your
|
|
||||||
changes only perform the minimum required edits to make your add-on work.
|
|
||||||
You should avoid overwriting or interfering with existing data as much
|
|
||||||
as possible, instead opting to append your own changes, e.g.:
|
|
||||||
|
|
||||||
def on_deck_browser_will_render_content(deck_browser, content):
|
|
||||||
content.stats += "\n<div>my html</div>"
|
|
||||||
""",
|
|
||||||
),
|
|
||||||
Hook(
|
Hook(
|
||||||
name="reviewer_did_show_question",
|
name="reviewer_did_show_question",
|
||||||
args=["card: Card"],
|
args=["card: Card"],
|
||||||
|
|
@ -114,6 +89,17 @@ hooks = [
|
||||||
legacy_hook="reviewCleanup",
|
legacy_hook="reviewCleanup",
|
||||||
doc="Called before Anki transitions from the review screen to another screen.",
|
doc="Called before Anki transitions from the review screen to another screen.",
|
||||||
),
|
),
|
||||||
|
# Card layout
|
||||||
|
###################
|
||||||
|
Hook(
|
||||||
|
name="card_layout_will_show",
|
||||||
|
args=["clayout: aqt.clayout.CardLayout"],
|
||||||
|
doc="""Allow to change the display of the card layout. After most values are
|
||||||
|
set and before the window is actually shown.""",
|
||||||
|
),
|
||||||
|
# Multiple windows
|
||||||
|
###################
|
||||||
|
# reviewer, clayout and browser
|
||||||
Hook(
|
Hook(
|
||||||
name="card_will_show",
|
name="card_will_show",
|
||||||
args=["text: str", "card: Card", "kind: str"],
|
args=["text: str", "card: Card", "kind: str"],
|
||||||
|
|
@ -121,6 +107,33 @@ hooks = [
|
||||||
legacy_hook="prepareQA",
|
legacy_hook="prepareQA",
|
||||||
doc="Can modify card text before review/preview.",
|
doc="Can modify card text before review/preview.",
|
||||||
),
|
),
|
||||||
|
# Deck browser
|
||||||
|
###################
|
||||||
|
Hook(
|
||||||
|
name="deck_browser_did_render",
|
||||||
|
args=["deck_browser: aqt.deckbrowser.DeckBrowser"],
|
||||||
|
doc="""Allow to update the deck browser window. E.g. change its title.""",
|
||||||
|
),
|
||||||
|
Hook(
|
||||||
|
name="deck_browser_will_render_content",
|
||||||
|
args=[
|
||||||
|
"deck_browser: aqt.deckbrowser.DeckBrowser",
|
||||||
|
"content: aqt.deckbrowser.DeckBrowserContent",
|
||||||
|
],
|
||||||
|
doc="""Used to modify HTML content sections in the deck browser body
|
||||||
|
|
||||||
|
'content' contains the sections of HTML content the deck browser body
|
||||||
|
will be updated with.
|
||||||
|
|
||||||
|
When modifying the content of a particular section, please make sure your
|
||||||
|
changes only perform the minimum required edits to make your add-on work.
|
||||||
|
You should avoid overwriting or interfering with existing data as much
|
||||||
|
as possible, instead opting to append your own changes, e.g.:
|
||||||
|
|
||||||
|
def on_deck_browser_will_render_content(deck_browser, content):
|
||||||
|
content.stats += "\n<div>my html</div>"
|
||||||
|
""",
|
||||||
|
),
|
||||||
# Deck options
|
# Deck options
|
||||||
###################
|
###################
|
||||||
Hook(
|
Hook(
|
||||||
|
|
@ -145,6 +158,7 @@ hooks = [
|
||||||
),
|
),
|
||||||
# Browser
|
# Browser
|
||||||
###################
|
###################
|
||||||
|
Hook(name="browser_will_show", args=["browser: aqt.browser.Browser"]),
|
||||||
Hook(
|
Hook(
|
||||||
name="browser_menus_did_init",
|
name="browser_menus_did_init",
|
||||||
args=["browser: aqt.browser.Browser"],
|
args=["browser: aqt.browser.Browser"],
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ DEPS := .build/tools .build/vernum ../meta/buildhash \
|
||||||
$(wildcard $(QT_FTL_TEMPLATES)/*.ftl) \
|
$(wildcard $(QT_FTL_TEMPLATES)/*.ftl) \
|
||||||
$(wildcard $(QT_FTL_LOCALES)/*/*.ftl) \
|
$(wildcard $(QT_FTL_LOCALES)/*/*.ftl) \
|
||||||
$(shell ${FIND} ../rslib/src -name '*.rs') $(wildcard ../proto/*) \
|
$(shell ${FIND} ../rslib/src -name '*.rs') $(wildcard ../proto/*) \
|
||||||
$(shell ${FIND} ../rslib/ftl -type f)
|
$(shell ${FIND} ../rslib/ftl -type f) \
|
||||||
|
$(shell ${FIND} ./src -type f)
|
||||||
|
|
||||||
.build/develop: $(DEPS)
|
.build/develop: $(DEPS)
|
||||||
touch ../proto/backend.proto
|
touch ../proto/backend.proto
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue