diff --git a/pylib/anki/models.py b/pylib/anki/models.py
index 0969e8f8d..ea7c48a7e 100644
--- a/pylib/anki/models.py
+++ b/pylib/anki/models.py
@@ -4,7 +4,6 @@
from __future__ import annotations
import copy
-import re
import time
from typing import Any, Dict, List, Optional, Tuple, Union
diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py
index 44ceef559..12a0e1ade 100644
--- a/pylib/anki/rsbackend.py
+++ b/pylib/anki/rsbackend.py
@@ -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]
diff --git a/pylib/anki/template.py b/pylib/anki/template.py
index 298f4db8f..617d90d52 100644
--- a/pylib/anki/template.py
+++ b/pylib/anki/template.py
@@ -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
- 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
# 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,
"",
)
diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py
index e79efb9f5..7246295aa 100644
--- a/pylib/tests/test_models.py
+++ b/pylib/tests/test_models.py
@@ -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
diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py
index 31b844e87..fa90d1c4a 100644
--- a/qt/aqt/clayout.py
+++ b/qt/aqt/clayout.py
@@ -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)
- 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")
- a = gui_hooks.card_will_show(a, c, "clayoutAnswer")
+ 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
diff --git a/qt/designer/preview.ui b/qt/designer/preview.ui
index e8c774cb7..8620a12dc 100644
--- a/qt/designer/preview.ui
+++ b/qt/designer/preview.ui
@@ -6,40 +6,76 @@
0
0
- 335
- 282
+ 717
+ 636
- Form
+ Form
-
-
- 0
-
+
-
-
-
- Front Preview
-
-
-
- 0
-
-
-
-
- -
-
-
- Back Preview
-
-
-
- 0
-
-
-
+
+
-
+
+
+ QLayout::SetMinimumSize
+
+
-
+
+
+
+
+
+ true
+
+
+
+ QLayout::SetMinimumSize
+
+
-
+
+
-
+
+
+ FRONT
+
+
+ true
+
+
+
+ -
+
+
+ BACK
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+
+
diff --git a/qt/designer/template.ui b/qt/designer/template.ui
index 6f802283a..e8fac2d02 100644
--- a/qt/designer/template.ui
+++ b/qt/designer/template.ui
@@ -6,8 +6,8 @@
0
0
- 470
- 569
+ 525
+ 721
@@ -19,173 +19,62 @@
Form
-
-
+
+
0
-
+
+ 0
+
+
+ 0
+
+
0
-
-
-
- 0
+
+
+ CHANGES_WILL_AFFECT
-
-
-
-
- Front Template
-
-
-
- 0
-
-
- 0
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
+
-
-
-
+
+
0
-
-
-
-
- Styling
-
-
-
- 0
-
-
- 0
-
-
-
-
-
-
-
-
- -
-
-
- 0
-
+
+
+ FRONT
+
+
-
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 1
- 15
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 1
- 40
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Preferred
-
-
-
- 1
- 10
-
-
-
+
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
- 10
- 0
-
-
-
- Back Template
-
-
-
- 0
-
-
- 0
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
+
+
+
+ BACK
+
+
+ -
+
+
+
+
+
+
+ STYLING
+
+
+ -
+
+
+
+
+