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 from __future__ import annotations
import copy import copy
import re
import time import time
from typing import Any, Dict, List, Optional, Tuple, Union 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)) self._run_command(pb.BackendInput(set_preferences=prefs))
def cloze_numbers_in_note(self, note: pb.Note) -> List[int]: 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( def translate_string_in(
key: TR, **kwargs: Union[str, int, float] key: TR, **kwargs: Union[str, int, float]

View File

@ -34,7 +34,6 @@ from typing import Any, Dict, List, Optional, Tuple
import anki import anki
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.decks import DeckManager
from anki.models import NoteType from anki.models import NoteType
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import PartiallyRenderedCard, TemplateReplacementList from anki.rsbackend import PartiallyRenderedCard, TemplateReplacementList
@ -57,9 +56,11 @@ class TemplateRenderContext:
@classmethod @classmethod
def from_card_layout( def from_card_layout(
cls, note: Note, card: Card, template: Dict cls, note: Note, card: Card, notetype: NoteType, template: Dict
) -> TemplateRenderContext: ) -> TemplateRenderContext:
return TemplateRenderContext(note.col, card, note, template=template) return TemplateRenderContext(
note.col, card, note, notetype=notetype, template=template
)
def __init__( def __init__(
self, self,
@ -67,6 +68,7 @@ class TemplateRenderContext:
card: Card, card: Card,
note: Note, note: Note,
browser: bool = False, browser: bool = False,
notetype: NoteType = None,
template: Optional[Dict] = None, template: Optional[Dict] = None,
) -> None: ) -> None:
self._col = col.weakref() self._col = col.weakref()
@ -74,7 +76,10 @@ class TemplateRenderContext:
self._note = note self._note = note
self._browser = browser self._browser = browser
self._template = template self._template = template
self._note_type = note.model() if not notetype:
self._note_type = note.model()
else:
self._note_type = notetype
# if you need to store extra state to share amongst rendering # if you need to store extra state to share amongst rendering
# hooks, you can insert it into this dictionary # hooks, you can insert it into this dictionary
@ -85,7 +90,8 @@ class TemplateRenderContext:
# legacy # legacy
def fields(self) -> Dict[str, str]: 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: def card(self) -> Card:
"""Returns the card being rendered. """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 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( def apply_custom_filters(
rendered: TemplateReplacementList, rendered: TemplateReplacementList,
ctx: TemplateRenderContext, ctx: TemplateRenderContext,
@ -226,7 +212,7 @@ def apply_custom_filters(
"fmod_" + filter_name, "fmod_" + filter_name,
field_text, field_text,
"", "",
ctx.fields(), ctx.note().items(),
node.field_name, node.field_name,
"", "",
) )

View File

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

View File

@ -2,6 +2,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import copy
import json import json
import re import re
from typing import List, Optional from typing import List, Optional
@ -13,7 +14,7 @@ from anki.lang import _, ngettext
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import TemplateError from anki.rsbackend import TemplateError
from anki.template import TemplateRenderContext 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 import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player, play_clicked_audio from aqt.sound import av_player, play_clicked_audio
@ -36,6 +37,7 @@ from aqt.webview import AnkiWebView
# fixme: card count when removing # fixme: card count when removing
# fixme: i18n # fixme: i18n
# fixme: change tracking and tooltip in fields # fixme: change tracking and tooltip in fields
# fixme: replay suppression
class CardLayout(QDialog): class CardLayout(QDialog):
@ -77,7 +79,6 @@ class CardLayout(QDialog):
def redraw_everything(self): def redraw_everything(self):
self.ignore_change_signals = True self.ignore_change_signals = True
self.updateTopArea() self.updateTopArea()
self.updateMainArea()
self.ignore_change_signals = False self.ignore_change_signals = False
self.update_current_ordinal_and_redraw(self.ord) self.update_current_ordinal_and_redraw(self.ord)
@ -184,41 +185,98 @@ class CardLayout(QDialog):
# template area # template area
tform = self.tform = aqt.forms.template.Ui_Form() tform = self.tform = aqt.forms.template.Ui_Form()
tform.setupUi(left) tform.setupUi(left)
tform.label1.setText("") # tform.groupBox_3.setTitle(_("Styling (shared between cards)"))
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)"))
qconnect(tform.front.textChanged, self.write_edits_to_template_and_redraw) qconnect(tform.front.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.css.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.back.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.tabWidget.currentChanged, self.on_editor_changed)
l.addWidget(left, 5) 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 # preview area
right = QWidget() right = QWidget()
self.pform: Any = aqt.forms.preview.Ui_Form() self.pform: Any = aqt.forms.preview.Ui_Form()
pform = self.pform pform = self.pform
pform.setupUi(right) pform.setupUi(right)
if self.style().objectName() == "gtk+":
# gtk+ requires margins in inner layout if self._isCloze():
pform.frontPrevBox.setContentsMargins(0, 11, 0, 0) nums = self.note.cloze_numbers_in_fields()
pform.backPrevBox.setContentsMargins(0, 11, 0, 0) 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() self.setupWebviews()
l.addWidget(right, 5) l.addWidget(right, 5)
w.setLayout(l) 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): 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 = self.pform
pform.frontWeb = AnkiWebView(title="card layout front") pform.frontWeb = AnkiWebView(title="card layout")
pform.frontPrevBox.addWidget(pform.frontWeb) pform.verticalLayout.addWidget(pform.frontWeb)
pform.backWeb = AnkiWebView(title="card layout back") pform.verticalLayout.setStretch(1, 99)
pform.backPrevBox.addWidget(pform.backWeb) pform.preview_front.isChecked()
qconnect(pform.preview_front.toggled, self.on_preview_toggled)
qconnect(pform.preview_back.toggled, self.on_preview_toggled)
jsinc = [ jsinc = [
"jquery.js", "jquery.js",
"browsersel.js", "browsersel.js",
@ -229,27 +287,24 @@ class CardLayout(QDialog):
pform.frontWeb.stdHtml( pform.frontWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self, 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.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: def _on_bridge_cmd(self, cmd: str) -> Any:
if cmd.startswith("play:"): if cmd.startswith("play:"):
play_clicked_audio(cmd, self.rendered_card) 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: def ephemeral_card_for_rendering(self) -> Card:
card = Card(self.col) card = Card(self.col)
card.ord = self.ord 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( output = TemplateRenderContext.from_card_layout(
self.note, card, template=self.current_template() self.note, card, notetype=self.model, template=template
).render() ).render()
card.set_render_output(output) card.set_render_output(output)
return card return card
@ -288,6 +343,8 @@ class CardLayout(QDialog):
########################################################################## ##########################################################################
def current_template(self) -> Dict: def current_template(self) -> Dict:
if self._isCloze():
return self.templates[0]
return self.templates[self.ord] return self.templates[self.ord]
def fill_fields_from_template(self): def fill_fields_from_template(self):
@ -344,18 +401,24 @@ class CardLayout(QDialog):
bodyclass = theme_manager.body_classes_for_card_ord(c.ord) bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
q = ti(self.mw.prepare_card_text_for_display(c.q())) if self.pform.preview_front.isChecked():
q = gui_hooks.card_will_show(q, c, "clayoutQuestion") q = ti(self.mw.prepare_card_text_for_display(c.q()))
q = gui_hooks.card_will_show(q, c, "clayoutQuestion")
a = ti(self.mw.prepare_card_text_for_display(c.a()), type="a") text = q
a = gui_hooks.card_will_show(a, c, "clayoutAnswer") 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 # use _showAnswer to avoid the longer delay
self.pform.frontWeb.eval("_showAnswer(%s,'%s');" % (json.dumps(q), bodyclass)) self.pform.frontWeb.eval(
self.pform.backWeb.eval("_showAnswer(%s, '%s');" % (json.dumps(a), bodyclass)) "_showAnswer(%s,'%s');" % (json.dumps(text), bodyclass)
)
if c.id not in self.playedAudio: 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.playedAudio[c.id] = True
self.updateCardNames() self.updateCardNames()
@ -655,7 +718,6 @@ Enter deck to place new %s cards in, or leave blank:"""
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
saveGeom(self, "CardLayout") saveGeom(self, "CardLayout")
self.pform.frontWeb = None self.pform.frontWeb = None
self.pform.backWeb = None
self.model = None self.model = None
self.rendered_card = None self.rendered_card = None
self.mw = None self.mw = None

View File

@ -6,40 +6,76 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>335</width> <width>717</width>
<height>282</height> <height>636</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string notr="true">Form</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item> <item>
<widget class="QGroupBox" name="groupBox"> <layout class="QVBoxLayout" name="verticalLayout">
<property name="title"> <item>
<string>Front Preview</string> <layout class="QHBoxLayout" name="horizontalLayout_4">
</property> <property name="sizeConstraint">
<layout class="QVBoxLayout" name="frontPrevBox"> <enum>QLayout::SetMinimumSize</enum>
<property name="margin"> </property>
<number>0</number> <item>
</property> <widget class="QGroupBox" name="front_back_box">
</layout> <property name="title">
</widget> <string/>
</item> </property>
<item> <property name="flat">
<widget class="QGroupBox" name="groupBox_2"> <bool>true</bool>
<property name="title"> </property>
<string>Back Preview</string> <layout class="QHBoxLayout" name="horizontalLayout_2">
</property> <property name="sizeConstraint">
<layout class="QVBoxLayout" name="backPrevBox"> <enum>QLayout::SetMinimumSize</enum>
<property name="margin"> </property>
<number>0</number> <item>
</property> <layout class="QHBoxLayout" name="horizontalLayout">
</layout> <item>
</widget> <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>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<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>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>470</width> <width>525</width>
<height>569</height> <height>721</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -19,173 +19,62 @@
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_5"> <layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing"> <property name="leftMargin">
<number>0</number> <number>0</number>
</property> </property>
<property name="margin"> <property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <widget class="QLabel" name="changes_affect_label">
<property name="spacing"> <property name="text">
<number>0</number> <string notr="true">CHANGES_WILL_AFFECT</string>
</property> </property>
<item> </widget>
<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">
<number>0</number>
</property>
<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>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <widget class="QTabWidget" name="tabWidget">
<property name="spacing"> <property name="currentIndex">
<number>0</number> <number>0</number>
</property> </property>
<item> <widget class="QWidget" name="tab">
<widget class="QGroupBox" name="groupBox_3"> <attribute name="title">
<property name="title"> <string notr="true">FRONT</string>
<string>Styling</string> </attribute>
</property> <layout class="QVBoxLayout" name="verticalLayout_2">
<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> <item>
<spacer name="verticalSpacer"> <widget class="QTextEdit" name="front"/>
<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> </item>
</layout> </layout>
</item> </widget>
</layout> <widget class="QWidget" name="tab_2">
</item> <attribute name="title">
<item> <string notr="true">BACK</string>
<layout class="QHBoxLayout" name="horizontalLayout_3"> </attribute>
<property name="spacing"> <layout class="QVBoxLayout" name="verticalLayout_3">
<number>0</number> <item>
</property> <widget class="QTextEdit" name="back"/>
<item> </item>
<widget class="QGroupBox" name="groupBox_2"> </layout>
<property name="sizePolicy"> </widget>
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> <widget class="QWidget" name="tab_3">
<horstretch>10</horstretch> <attribute name="title">
<verstretch>0</verstretch> <string notr="true">STYLING</string>
</sizepolicy> </attribute>
</property> <layout class="QVBoxLayout" name="verticalLayout_5">
<property name="title"> <item>
<string>Back Template</string> <widget class="QTextEdit" name="css"/>
</property> </item>
<layout class="QVBoxLayout" name="tlayout3"> </layout>
<property name="spacing"> </widget>
<number>0</number> </widget>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QTextEdit" name="back"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label2">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item> </item>
</layout> </layout>
</widget> </widget>