anki/qt/aqt/previewer.py

373 lines
11 KiB
Python
Raw Normal View History

import json
import re
import time
2020-03-30 10:37:38 +02:00
from typing import Any, List, Optional, Union
2020-03-30 15:40:19 +02:00
from anki.cards import Card
from anki.lang import _
from aqt import AnkiQt, gui_hooks
from aqt.qt import (
QAbstractItemView,
QCheckBox,
QDialog,
QDialogButtonBox,
QKeySequence,
Qt,
QVBoxLayout,
QWidget,
)
from aqt.sound import av_player, play_clicked_audio
from aqt.theme import theme_manager
from aqt.utils import restoreGeom, saveGeom
from aqt.webview import AnkiWebView
class Previewer(QDialog):
2020-04-02 17:34:53 +02:00
_last_state = None
_card_changed = False
_last_render: Union[int, float] = 0
_timer = None
2020-04-03 00:27:26 +02:00
_show_both_sides = False
def __init__(self, parent: QWidget, mw: AnkiQt):
super().__init__(None, Qt.Window)
self._open = True
self._parent = parent
self.mw = mw
2020-03-30 15:40:19 +02:00
def card(self) -> Optional[Card]:
raise NotImplementedError
2020-03-30 15:40:19 +02:00
def open(self):
self._state = "question"
2020-04-02 17:34:53 +02:00
self._last_state = None
self._create_gui()
2020-04-02 17:34:53 +02:00
self._setup_web_view()
2020-04-03 00:29:35 +02:00
self.render_card(True)
self.show()
def _create_gui(self):
self.setWindowTitle(_("Preview"))
self.finished.connect(self._onFinished)
self.silentlyClose = True
2020-03-29 22:28:47 +02:00
self.vbox = QVBoxLayout()
self.vbox.setContentsMargins(0, 0, 0, 0)
self._web = AnkiWebView(title="previewer")
self.vbox.addWidget(self._web)
2020-03-29 22:28:47 +02:00
self.bbox = QDialogButtonBox()
self._replay = self.bbox.addButton(
_("Replay Audio"), QDialogButtonBox.ActionRole
)
self._replay.setAutoDefault(False)
self._replay.setShortcut(QKeySequence("R"))
self._replay.setToolTip(_("Shortcut key: %s" % "R"))
self._replay.clicked.connect(self._onReplayAudio)
2020-04-03 00:27:26 +02:00
both_sides_button = QCheckBox(_("Show Both Sides"))
both_sides_button.setShortcut(QKeySequence("B"))
both_sides_button.setToolTip(_("Shortcut key: %s" % "B"))
self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole)
self._show_both_sides = self.mw.col.conf.get("previewBothSides", False)
both_sides_button.setChecked(self._show_both_sides)
both_sides_button.toggled.connect(self._on_show_both_sides)
2020-03-29 22:28:47 +02:00
self.vbox.addWidget(self.bbox)
self.setLayout(self.vbox)
restoreGeom(self, "preview")
2020-04-02 17:34:53 +02:00
def _on_finished(self, ok):
saveGeom(self, "preview")
2020-04-02 17:34:53 +02:00
self.mw.progress.timer(100, self._on_close, False)
2020-04-02 17:34:53 +02:00
def _on_replay_audio(self):
self.mw.reviewer.replayAudio(self)
def close(self):
if self:
self.close()
2020-04-02 17:34:53 +02:00
self._on_close()
2020-04-02 17:34:53 +02:00
def _on_close(self):
self._open = False
2020-04-02 17:34:53 +02:00
def _setup_web_view(self):
jsinc = [
"jquery.js",
"browsersel.js",
"mathjax/conf.js",
"mathjax/MathJax.js",
"reviewer.js",
]
self._previewWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
)
self._web.set_bridge_command(self._on_bridge_cmd, self)
def _on_bridge_cmd(self, cmd: str) -> Any:
if cmd.startswith("play:"):
2020-03-30 15:40:19 +02:00
play_clicked_audio(cmd, self.card())
2020-04-03 00:29:35 +02:00
def render_card(self, cardChanged=False):
2020-04-02 17:34:53 +02:00
self.cancel_timer()
# Keep track of whether render() has ever been called
# with cardChanged=True since the last successful render
2020-04-02 17:34:53 +02:00
self._card_changed |= cardChanged
# avoid rendering in quick succession
2020-04-03 00:27:26 +02:00
elap_ms = int((time.time() - self._last_render) * 1000)
delay = 300
2020-04-03 00:27:26 +02:00
if elap_ms < delay:
self._timer = self.mw.progress.timer(
2020-04-03 00:27:26 +02:00
delay - elap_ms, self._render_scheduled, False
)
else:
2020-04-02 17:34:53 +02:00
self._render_scheduled()
2020-04-02 17:34:53 +02:00
def cancel_timer(self):
if self._timer:
self._timer.stop()
self._timer = None
2020-04-02 17:34:53 +02:00
def _render_scheduled(self) -> None:
self.cancel_timer()
self._last_render = time.time()
if not self._open:
return
2020-03-30 15:40:19 +02:00
c = self.card()
func = "_showQuestion"
if not c:
txt = _("(please select 1 card)")
bodyclass = ""
2020-04-02 17:34:53 +02:00
self._last_state = None
else:
2020-04-03 00:27:26 +02:00
if self._show_both_sides:
self._state = "answer"
2020-04-02 17:34:53 +02:00
elif self._card_changed:
self._state = "question"
2020-04-02 17:34:53 +02:00
currentState = self._state_and_mod()
if currentState == self._last_state:
# nothing has changed, avoid refreshing
return
# need to force reload even if answer
txt = c.q(reload=True)
if self._state == "answer":
func = "_showAnswer"
txt = c.a()
txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt)
bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
if self.mw.reviewer.autoplay(c):
2020-04-03 00:27:26 +02:00
if self._show_both_sides:
# if we're showing both sides at once, remove any audio
# from the answer that's appeared on the question already
question_audio = c.question_av_tags()
only_on_answer_audio = [
x for x in c.answer_av_tags() if x not in question_audio
]
audio = question_audio + only_on_answer_audio
elif self._state == "question":
audio = c.question_av_tags()
else:
audio = c.answer_av_tags()
av_player.play_tags(audio)
else:
av_player.clear_queue_and_maybe_interrupt()
txt = self.mw.prepare_card_text_for_display(txt)
txt = gui_hooks.card_will_show(txt, c, "preview" + self._state.capitalize())
2020-04-02 17:34:53 +02:00
self._last_state = self._state_and_mod()
self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass))
2020-04-02 17:34:53 +02:00
self._card_changed = False
2020-04-02 17:34:53 +02:00
def _on_show_both_sides(self, toggle):
2020-04-03 00:27:26 +02:00
self._show_both_sides = toggle
self.mw.col.conf["previewBothSides"] = toggle
self.mw.col.setMod()
if self._state == "answer" and not toggle:
self._state = "question"
2020-04-03 00:29:35 +02:00
self.render_card()
2020-04-02 17:34:53 +02:00
def _state_and_mod(self):
2020-03-30 15:40:19 +02:00
c = self.card()
n = c.note()
n.load()
return (self._state, c.id, n.mod)
2020-04-03 00:27:26 +02:00
class MultiCardPreviewer(Previewer):
def card(self) -> Optional[Card]:
# need to state explicitly it's not implement to avoid W0223
raise NotImplementedError
def _create_gui(self):
super()._create_gui()
self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole)
self._prev.setAutoDefault(False)
self._prev.setShortcut(QKeySequence("Left"))
self._prev.setToolTip(_("Shortcut key: Left arrow"))
self._next = self.bbox.addButton(">", QDialogButtonBox.ActionRole)
self._next.setAutoDefault(True)
self._next.setShortcut(QKeySequence("Right"))
self._next.setToolTip(_("Shortcut key: Right arrow or Enter"))
2020-04-02 17:34:53 +02:00
self._prev.clicked.connect(self._on_prev)
self._next.clicked.connect(self._on_next)
2020-04-02 17:34:53 +02:00
def _on_prev(self):
2020-04-03 00:27:26 +02:00
if self._state == "answer" and not self._show_both_sides:
self._state = "question"
2020-04-03 00:29:35 +02:00
self.render_card()
else:
2020-04-02 17:34:53 +02:00
self._on_prev_card()
2020-03-30 09:58:51 +02:00
2020-04-02 17:34:53 +02:00
def _on_prev_card(self):
2020-04-03 00:27:56 +02:00
pass
2020-04-02 17:34:53 +02:00
def _on_next(self):
if self._state == "question":
self._state = "answer"
2020-04-03 00:29:35 +02:00
self.render_card()
else:
2020-04-02 17:34:53 +02:00
self._on_next_card()
2020-03-30 09:58:51 +02:00
2020-04-02 17:34:53 +02:00
def _on_next_card(self):
2020-04-03 00:27:56 +02:00
pass
def _updateButtons(self):
if not self._open:
return
self._prev.setEnabled(self._should_enable_prev())
self._next.setEnabled(self._should_enable_next())
def _should_enable_prev(self):
2020-04-03 00:27:26 +02:00
return self._state == "answer" and not self._show_both_sides
def _should_enable_next(self):
return self._state == "question"
2020-04-02 17:34:53 +02:00
def _on_close(self):
super()._on_close()
self._prev = None
self._next = None
2020-04-03 00:27:26 +02:00
class BrowserPreviewer(MultiCardPreviewer):
def card(self) -> Optional[Card]:
if self._parent.singleCard:
return self._parent.card
else:
return None
2020-04-02 17:34:53 +02:00
def _on_finished(self, ok):
super()._on_finished(ok)
self._parent.form.previewButton.setChecked(False)
2020-04-02 17:34:53 +02:00
def _on_prev_card(self):
self._parent.editor.saveNow(
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
)
2020-04-02 17:34:53 +02:00
def _on_next_card(self):
self._parent.editor.saveNow(
lambda: self._parent._moveCur(QAbstractItemView.MoveDown)
)
def _should_enable_prev(self):
return super()._should_enable_prev() or self._parent.currentRow() > 0
def _should_enable_next(self):
return (
super()._should_enable_next()
or self._parent.currentRow() < self._parent.model.rowCount(None) - 1
)
2020-04-02 17:34:53 +02:00
def _on_close(self):
super()._on_close()
self._parent.previewer = None
2020-04-02 17:34:53 +02:00
def _render_scheduled(self) -> None:
super()._render_scheduled()
self._updateButtons()
2020-03-29 23:41:00 +02:00
2020-04-03 00:27:26 +02:00
class CardListPreviewer(MultiCardPreviewer):
def __init__(self, cards: List[Union[Card, int]], *args, **kwargs):
"""A previewer displaying a list of card.
List can be changed by setting self.cards to a new value.
self.cards contains both cid and card. So that card is loaded
only when required and is not loaded twice.
"""
2020-03-30 10:37:38 +02:00
self.index = 0
self.cards = cards
super().__init__(*args, **kwargs)
def card(self):
if not self.cards:
return None
if isinstance(self.cards[self.index], int):
self.cards[self.index] = self.mw.col.getCard(self.cards[self.index])
return self.cards[self.index]
2020-03-30 10:37:38 +02:00
def open(self):
2020-03-30 10:37:38 +02:00
if not self.cards:
return
super().open()
2020-03-30 10:37:38 +02:00
2020-04-02 17:34:53 +02:00
def _on_prev_card(self):
2020-03-30 10:37:38 +02:00
self.index -= 1
2020-04-03 00:29:35 +02:00
self.render_card()
2020-03-30 10:37:38 +02:00
2020-04-02 17:34:53 +02:00
def _on_next_card(self):
2020-03-30 10:37:38 +02:00
self.index += 1
2020-04-03 00:29:35 +02:00
self.render_card()
2020-03-30 10:37:38 +02:00
def _should_enable_prev(self):
return super()._should_enable_prev() or self.index > 0
def _should_enable_next(self):
return super()._should_enable_next() or self.index < len(self.cards) - 1
def _on_other_side(self):
if self._state == "question":
self._state = "answer"
2020-03-30 10:37:38 +02:00
else:
self._state = "question"
2020-04-03 00:29:35 +02:00
self.render_card()
2020-03-30 10:37:38 +02:00
2020-03-29 23:41:00 +02:00
class SingleCardPreviewer(Previewer):
def __init__(self, card: Card, *args, **kwargs):
self._card = card
super().__init__(*args, **kwargs)
def card(self) -> Card:
return self._card
def _create_gui(self):
super()._create_gui()
self._other_side = self.bbox.addButton(
"Other side", QDialogButtonBox.ActionRole
)
self._other_side.setAutoDefault(False)
self._other_side.setShortcut(QKeySequence("Right"))
self._other_side.setShortcut(QKeySequence("Left"))
self._other_side.setToolTip(_("Shortcut key: Left or Right arrow"))
self._other_side.clicked.connect(self._on_other_side)
def _on_other_side(self):
if self._state == "question":
self._state = "answer"
2020-03-29 23:41:00 +02:00
else:
self._state = "question"
2020-04-03 00:29:35 +02:00
self.render_card()