2012-12-21 08:51:59 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
2020-02-08 23:59:29 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2013-10-22 07:28:45 +02:00
|
|
|
import difflib
|
2017-12-11 07:20:00 +01:00
|
|
|
import html
|
2019-03-04 08:25:19 +01:00
|
|
|
import json
|
2019-12-20 10:19:03 +01:00
|
|
|
import re
|
|
|
|
import unicodedata as ucd
|
2020-02-27 04:11:21 +01:00
|
|
|
from typing import Callable, List, Optional, Sequence, Tuple, Union
|
|
|
|
|
|
|
|
from PyQt5.QtCore import Qt
|
2019-12-16 10:46:40 +01:00
|
|
|
|
2020-01-13 04:57:51 +01:00
|
|
|
from anki import hooks
|
2019-12-16 10:46:40 +01:00
|
|
|
from anki.cards import Card
|
2013-05-24 05:53:41 +02:00
|
|
|
from anki.lang import _, ngettext
|
2020-01-23 06:08:10 +01:00
|
|
|
from anki.utils import stripHTML
|
2020-01-13 09:37:08 +01:00
|
|
|
from aqt import AnkiQt, gui_hooks
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.qt import *
|
2020-01-24 02:06:11 +01:00
|
|
|
from aqt.sound import av_player, getAudio, play_clicked_audio
|
2020-01-23 06:08:10 +01:00
|
|
|
from aqt.theme import theme_manager
|
2020-01-22 01:46:35 +01:00
|
|
|
from aqt.toolbar import BottomBar
|
2020-01-24 02:06:11 +01:00
|
|
|
from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, tooltip
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2013-10-22 07:28:45 +02:00
|
|
|
|
2020-02-08 23:59:29 +01:00
|
|
|
class ReviewerBottomBar:
|
2020-02-27 04:11:21 +01:00
|
|
|
def __init__(self, reviewer: Reviewer) -> None:
|
2020-02-08 23:59:29 +01:00
|
|
|
self.reviewer = reviewer
|
|
|
|
|
2020-04-13 01:04:30 +02:00
|
|
|
|
2020-04-13 00:59:36 +02:00
|
|
|
def replay_audio(card: Card, question_side: bool) -> None:
|
|
|
|
if question_side:
|
|
|
|
av_player.play_tags(card.question_av_tags())
|
|
|
|
else:
|
|
|
|
tags = card.answer_av_tags()
|
|
|
|
if card.replay_question_audio_on_answer_side():
|
|
|
|
tags = card.question_av_tags() + tags
|
|
|
|
av_player.play_tags(tags)
|
2020-02-08 23:59:29 +01:00
|
|
|
|
2020-04-13 01:04:30 +02:00
|
|
|
|
2017-02-06 23:21:33 +01:00
|
|
|
class Reviewer:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Manage reviews. Maintains a separate state."
|
|
|
|
|
2020-01-13 04:57:51 +01:00
|
|
|
def __init__(self, mw: AnkiQt) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw = mw
|
|
|
|
self.web = mw.web
|
2020-01-15 22:41:23 +01:00
|
|
|
self.card: Optional[Card] = None
|
2019-12-16 10:46:40 +01:00
|
|
|
self.cardQueue: List[Card] = []
|
2012-12-21 08:51:59 +01:00
|
|
|
self.hadCardQueue = False
|
2019-12-16 10:46:40 +01:00
|
|
|
self._answeredIds: List[int] = []
|
2020-02-27 04:11:21 +01:00
|
|
|
self._recordedAudio: Optional[str] = None
|
|
|
|
self.typeCorrect: str = None # web init happens before this is set
|
2020-01-15 22:41:23 +01:00
|
|
|
self.state: Optional[str] = None
|
2020-01-22 01:46:35 +01:00
|
|
|
self.bottom = BottomBar(mw, mw.bottomWeb)
|
2020-01-15 07:53:24 +01:00
|
|
|
hooks.card_did_leech.append(self.onLeech)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def show(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.col.reset()
|
2020-02-27 04:11:21 +01:00
|
|
|
self.mw.setStateShortcuts(self._shortcutKeys()) # type: ignore
|
2020-02-08 23:59:29 +01:00
|
|
|
self.web.set_bridge_command(self._linkHandler, self)
|
|
|
|
self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
|
2020-02-27 04:11:21 +01:00
|
|
|
self._reps: int = None
|
2012-12-21 08:51:59 +01:00
|
|
|
self.nextCard()
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def lastCard(self) -> Optional[Card]:
|
2012-12-21 08:51:59 +01:00
|
|
|
if self._answeredIds:
|
|
|
|
if not self.card or self._answeredIds[-1] != self.card.id:
|
|
|
|
try:
|
|
|
|
return self.mw.col.getCard(self._answeredIds[-1])
|
|
|
|
except TypeError:
|
|
|
|
# id was deleted
|
2020-02-27 04:11:21 +01:00
|
|
|
return None
|
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def cleanup(self) -> None:
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.reviewer_will_end()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Fetching a card
|
|
|
|
##########################################################################
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def nextCard(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
elapsed = self.mw.col.timeboxReached()
|
|
|
|
if elapsed:
|
2020-02-27 04:11:21 +01:00
|
|
|
assert not isinstance(elapsed, bool)
|
2019-12-23 01:34:10 +01:00
|
|
|
part1 = (
|
|
|
|
ngettext("%d card studied in", "%d cards studied in", elapsed[1])
|
|
|
|
% elapsed[1]
|
|
|
|
)
|
|
|
|
mins = int(round(elapsed[0] / 60))
|
2013-05-24 05:53:41 +02:00
|
|
|
part2 = ngettext("%s minute.", "%s minutes.", mins) % mins
|
|
|
|
fin = _("Finish")
|
2019-12-23 01:34:10 +01:00
|
|
|
diag = askUserDialog("%s %s" % (part1, part2), [_("Continue"), fin])
|
2013-05-24 05:53:41 +02:00
|
|
|
diag.setIcon(QMessageBox.Information)
|
|
|
|
if diag.run() == fin:
|
|
|
|
return self.mw.moveToState("deckBrowser")
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.col.startTimebox()
|
|
|
|
if self.cardQueue:
|
|
|
|
# undone/edited cards to show
|
|
|
|
c = self.cardQueue.pop()
|
|
|
|
c.startTimer()
|
|
|
|
self.hadCardQueue = True
|
|
|
|
else:
|
|
|
|
if self.hadCardQueue:
|
|
|
|
# the undone/edited cards may be sitting in the regular queue;
|
|
|
|
# need to reset
|
|
|
|
self.mw.col.reset()
|
|
|
|
self.hadCardQueue = False
|
|
|
|
c = self.mw.col.sched.getCard()
|
|
|
|
self.card = c
|
|
|
|
if not c:
|
|
|
|
self.mw.moveToState("overview")
|
|
|
|
return
|
|
|
|
if self._reps is None or self._reps % 100 == 0:
|
|
|
|
# we recycle the webview periodically so webkit can free memory
|
|
|
|
self._initWeb()
|
2017-08-02 08:22:54 +02:00
|
|
|
self._showQuestion()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Audio
|
|
|
|
##########################################################################
|
|
|
|
|
2020-04-13 00:59:36 +02:00
|
|
|
def replayAudio(self) -> None:
|
|
|
|
if self.state == "question":
|
|
|
|
replay_audio(self.card, True)
|
|
|
|
elif self.state == "answer":
|
|
|
|
replay_audio(self.card, False)
|
2020-01-21 05:47:03 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Initializing the webview
|
|
|
|
##########################################################################
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def revHtml(self) -> str:
|
2017-08-10 13:39:04 +02:00
|
|
|
extra = self.mw.col.conf.get("reviewExtra", "")
|
2019-12-23 01:34:10 +01:00
|
|
|
fade = ""
|
2018-08-30 05:26:08 +02:00
|
|
|
if self.mw.pm.glMode() == "software":
|
2019-12-23 01:34:10 +01:00
|
|
|
fade = "<script>qFade=0;</script>"
|
2018-01-16 07:07:30 +01:00
|
|
|
return """
|
2017-08-16 12:30:29 +02:00
|
|
|
<div id=_mark>★</div>
|
2017-08-12 08:08:10 +02:00
|
|
|
<div id=_flag>⚑</div>
|
2018-09-10 05:32:42 +02:00
|
|
|
{}
|
2017-08-06 04:01:30 +02:00
|
|
|
<div id=qa></div>
|
2018-01-16 07:07:30 +01:00
|
|
|
{}
|
2019-12-23 01:34:10 +01:00
|
|
|
""".format(
|
|
|
|
fade, extra
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _initWeb(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self._reps = 0
|
|
|
|
# main window
|
2019-12-23 01:34:10 +01:00
|
|
|
self.web.stdHtml(
|
|
|
|
self.revHtml(),
|
2020-02-15 15:03:43 +01:00
|
|
|
css=["reviewer.css"],
|
2019-12-23 01:34:10 +01:00
|
|
|
js=[
|
2020-02-15 15:03:43 +01:00
|
|
|
"jquery.js",
|
|
|
|
"browsersel.js",
|
|
|
|
"mathjax/conf.js",
|
|
|
|
"mathjax/MathJax.js",
|
|
|
|
"reviewer.js",
|
2019-12-23 01:34:10 +01:00
|
|
|
],
|
2020-02-12 22:00:13 +01:00
|
|
|
context=self,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
# show answer / ease buttons
|
|
|
|
self.bottom.web.show()
|
|
|
|
self.bottom.web.stdHtml(
|
|
|
|
self._bottomHTML(),
|
2020-02-15 15:03:43 +01:00
|
|
|
css=["toolbar-bottom.css", "reviewer-bottom.css"],
|
|
|
|
js=["jquery.js", "reviewer-bottom.js"],
|
2020-02-12 22:00:13 +01:00
|
|
|
context=ReviewerBottomBar(self),
|
2017-07-28 08:19:06 +02:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Showing the question
|
|
|
|
##########################################################################
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _mungeQA(self, buf: str) -> str:
|
2020-01-24 02:06:11 +01:00
|
|
|
return self.typeAnsFilter(self.mw.prepare_card_text_for_display(buf))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def _showQuestion(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self._reps += 1
|
|
|
|
self.state = "question"
|
2020-02-27 04:11:21 +01:00
|
|
|
self.typedAnswer: str = None
|
2012-12-21 08:51:59 +01:00
|
|
|
c = self.card
|
|
|
|
# grab the question and play audio
|
2020-05-13 03:17:44 +02:00
|
|
|
q = c.q()
|
2020-01-24 02:06:11 +01:00
|
|
|
# play audio?
|
2020-04-13 01:04:30 +02:00
|
|
|
if c.autoplay():
|
2020-01-24 02:06:11 +01:00
|
|
|
av_player.play_tags(c.question_av_tags())
|
2020-02-25 08:49:06 +01:00
|
|
|
else:
|
2020-03-14 11:04:19 +01:00
|
|
|
av_player.clear_queue_and_maybe_interrupt()
|
2020-01-21 05:47:03 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# render & update bottom
|
|
|
|
q = self._mungeQA(q)
|
2020-01-15 08:45:35 +01:00
|
|
|
q = gui_hooks.card_will_show(q, c, "reviewQuestion")
|
2017-07-29 06:24:45 +02:00
|
|
|
|
2020-01-23 06:08:10 +01:00
|
|
|
bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
|
2017-07-29 06:24:45 +02:00
|
|
|
|
2017-08-06 04:01:30 +02:00
|
|
|
self.web.eval("_showQuestion(%s,'%s');" % (json.dumps(q), bodyclass))
|
2017-08-12 08:08:10 +02:00
|
|
|
self._drawFlag()
|
2017-08-16 12:30:29 +02:00
|
|
|
self._drawMark()
|
2017-08-02 08:22:54 +02:00
|
|
|
self._showAnswerButton()
|
2012-12-21 08:51:59 +01:00
|
|
|
# if we have a type answer field, focus main web
|
|
|
|
if self.typeCorrect:
|
|
|
|
self.mw.web.setFocus()
|
|
|
|
# user hook
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.reviewer_did_show_question(c)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def autoplay(self, card: Card) -> bool:
|
2020-04-13 01:04:30 +02:00
|
|
|
print("use card.autoplay() instead of reviewer.autoplay(card)")
|
|
|
|
return card.autoplay()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _drawFlag(self) -> None:
|
2017-08-12 08:08:10 +02:00
|
|
|
self.web.eval("_drawFlag(%s);" % self.card.userFlag())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _drawMark(self) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
self.web.eval("_drawMark(%s);" % json.dumps(self.card.note().hasTag("marked")))
|
2017-08-16 12:30:29 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Showing the answer
|
|
|
|
##########################################################################
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def _showAnswer(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.mw.state != "review":
|
|
|
|
# showing resetRequired screen; ignore space
|
|
|
|
return
|
|
|
|
self.state = "answer"
|
|
|
|
c = self.card
|
|
|
|
a = c.a()
|
|
|
|
# play audio?
|
2020-04-13 01:04:30 +02:00
|
|
|
if c.autoplay():
|
2020-01-24 02:06:11 +01:00
|
|
|
av_player.play_tags(c.answer_av_tags())
|
2020-02-25 08:49:06 +01:00
|
|
|
else:
|
2020-03-14 11:04:19 +01:00
|
|
|
av_player.clear_queue_and_maybe_interrupt()
|
2020-01-24 02:06:11 +01:00
|
|
|
|
2017-08-07 08:15:15 +02:00
|
|
|
a = self._mungeQA(a)
|
2020-01-15 08:45:35 +01:00
|
|
|
a = gui_hooks.card_will_show(a, c, "reviewAnswer")
|
2012-12-21 08:51:59 +01:00
|
|
|
# render and update bottom
|
2017-08-06 04:01:30 +02:00
|
|
|
self.web.eval("_showAnswer(%s);" % json.dumps(a))
|
2012-12-21 08:51:59 +01:00
|
|
|
self._showEaseButtons()
|
|
|
|
# user hook
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.reviewer_did_show_answer(c)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Answering a card
|
|
|
|
############################################################
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _answerCard(self, ease: int) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Reschedule card and show next."
|
|
|
|
if self.mw.state != "review":
|
|
|
|
# showing resetRequired screen; ignore key
|
|
|
|
return
|
|
|
|
if self.state != "answer":
|
|
|
|
return
|
|
|
|
if self.mw.col.sched.answerButtons(self.card) < ease:
|
|
|
|
return
|
2020-01-24 15:36:05 +01:00
|
|
|
proceed, ease = gui_hooks.reviewer_will_answer_card(
|
|
|
|
(True, ease), self, self.card
|
|
|
|
)
|
|
|
|
if not proceed:
|
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.col.sched.answerCard(self.card, ease)
|
2020-01-24 15:36:05 +01:00
|
|
|
gui_hooks.reviewer_did_answer_card(self, self.card, ease)
|
2012-12-21 08:51:59 +01:00
|
|
|
self._answeredIds.append(self.card.id)
|
|
|
|
self.mw.autosave()
|
|
|
|
self.nextCard()
|
|
|
|
|
|
|
|
# Handlers
|
|
|
|
############################################################
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _shortcutKeys(
|
|
|
|
self,
|
|
|
|
) -> List[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]:
|
2017-06-22 08:36:54 +02:00
|
|
|
return [
|
|
|
|
("e", self.mw.onEditCurrent),
|
|
|
|
(" ", self.onEnterKey),
|
|
|
|
(Qt.Key_Return, self.onEnterKey),
|
|
|
|
(Qt.Key_Enter, self.onEnterKey),
|
|
|
|
("r", self.replayAudio),
|
|
|
|
(Qt.Key_F5, self.replayAudio),
|
2017-08-12 08:08:10 +02:00
|
|
|
("Ctrl+1", lambda: self.setFlag(1)),
|
|
|
|
("Ctrl+2", lambda: self.setFlag(2)),
|
|
|
|
("Ctrl+3", lambda: self.setFlag(3)),
|
|
|
|
("Ctrl+4", lambda: self.setFlag(4)),
|
2017-08-16 12:30:29 +02:00
|
|
|
("*", self.onMark),
|
2017-06-22 08:36:54 +02:00
|
|
|
("=", self.onBuryNote),
|
|
|
|
("-", self.onBuryCard),
|
|
|
|
("!", self.onSuspend),
|
|
|
|
("@", self.onSuspendCard),
|
2017-07-26 02:52:04 +02:00
|
|
|
("Ctrl+Delete", self.onDelete),
|
2017-06-22 08:36:54 +02:00
|
|
|
("v", self.onReplayRecorded),
|
|
|
|
("Shift+v", self.onRecordVoice),
|
|
|
|
("o", self.onOptions),
|
|
|
|
("1", lambda: self._answerCard(1)),
|
|
|
|
("2", lambda: self._answerCard(2)),
|
|
|
|
("3", lambda: self._answerCard(3)),
|
|
|
|
("4", lambda: self._answerCard(4)),
|
2020-01-22 03:50:33 +01:00
|
|
|
("5", self.on_pause_audio),
|
|
|
|
("6", self.on_seek_backward),
|
|
|
|
("7", self.on_seek_forward),
|
2017-06-22 08:36:54 +02:00
|
|
|
]
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def on_pause_audio(self) -> None:
|
2020-01-22 03:50:33 +01:00
|
|
|
av_player.toggle_pause()
|
|
|
|
|
|
|
|
seek_secs = 5
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def on_seek_backward(self) -> None:
|
2020-01-22 03:50:33 +01:00
|
|
|
av_player.seek_relative(-self.seek_secs)
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def on_seek_forward(self) -> None:
|
2020-01-22 03:50:33 +01:00
|
|
|
av_player.seek_relative(self.seek_secs)
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onEnterKey(self) -> None:
|
2017-06-22 08:36:54 +02:00
|
|
|
if self.state == "question":
|
|
|
|
self._getTypedAnswer()
|
|
|
|
elif self.state == "answer":
|
2019-12-23 01:34:10 +01:00
|
|
|
self.bottom.web.evalWithCallback(
|
|
|
|
"selectedAnswerButton()", self._onAnswerButton
|
|
|
|
)
|
2018-09-24 06:16:08 +02:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _onAnswerButton(self, val: str) -> None:
|
2018-09-24 06:16:08 +02:00
|
|
|
# button selected?
|
2018-09-26 08:31:31 +02:00
|
|
|
if val and val in "1234":
|
2018-09-24 06:16:08 +02:00
|
|
|
self._answerCard(int(val))
|
|
|
|
else:
|
2017-06-22 08:36:54 +02:00
|
|
|
self._answerCard(self._defaultEase())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _linkHandler(self, url: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if url == "ans":
|
2016-07-05 05:14:45 +02:00
|
|
|
self._getTypedAnswer()
|
2012-12-21 08:51:59 +01:00
|
|
|
elif url.startswith("ease"):
|
|
|
|
self._answerCard(int(url[4:]))
|
|
|
|
elif url == "edit":
|
|
|
|
self.mw.onEditCurrent()
|
|
|
|
elif url == "more":
|
|
|
|
self.showContextMenu()
|
2020-01-21 05:47:03 +01:00
|
|
|
elif url.startswith("play:"):
|
2020-01-24 02:06:11 +01:00
|
|
|
play_clicked_audio(url, self.card)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2016-07-07 03:08:32 +02:00
|
|
|
print("unrecognized anki link:", url)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Type in the answer
|
|
|
|
##########################################################################
|
|
|
|
|
2019-03-04 08:03:43 +01:00
|
|
|
typeAnsPat = r"\[\[type:(.+?)\]\]"
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def typeAnsFilter(self, buf: str) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.state == "question":
|
|
|
|
return self.typeAnsQuestionFilter(buf)
|
|
|
|
else:
|
|
|
|
return self.typeAnsAnswerFilter(buf)
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def typeAnsQuestionFilter(self, buf: str) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.typeCorrect = None
|
|
|
|
clozeIdx = None
|
|
|
|
m = re.search(self.typeAnsPat, buf)
|
|
|
|
if not m:
|
|
|
|
return buf
|
|
|
|
fld = m.group(1)
|
|
|
|
# if it's a cloze, extract data
|
|
|
|
if fld.startswith("cloze:"):
|
|
|
|
# get field and cloze position
|
|
|
|
clozeIdx = self.card.ord + 1
|
|
|
|
fld = fld.split(":")[1]
|
|
|
|
# loop through fields for a match
|
2019-12-23 01:34:10 +01:00
|
|
|
for f in self.card.model()["flds"]:
|
|
|
|
if f["name"] == fld:
|
|
|
|
self.typeCorrect = self.card.note()[f["name"]]
|
2012-12-21 08:51:59 +01:00
|
|
|
if clozeIdx:
|
|
|
|
# narrow to cloze
|
2019-12-23 01:34:10 +01:00
|
|
|
self.typeCorrect = self._contentForCloze(self.typeCorrect, clozeIdx)
|
|
|
|
self.typeFont = f["font"]
|
|
|
|
self.typeSize = f["size"]
|
2012-12-21 08:51:59 +01:00
|
|
|
break
|
|
|
|
if not self.typeCorrect:
|
|
|
|
if self.typeCorrect is None:
|
|
|
|
if clozeIdx:
|
2019-12-23 01:34:10 +01:00
|
|
|
warn = _(
|
|
|
|
"""\
|
|
|
|
Please run Tools>Empty Cards"""
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
warn = _("Type answer: unknown field %s") % fld
|
|
|
|
return re.sub(self.typeAnsPat, warn, buf)
|
|
|
|
else:
|
|
|
|
# empty field, remove type answer pattern
|
|
|
|
return re.sub(self.typeAnsPat, "", buf)
|
2019-12-23 01:34:10 +01:00
|
|
|
return re.sub(
|
|
|
|
self.typeAnsPat,
|
|
|
|
"""
|
2012-12-21 08:51:59 +01:00
|
|
|
<center>
|
|
|
|
<input type=text id=typeans onkeypress="_typeAnsPress();"
|
|
|
|
style="font-family: '%s'; font-size: %spx;">
|
|
|
|
</center>
|
2019-12-23 01:34:10 +01:00
|
|
|
"""
|
|
|
|
% (self.typeFont, self.typeSize),
|
|
|
|
buf,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def typeAnsAnswerFilter(self, buf: str) -> str:
|
2013-06-12 04:22:13 +02:00
|
|
|
if not self.typeCorrect:
|
2013-04-27 16:12:03 +02:00
|
|
|
return re.sub(self.typeAnsPat, "", buf)
|
2013-05-20 10:56:01 +02:00
|
|
|
origSize = len(buf)
|
|
|
|
buf = buf.replace("<hr id=answer>", "")
|
|
|
|
hadHR = len(buf) != origSize
|
2012-12-21 08:51:59 +01:00
|
|
|
# munge correct value
|
2018-09-24 09:44:34 +02:00
|
|
|
cor = self.mw.col.media.strip(self.typeCorrect)
|
|
|
|
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
|
|
|
|
cor = stripHTML(cor)
|
2013-05-24 04:01:24 +02:00
|
|
|
# ensure we don't chomp multiple whitespace
|
|
|
|
cor = cor.replace(" ", " ")
|
2020-02-27 04:11:21 +01:00
|
|
|
cor = html.unescape(cor)
|
2016-05-12 06:45:35 +02:00
|
|
|
cor = cor.replace("\xa0", " ")
|
2018-09-24 09:44:34 +02:00
|
|
|
cor = cor.strip()
|
2012-12-21 08:51:59 +01:00
|
|
|
given = self.typedAnswer
|
|
|
|
# compare with typed answer
|
2013-05-20 10:32:13 +02:00
|
|
|
res = self.correct(given, cor, showBad=False)
|
2012-12-21 08:51:59 +01:00
|
|
|
# and update the type answer area
|
|
|
|
def repl(match):
|
|
|
|
# can't pass a string in directly, and can't use re.escape as it
|
|
|
|
# escapes too much
|
2013-05-20 10:56:01 +02:00
|
|
|
s = """
|
2012-12-21 08:51:59 +01:00
|
|
|
<span style="font-family: '%s'; font-size: %spx">%s</span>""" % (
|
2019-12-23 01:34:10 +01:00
|
|
|
self.typeFont,
|
|
|
|
self.typeSize,
|
|
|
|
res,
|
|
|
|
)
|
2013-05-20 10:56:01 +02:00
|
|
|
if hadHR:
|
|
|
|
# a hack to ensure the q/a separator falls before the answer
|
|
|
|
# comparison when user is using {{FrontSide}}
|
|
|
|
s = "<hr id=answer>" + s
|
|
|
|
return s
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
return re.sub(self.typeAnsPat, repl, buf)
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _contentForCloze(self, txt: str, idx) -> str:
|
2019-12-23 01:34:10 +01:00
|
|
|
matches = re.findall(r"\{\{c%s::(.+?)\}\}" % idx, txt, re.DOTALL)
|
2012-12-21 08:51:59 +01:00
|
|
|
if not matches:
|
|
|
|
return None
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def noHint(txt):
|
|
|
|
if "::" in txt:
|
|
|
|
return txt.split("::")[0]
|
|
|
|
return txt
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
matches = [noHint(txt) for txt in matches]
|
2014-03-27 23:25:27 +01:00
|
|
|
uniqMatches = set(matches)
|
|
|
|
if len(uniqMatches) == 1:
|
2012-12-21 08:51:59 +01:00
|
|
|
txt = matches[0]
|
2014-03-27 23:25:27 +01:00
|
|
|
else:
|
|
|
|
txt = ", ".join(matches)
|
2012-12-21 08:51:59 +01:00
|
|
|
return txt
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def tokenizeComparison(
|
|
|
|
self, given: str, correct: str
|
|
|
|
) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]:
|
2013-06-05 11:39:42 +02:00
|
|
|
# compare in NFC form so accents appear correct
|
|
|
|
given = ucd.normalize("NFC", given)
|
|
|
|
correct = ucd.normalize("NFC", correct)
|
2017-10-18 18:12:04 +02:00
|
|
|
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
|
2020-02-27 04:11:21 +01:00
|
|
|
givenElems: List[Tuple[bool, str]] = []
|
|
|
|
correctElems: List[Tuple[bool, str]] = []
|
2013-05-20 10:32:13 +02:00
|
|
|
givenPoint = 0
|
|
|
|
correctPoint = 0
|
|
|
|
offby = 0
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None:
|
2013-05-20 10:32:13 +02:00
|
|
|
if old != new:
|
2020-02-27 04:11:21 +01:00
|
|
|
array.append((False, s[old:new]))
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def logGood(
|
|
|
|
start: int, cnt: int, s: str, array: List[Tuple[bool, str]]
|
|
|
|
) -> None:
|
2013-05-20 10:32:13 +02:00
|
|
|
if cnt:
|
2020-02-27 04:11:21 +01:00
|
|
|
array.append((True, s[start : start + cnt]))
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2013-05-20 10:32:13 +02:00
|
|
|
for x, y, cnt in s.get_matching_blocks():
|
|
|
|
# if anything was missed in correct, pad given
|
2019-12-23 01:34:10 +01:00
|
|
|
if cnt and y - offby > x:
|
|
|
|
givenElems.append((False, "-" * (y - x - offby)))
|
|
|
|
offby = y - x
|
2013-05-20 10:32:13 +02:00
|
|
|
# log any proceeding bad elems
|
|
|
|
logBad(givenPoint, x, given, givenElems)
|
|
|
|
logBad(correctPoint, y, correct, correctElems)
|
2019-12-23 01:34:10 +01:00
|
|
|
givenPoint = x + cnt
|
|
|
|
correctPoint = y + cnt
|
2013-05-20 10:32:13 +02:00
|
|
|
# log the match
|
|
|
|
logGood(x, cnt, given, givenElems)
|
|
|
|
logGood(y, cnt, correct, correctElems)
|
|
|
|
return givenElems, correctElems
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def correct(self, given: str, correct: str, showBad: bool = True) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Diff-corrects the typed-in answer."
|
2013-05-20 10:32:13 +02:00
|
|
|
givenElems, correctElems = self.tokenizeComparison(given, correct)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def good(s: str) -> str:
|
2019-12-23 01:34:10 +01:00
|
|
|
return "<span class=typeGood>" + html.escape(s) + "</span>"
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def bad(s: str) -> str:
|
2019-12-23 01:34:10 +01:00
|
|
|
return "<span class=typeBad>" + html.escape(s) + "</span>"
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def missed(s: str) -> str:
|
2019-12-23 01:34:10 +01:00
|
|
|
return "<span class=typeMissed>" + html.escape(s) + "</span>"
|
|
|
|
|
2013-05-20 10:32:13 +02:00
|
|
|
if given == correct:
|
|
|
|
res = good(given)
|
|
|
|
else:
|
|
|
|
res = ""
|
|
|
|
for ok, txt in givenElems:
|
2019-12-16 05:39:54 +01:00
|
|
|
txt = self._noLoneMarks(txt)
|
2013-05-20 10:32:13 +02:00
|
|
|
if ok:
|
|
|
|
res += good(txt)
|
|
|
|
else:
|
|
|
|
res += bad(txt)
|
2020-03-16 00:39:26 +01:00
|
|
|
res += "<br><span id=typearrow>↓</span><br>"
|
2013-05-20 10:32:13 +02:00
|
|
|
for ok, txt in correctElems:
|
2019-12-16 05:39:54 +01:00
|
|
|
txt = self._noLoneMarks(txt)
|
2013-05-20 10:32:13 +02:00
|
|
|
if ok:
|
|
|
|
res += good(txt)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2013-05-20 10:32:13 +02:00
|
|
|
res += missed(txt)
|
|
|
|
res = "<div><code id=typeans>" + res + "</code></div>"
|
|
|
|
return res
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _noLoneMarks(self, s: str) -> str:
|
2019-12-16 05:39:54 +01:00
|
|
|
# ensure a combining character at the start does not join to
|
|
|
|
# previous text
|
|
|
|
if s and ucd.category(s[0]).startswith("M"):
|
|
|
|
return "\xa0" + s
|
|
|
|
return s
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _getTypedAnswer(self) -> None:
|
2016-07-05 05:14:45 +02:00
|
|
|
self.web.evalWithCallback("typeans ? typeans.value : null", self._onTypedAnswer)
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _onTypedAnswer(self, val: None) -> None:
|
2017-10-24 09:08:36 +02:00
|
|
|
self.typedAnswer = val or ""
|
2016-07-05 05:14:45 +02:00
|
|
|
self._showAnswer()
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Bottom bar
|
|
|
|
##########################################################################
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _bottomHTML(self) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
return """
|
2016-06-07 06:27:33 +02:00
|
|
|
<center id=outer>
|
|
|
|
<table id=innertable width=100%% cellspacing=0 cellpadding=0>
|
2012-12-21 08:51:59 +01:00
|
|
|
<tr>
|
|
|
|
<td align=left width=50 valign=top class=stat>
|
|
|
|
<br>
|
2016-06-06 07:50:03 +02:00
|
|
|
<button title="%(editkey)s" onclick="pycmd('edit');">%(edit)s</button></td>
|
2012-12-21 08:51:59 +01:00
|
|
|
<td align=center valign=top id=middle>
|
|
|
|
</td>
|
|
|
|
<td width=50 align=right valign=top class=stat><span id=time class=stattxt>
|
|
|
|
</span><br>
|
2016-06-06 07:50:03 +02:00
|
|
|
<button onclick="pycmd('more');">%(more)s %(downArrow)s</button>
|
2012-12-21 08:51:59 +01:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
2016-06-07 06:27:33 +02:00
|
|
|
</center>
|
2012-12-21 08:51:59 +01:00
|
|
|
<script>
|
2017-07-28 08:19:06 +02:00
|
|
|
time = %(time)d;
|
2012-12-21 08:51:59 +01:00
|
|
|
</script>
|
2019-12-23 01:34:10 +01:00
|
|
|
""" % dict(
|
|
|
|
rem=self._remaining(),
|
|
|
|
edit=_("Edit"),
|
|
|
|
editkey=_("Shortcut key: %s") % "E",
|
|
|
|
more=_("More"),
|
|
|
|
downArrow=downArrow(),
|
|
|
|
time=self.card.timeTaken() // 1000,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _showAnswerButton(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.typeCorrect:
|
|
|
|
self.bottom.web.setFocus()
|
2019-12-23 01:34:10 +01:00
|
|
|
middle = """
|
2012-12-21 08:51:59 +01:00
|
|
|
<span class=stattxt>%s</span><br>
|
2019-12-23 01:34:10 +01:00
|
|
|
<button title="%s" id=ansbut onclick='pycmd("ans");'>%s</button>""" % (
|
|
|
|
self._remaining(),
|
|
|
|
_("Shortcut key: %s") % _("Space"),
|
|
|
|
_("Show Answer"),
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
# wrap it in a table so it has the same top margin as the ease buttons
|
2019-12-23 01:34:10 +01:00
|
|
|
middle = (
|
|
|
|
"<table cellpadding=0><tr><td class=stat2 align=center>%s</td></tr></table>"
|
|
|
|
% middle
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.card.shouldShowTimer():
|
|
|
|
maxTime = self.card.timeLimit() / 1000
|
|
|
|
else:
|
|
|
|
maxTime = 0
|
2019-12-23 01:34:10 +01:00
|
|
|
self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime))
|
2017-08-02 08:22:54 +02:00
|
|
|
self.bottom.web.adjustHeightToFit()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _showEaseButtons(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.bottom.web.setFocus()
|
|
|
|
middle = self._answerButtons()
|
|
|
|
self.bottom.web.eval("showAnswer(%s);" % json.dumps(middle))
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _remaining(self) -> str:
|
2019-12-23 01:34:10 +01:00
|
|
|
if not self.mw.col.conf["dueCounts"]:
|
2012-12-21 08:51:59 +01:00
|
|
|
return ""
|
|
|
|
if self.hadCardQueue:
|
|
|
|
# if it's come from the undo queue, don't count it separately
|
2020-02-27 04:11:21 +01:00
|
|
|
counts: List[Union[int, str]] = list(self.mw.col.sched.counts())
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
counts = list(self.mw.col.sched.counts(self.card))
|
|
|
|
idx = self.mw.col.sched.countIdx(self.card)
|
|
|
|
counts[idx] = "<u>%s</u>" % (counts[idx])
|
|
|
|
space = " + "
|
2020-01-23 06:08:10 +01:00
|
|
|
ctxt = "<span class=new-count>%s</span>" % counts[0]
|
|
|
|
ctxt += space + "<span class=learn-count>%s</span>" % counts[1]
|
|
|
|
ctxt += space + "<span class=review-count>%s</span>" % counts[2]
|
2012-12-21 08:51:59 +01:00
|
|
|
return ctxt
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _defaultEase(self) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.mw.col.sched.answerButtons(self.card) == 4:
|
|
|
|
return 3
|
|
|
|
else:
|
|
|
|
return 2
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _answerButtonList(self) -> Sequence[Tuple[int, str]]:
|
2012-12-21 08:51:59 +01:00
|
|
|
l = ((1, _("Again")),)
|
|
|
|
cnt = self.mw.col.sched.answerButtons(self.card)
|
|
|
|
if cnt == 2:
|
|
|
|
return l + ((2, _("Good")),)
|
|
|
|
elif cnt == 3:
|
|
|
|
return l + ((2, _("Good")), (3, _("Easy")))
|
|
|
|
else:
|
|
|
|
return l + ((2, _("Hard")), (3, _("Good")), (4, _("Easy")))
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _answerButtons(self) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
default = self._defaultEase()
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def but(i, label):
|
|
|
|
if i == default:
|
|
|
|
extra = "id=defease"
|
|
|
|
else:
|
|
|
|
extra = ""
|
|
|
|
due = self._buttonTime(i)
|
2019-12-23 01:34:10 +01:00
|
|
|
return """
|
2018-09-24 06:16:08 +02:00
|
|
|
<td align=center>%s<button %s title="%s" data-ease="%s" onclick='pycmd("ease%d");'>\
|
2019-12-23 01:34:10 +01:00
|
|
|
%s</button></td>""" % (
|
|
|
|
due,
|
|
|
|
extra,
|
|
|
|
_("Shortcut key: %s") % i,
|
|
|
|
i,
|
|
|
|
i,
|
|
|
|
label,
|
|
|
|
)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
buf = "<center><table cellpading=0 cellspacing=0><tr>"
|
|
|
|
for ease, label in self._answerButtonList():
|
|
|
|
buf += but(ease, label)
|
|
|
|
buf += "</tr></table>"
|
|
|
|
script = """
|
|
|
|
<script>$(function () { $("#defease").focus(); });</script>"""
|
|
|
|
return buf + script
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _buttonTime(self, i: int) -> str:
|
2019-12-23 01:34:10 +01:00
|
|
|
if not self.mw.col.conf["estTimes"]:
|
2012-12-21 08:51:59 +01:00
|
|
|
return "<div class=spacer></div>"
|
|
|
|
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " "
|
2019-12-23 01:34:10 +01:00
|
|
|
return "<span class=nobold>%s</span><br>" % txt
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Leeches
|
|
|
|
##########################################################################
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onLeech(self, card: Card) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
# for now
|
|
|
|
s = _("Card was a leech.")
|
|
|
|
if card.queue < 0:
|
|
|
|
s += " " + _("It has been suspended.")
|
|
|
|
tooltip(s)
|
|
|
|
|
|
|
|
# Context menu
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
# note the shortcuts listed here also need to be defined above
|
2018-01-07 18:19:49 +01:00
|
|
|
def _contextMenu(self):
|
2018-11-12 03:01:54 +01:00
|
|
|
currentFlag = self.card and self.card.userFlag()
|
2012-12-21 08:51:59 +01:00
|
|
|
opts = [
|
2019-12-23 01:34:10 +01:00
|
|
|
[
|
|
|
|
_("Flag Card"),
|
|
|
|
[
|
|
|
|
[
|
|
|
|
_("Red Flag"),
|
|
|
|
"Ctrl+1",
|
|
|
|
lambda: self.setFlag(1),
|
|
|
|
dict(checked=currentFlag == 1),
|
|
|
|
],
|
|
|
|
[
|
|
|
|
_("Orange Flag"),
|
|
|
|
"Ctrl+2",
|
|
|
|
lambda: self.setFlag(2),
|
|
|
|
dict(checked=currentFlag == 2),
|
|
|
|
],
|
|
|
|
[
|
|
|
|
_("Green Flag"),
|
|
|
|
"Ctrl+3",
|
|
|
|
lambda: self.setFlag(3),
|
|
|
|
dict(checked=currentFlag == 3),
|
|
|
|
],
|
|
|
|
[
|
|
|
|
_("Blue Flag"),
|
|
|
|
"Ctrl+4",
|
|
|
|
lambda: self.setFlag(4),
|
|
|
|
dict(checked=currentFlag == 4),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
],
|
2017-08-16 12:30:29 +02:00
|
|
|
[_("Mark Note"), "*", self.onMark],
|
2013-10-22 07:28:45 +02:00
|
|
|
[_("Bury Card"), "-", self.onBuryCard],
|
|
|
|
[_("Bury Note"), "=", self.onBuryNote],
|
2013-01-29 01:46:17 +01:00
|
|
|
[_("Suspend Card"), "@", self.onSuspendCard],
|
2012-12-21 08:51:59 +01:00
|
|
|
[_("Suspend Note"), "!", self.onSuspend],
|
2017-07-26 02:52:04 +02:00
|
|
|
[_("Delete Note"), "Ctrl+Delete", self.onDelete],
|
2012-12-21 08:51:59 +01:00
|
|
|
[_("Options"), "O", self.onOptions],
|
|
|
|
None,
|
|
|
|
[_("Replay Audio"), "R", self.replayAudio],
|
2020-01-22 03:50:33 +01:00
|
|
|
[_("Pause Audio"), "5", self.on_pause_audio],
|
|
|
|
[_("Audio -5s"), "6", self.on_seek_backward],
|
|
|
|
[_("Audio +5s"), "7", self.on_seek_forward],
|
2012-12-21 08:51:59 +01:00
|
|
|
[_("Record Own Voice"), "Shift+V", self.onRecordVoice],
|
|
|
|
[_("Replay Own Voice"), "V", self.onReplayRecorded],
|
|
|
|
]
|
2018-01-07 18:19:49 +01:00
|
|
|
return opts
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def showContextMenu(self) -> None:
|
2018-01-07 18:19:49 +01:00
|
|
|
opts = self._contextMenu()
|
2012-12-21 08:51:59 +01:00
|
|
|
m = QMenu(self.mw)
|
2017-08-12 08:08:10 +02:00
|
|
|
self._addMenuItems(m, opts)
|
|
|
|
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.reviewer_will_show_context_menu(self, m)
|
2019-02-05 05:37:07 +01:00
|
|
|
qtMenuShortcutWorkaround(m)
|
2017-08-12 08:08:10 +02:00
|
|
|
m.exec_(QCursor.pos())
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def _addMenuItems(self, m, rows) -> None:
|
2017-08-12 08:08:10 +02:00
|
|
|
for row in rows:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not row:
|
|
|
|
m.addSeparator()
|
|
|
|
continue
|
2017-08-12 08:08:10 +02:00
|
|
|
if len(row) == 2:
|
|
|
|
subm = m.addMenu(row[0])
|
|
|
|
self._addMenuItems(subm, row[1])
|
2019-02-05 05:37:07 +01:00
|
|
|
qtMenuShortcutWorkaround(subm)
|
2017-08-12 08:08:10 +02:00
|
|
|
continue
|
2018-11-12 03:01:54 +01:00
|
|
|
if len(row) == 4:
|
|
|
|
label, scut, func, opts = row
|
|
|
|
else:
|
|
|
|
label, scut, func = row
|
|
|
|
opts = {}
|
2012-12-21 08:51:59 +01:00
|
|
|
a = m.addAction(label)
|
2017-01-10 09:39:31 +01:00
|
|
|
if scut:
|
|
|
|
a.setShortcut(QKeySequence(scut))
|
2018-11-12 03:01:54 +01:00
|
|
|
if opts.get("checked"):
|
|
|
|
a.setCheckable(True)
|
|
|
|
a.setChecked(True)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, func)
|
2017-08-12 08:08:10 +02:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onOptions(self) -> None:
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def setFlag(self, flag: int) -> None:
|
2018-11-12 02:58:36 +01:00
|
|
|
# need to toggle off?
|
|
|
|
if self.card.userFlag() == flag:
|
|
|
|
flag = 0
|
2017-08-12 08:08:10 +02:00
|
|
|
self.card.setUserFlag(flag)
|
|
|
|
self.card.flush()
|
|
|
|
self._drawFlag()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onMark(self) -> None:
|
2017-08-16 12:30:29 +02:00
|
|
|
f = self.card.note()
|
|
|
|
if f.hasTag("marked"):
|
|
|
|
f.delTag("marked")
|
|
|
|
else:
|
|
|
|
f.addTag("marked")
|
|
|
|
f.flush()
|
|
|
|
self._drawMark()
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onSuspend(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.checkpoint(_("Suspend"))
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.col.sched.suspendCards([c.id for c in self.card.note().cards()])
|
2012-12-21 08:51:59 +01:00
|
|
|
tooltip(_("Note suspended."))
|
2013-01-29 01:46:17 +01:00
|
|
|
self.mw.reset()
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onSuspendCard(self) -> None:
|
2013-01-29 01:46:17 +01:00
|
|
|
self.mw.checkpoint(_("Suspend"))
|
|
|
|
self.mw.col.sched.suspendCards([self.card.id])
|
|
|
|
tooltip(_("Card suspended."))
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.reset()
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onDelete(self) -> None:
|
2012-12-22 00:51:21 +01:00
|
|
|
# need to check state because the shortcut is global to the main
|
|
|
|
# window
|
|
|
|
if self.mw.state != "review" or not self.card:
|
|
|
|
return
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.checkpoint(_("Delete"))
|
|
|
|
cnt = len(self.card.note().cards())
|
2020-06-04 10:21:04 +02:00
|
|
|
self.mw.col.remove_notes([self.card.note().id])
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.reset()
|
2019-12-23 01:34:10 +01:00
|
|
|
tooltip(
|
|
|
|
ngettext(
|
|
|
|
"Note and its %d card deleted.", "Note and its %d cards deleted.", cnt
|
|
|
|
)
|
|
|
|
% cnt
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onBuryCard(self) -> None:
|
2013-10-22 07:28:45 +02:00
|
|
|
self.mw.checkpoint(_("Bury"))
|
|
|
|
self.mw.col.sched.buryCards([self.card.id])
|
|
|
|
self.mw.reset()
|
|
|
|
tooltip(_("Card buried."))
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onBuryNote(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.checkpoint(_("Bury"))
|
|
|
|
self.mw.col.sched.buryNote(self.card.nid)
|
|
|
|
self.mw.reset()
|
|
|
|
tooltip(_("Note buried."))
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onRecordVoice(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self._recordedAudio = getAudio(self.mw, encode=False)
|
|
|
|
self.onReplayRecorded()
|
|
|
|
|
2020-02-27 04:11:21 +01:00
|
|
|
def onReplayRecorded(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self._recordedAudio:
|
2020-02-27 04:11:21 +01:00
|
|
|
tooltip(_("You haven't recorded your voice yet."))
|
|
|
|
return
|
2020-01-20 11:10:38 +01:00
|
|
|
av_player.play_file(self._recordedAudio)
|