start reworking card layout screen

- front/back/css shown in tabs
- front/back preview switchable; only one webview needs to be loaded
- dropdown to select cloze number in preview
- search box to search in front/back/css
This commit is contained in:
Damien Elmes 2020-05-14 15:24:29 +10:00
parent f23eb350e4
commit 5167bb57be
7 changed files with 231 additions and 254 deletions

View File

@ -4,7 +4,6 @@
from __future__ import annotations
import copy
import re
import time
from typing import Any, Dict, List, Optional, Tuple, Union

View File

@ -809,7 +809,12 @@ class RustBackend:
self._run_command(pb.BackendInput(set_preferences=prefs))
def cloze_numbers_in_note(self, note: pb.Note) -> List[int]:
return list(self._run_command(pb.BackendInput(cloze_numbers_in_note=note)).cloze_numbers_in_note.numbers)
return list(
self._run_command(
pb.BackendInput(cloze_numbers_in_note=note)
).cloze_numbers_in_note.numbers
)
def translate_string_in(
key: TR, **kwargs: Union[str, int, float]

View File

@ -34,7 +34,6 @@ from typing import Any, Dict, List, Optional, Tuple
import anki
from anki import hooks
from anki.cards import Card
from anki.decks import DeckManager
from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import PartiallyRenderedCard, TemplateReplacementList
@ -57,9 +56,11 @@ class TemplateRenderContext:
@classmethod
def from_card_layout(
cls, note: Note, card: Card, template: Dict
cls, note: Note, card: Card, notetype: NoteType, template: Dict
) -> TemplateRenderContext:
return TemplateRenderContext(note.col, card, note, template=template)
return TemplateRenderContext(
note.col, card, note, notetype=notetype, template=template
)
def __init__(
self,
@ -67,6 +68,7 @@ class TemplateRenderContext:
card: Card,
note: Note,
browser: bool = False,
notetype: NoteType = None,
template: Optional[Dict] = None,
) -> None:
self._col = col.weakref()
@ -74,7 +76,10 @@ class TemplateRenderContext:
self._note = note
self._browser = browser
self._template = template
if not notetype:
self._note_type = note.model()
else:
self._note_type = notetype
# if you need to store extra state to share amongst rendering
# hooks, you can insert it into this dictionary
@ -85,7 +90,8 @@ class TemplateRenderContext:
# legacy
def fields(self) -> Dict[str, str]:
return fields_for_rendering(self.col(), self.card(), self.note())
print(".fields() is obsolote, use .note().items()")
return dict(self._note.items())
def card(self) -> Card:
"""Returns the card being rendered.
@ -177,26 +183,6 @@ def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]:
return q, a # type: ignore
# legacy
def fields_for_rendering(
col: anki.storage._Collection, card: Card, note: Note
) -> Dict[str, str]:
# fields from note
fields = dict(note.items())
# add special fields
fields["Tags"] = note.stringTags().strip()
fields["Type"] = card.note_type()["name"]
fields["Deck"] = col.decks.name(card.odid or card.did)
fields["Subdeck"] = DeckManager.basename(fields["Deck"])
fields["Card"] = card.template()["name"]
flag = card.userFlag()
fields["CardFlag"] = flag and f"flag{flag}" or ""
fields["c%d" % (card.ord + 1)] = "1"
return fields
def apply_custom_filters(
rendered: TemplateReplacementList,
ctx: TemplateRenderContext,
@ -226,7 +212,7 @@ def apply_custom_filters(
"fmod_" + filter_name,
field_text,
"",
ctx.fields(),
ctx.note().items(),
node.field_name,
"",
)

View File

@ -2,7 +2,7 @@
import time
from anki.consts import MODEL_CLOZE
from anki.utils import isWin, joinFields, stripHTML
from anki.utils import isWin, stripHTML
from tests.shared import getEmptyCol

View File

@ -2,6 +2,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import copy
import json
import re
from typing import List, Optional
@ -13,7 +14,7 @@ from anki.lang import _, ngettext
from anki.notes import Note
from anki.rsbackend import TemplateError
from anki.template import TemplateRenderContext
from anki.utils import isMac, isWin, joinFields
from anki.utils import isMac, isWin
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.sound import av_player, play_clicked_audio
@ -36,6 +37,7 @@ from aqt.webview import AnkiWebView
# fixme: card count when removing
# fixme: i18n
# fixme: change tracking and tooltip in fields
# fixme: replay suppression
class CardLayout(QDialog):
@ -77,7 +79,6 @@ class CardLayout(QDialog):
def redraw_everything(self):
self.ignore_change_signals = True
self.updateTopArea()
self.updateMainArea()
self.ignore_change_signals = False
self.update_current_ordinal_and_redraw(self.ord)
@ -184,41 +185,98 @@ class CardLayout(QDialog):
# template area
tform = self.tform = aqt.forms.template.Ui_Form()
tform.setupUi(left)
tform.label1.setText("")
tform.label2.setText("")
tform.labelc1.setText("")
tform.labelc2.setText("")
if self.style().objectName() == "gtk+":
# gtk+ requires margins in inner layout
tform.tlayout1.setContentsMargins(0, 11, 0, 0)
tform.tlayout2.setContentsMargins(0, 11, 0, 0)
tform.tlayout3.setContentsMargins(0, 11, 0, 0)
tform.groupBox_3.setTitle(_("Styling (shared between cards)"))
# tform.groupBox_3.setTitle(_("Styling (shared between cards)"))
qconnect(tform.front.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.css.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.back.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.tabWidget.currentChanged, self.on_editor_changed)
l.addWidget(left, 5)
self.search_box = search = QLineEdit()
search.setPlaceholderText("Search")
qconnect(search.textChanged, self.on_search_changed)
qconnect(search.returnPressed, self.on_search_next)
tform.tabWidget.setCornerWidget(search)
# preview area
right = QWidget()
self.pform: Any = aqt.forms.preview.Ui_Form()
pform = self.pform
pform.setupUi(right)
if self.style().objectName() == "gtk+":
# gtk+ requires margins in inner layout
pform.frontPrevBox.setContentsMargins(0, 11, 0, 0)
pform.backPrevBox.setContentsMargins(0, 11, 0, 0)
if self._isCloze():
nums = self.note.cloze_numbers_in_fields()
if self.ord + 1 not in nums:
# current card is empty
nums.append(self.ord + 1)
self.cloze_numbers = sorted(nums)
self.setup_cloze_number_box()
else:
self.cloze_numbers = []
self.pform.cloze_number_combo.setHidden(True)
self.setupWebviews()
l.addWidget(right, 5)
w.setLayout(l)
def setup_cloze_number_box(self):
names = (_("Cloze %d") % n for n in self.cloze_numbers)
self.pform.cloze_number_combo.addItems(names)
try:
idx = self.cloze_numbers.index(self.ord + 1)
self.pform.cloze_number_combo.setCurrentIndex(idx)
except ValueError:
# invalid cloze
pass
qconnect(
self.pform.cloze_number_combo.currentIndexChanged, self.on_change_cloze
)
def current_editor(self) -> QTextEdit:
idx = self.tform.tabWidget.currentIndex()
if idx == 0:
return self.tform.front
elif idx == 1:
return self.tform.back
else:
return self.tform.css
def on_change_cloze(self, idx: int) -> None:
self.ord = self.cloze_numbers[idx] - 1
self._renderPreview()
def on_editor_changed(self, idx: int) -> None:
if idx == 0:
self.pform.preview_front.setChecked(True)
elif idx == 1:
self.pform.preview_back.setChecked(True)
def on_search_changed(self, text: str):
editor = self.current_editor()
if not editor.find(text):
# try again from top
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.Start)
editor.setTextCursor(cursor)
editor.find(text)
def on_search_next(self):
self.on_search_changed(self.search_box.text())
def setupWebviews(self):
if theme_manager.night_mode and not theme_manager.macos_dark_mode():
# the grouping box renders incorrectly in the fusion theme. 5.9+
# 5.13 behave differently to 5.14, but it looks bad in either case,
# and adjusting the top margin makes the 'save PDF' button show in
# the wrong place, so for now we just disable the border instead
self.setStyleSheet("QGroupBox { border: 0; }")
pform = self.pform
pform.frontWeb = AnkiWebView(title="card layout front")
pform.frontPrevBox.addWidget(pform.frontWeb)
pform.backWeb = AnkiWebView(title="card layout back")
pform.backPrevBox.addWidget(pform.backWeb)
pform.frontWeb = AnkiWebView(title="card layout")
pform.verticalLayout.addWidget(pform.frontWeb)
pform.verticalLayout.setStretch(1, 99)
pform.preview_front.isChecked()
qconnect(pform.preview_front.toggled, self.on_preview_toggled)
qconnect(pform.preview_back.toggled, self.on_preview_toggled)
jsinc = [
"jquery.js",
"browsersel.js",
@ -229,27 +287,24 @@ class CardLayout(QDialog):
pform.frontWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
)
pform.backWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
)
pform.frontWeb.set_bridge_command(self._on_bridge_cmd, self)
pform.backWeb.set_bridge_command(self._on_bridge_cmd, self)
def on_preview_toggled(self):
self._renderPreview()
def _on_bridge_cmd(self, cmd: str) -> Any:
if cmd.startswith("play:"):
play_clicked_audio(cmd, self.rendered_card)
def updateMainArea(self):
if self._isCloze():
cnt = len(self.note.cloze_numbers_in_fields())
for g in self.pform.groupBox, self.pform.groupBox_2:
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
def ephemeral_card_for_rendering(self) -> Card:
card = Card(self.col)
card.ord = self.ord
template = copy.copy(self.current_template())
# may differ in cloze case
template["ord"] = card.ord
# this fetches notetype, we should pass it in
output = TemplateRenderContext.from_card_layout(
self.note, card, template=self.current_template()
self.note, card, notetype=self.model, template=template
).render()
card.set_render_output(output)
return card
@ -288,6 +343,8 @@ class CardLayout(QDialog):
##########################################################################
def current_template(self) -> Dict:
if self._isCloze():
return self.templates[0]
return self.templates[self.ord]
def fill_fields_from_template(self):
@ -344,18 +401,24 @@ class CardLayout(QDialog):
bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
if self.pform.preview_front.isChecked():
q = ti(self.mw.prepare_card_text_for_display(c.q()))
q = gui_hooks.card_will_show(q, c, "clayoutQuestion")
text = q
audio = c.question_av_tags()
else:
a = ti(self.mw.prepare_card_text_for_display(c.a()), type="a")
a = gui_hooks.card_will_show(a, c, "clayoutAnswer")
text = a
audio = c.answer_av_tags()
# use _showAnswer to avoid the longer delay
self.pform.frontWeb.eval("_showAnswer(%s,'%s');" % (json.dumps(q), bodyclass))
self.pform.backWeb.eval("_showAnswer(%s, '%s');" % (json.dumps(a), bodyclass))
self.pform.frontWeb.eval(
"_showAnswer(%s,'%s');" % (json.dumps(text), bodyclass)
)
if c.id not in self.playedAudio:
av_player.play_tags(c.question_av_tags() + c.answer_av_tags())
av_player.play_tags(audio)
self.playedAudio[c.id] = True
self.updateCardNames()
@ -655,7 +718,6 @@ Enter deck to place new %s cards in, or leave blank:"""
av_player.stop_and_clear_queue()
saveGeom(self, "CardLayout")
self.pform.frontWeb = None
self.pform.backWeb = None
self.model = None
self.rendered_card = None
self.mw = None

View File

@ -6,40 +6,76 @@
<rect>
<x>0</x>
<y>0</y>
<width>335</width>
<height>282</height>
<width>717</width>
<height>636</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string notr="true">Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="margin">
<number>0</number>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<widget class="QGroupBox" name="front_back_box">
<property name="title">
<string>Front Preview</string>
<string/>
</property>
<layout class="QVBoxLayout" name="frontPrevBox">
<property name="margin">
<number>0</number>
<property name="flat">
<bool>true</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="preview_front">
<property name="text">
<string notr="true">FRONT</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="preview_back">
<property name="text">
<string notr="true">BACK</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Back Preview</string>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<layout class="QVBoxLayout" name="backPrevBox">
<property name="margin">
<number>0</number>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="cloze_number_combo"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>470</width>
<height>569</height>
<width>525</width>
<height>721</height>
</rect>
</property>
<property name="sizePolicy">
@ -19,173 +19,62 @@
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="margin">
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
<widget class="QLabel" name="changes_affect_label">
<property name="text">
<string notr="true">CHANGES_WILL_AFFECT</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Front Template</string>
</property>
<layout class="QVBoxLayout" name="tlayout1">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string notr="true">FRONT</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTextEdit" name="front"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label1">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Styling</string>
</property>
<layout class="QVBoxLayout" name="tlayout2">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QTextEdit" name="css"/>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="labelc1">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="labelc2">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Back Template</string>
</property>
<layout class="QVBoxLayout" name="tlayout3">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string notr="true">BACK</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="back"/>
</item>
</layout>
</widget>
</item>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string notr="true">STYLING</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label2">
<property name="text">
<string/>
</property>
</widget>
<widget class="QTextEdit" name="css"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>