update the card layout screen

- changes are now committed in bulk when closing the dialog,
and can be canceled
- it's not necessary to save the note to the database to preview it
- duplicate fields are now shown as duplicates in the top list
- redraw preview more quickly
- use + instead of _ when deduplicating names, as the latter is a
glob character
This commit is contained in:
Damien Elmes 2020-05-13 17:24:49 +10:00
parent 12b8fe6147
commit 6680cdf1d3
11 changed files with 251 additions and 184 deletions

View file

@ -33,7 +33,7 @@ message BackendInput {
DeckTreeIn deck_tree = 18; DeckTreeIn deck_tree = 18;
SearchCardsIn search_cards = 19; SearchCardsIn search_cards = 19;
SearchNotesIn search_notes = 20; SearchNotesIn search_notes = 20;
// RenderCardIn render_card = 21; RenderUncommittedCardIn render_uncommitted_card = 21;
int64 local_minutes_west = 22; int64 local_minutes_west = 22;
string strip_av_tags = 23; string strip_av_tags = 23;
ExtractAVTagsIn extract_av_tags = 24; ExtractAVTagsIn extract_av_tags = 24;
@ -120,7 +120,7 @@ message BackendOutput {
DeckTreeNode deck_tree = 18; DeckTreeNode deck_tree = 18;
SearchCardsOut search_cards = 19; SearchCardsOut search_cards = 19;
SearchNotesOut search_notes = 20; SearchNotesOut search_notes = 20;
// RenderCardOut render_card = 21; RenderCardOut render_uncommitted_card = 21;
string add_media_file = 26; string add_media_file = 26;
Empty sync_media = 27; Empty sync_media = 27;
MediaCheckOut check_media = 28; MediaCheckOut check_media = 28;
@ -269,6 +269,12 @@ message RenderExistingCardIn {
bool browser = 2; bool browser = 2;
} }
message RenderUncommittedCardIn {
Note note = 1;
uint32 card_ord = 2;
bytes template = 3;
}
message RenderCardOut { message RenderCardOut {
repeated RenderedTemplateNode question_nodes = 1; repeated RenderedTemplateNode question_nodes = 1;
repeated RenderedTemplateNode answer_nodes = 2; repeated RenderedTemplateNode answer_nodes = 2;

View file

@ -14,7 +14,6 @@ from anki.models import NoteType, Template
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import BackendCard from anki.rsbackend import BackendCard
from anki.sound import AVTag from anki.sound import AVTag
from anki.utils import joinFields
# Cards # Cards
########################################################################## ##########################################################################
@ -133,6 +132,9 @@ class Card:
).render() ).render()
return self._render_output return self._render_output
def set_render_output(self, output: anki.template.TemplateRenderOutput) -> None:
self._render_output = output
def note(self, reload: bool = False) -> Note: def note(self, reload: bool = False) -> Note:
if not self._note or reload: if not self._note or reload:
self._note = self.col.getNote(self.nid) self._note = self.col.getNote(self.nid)

View file

@ -408,24 +408,27 @@ class ModelManager:
t["name"] = name t["name"] = name
return t return t
def addTemplate(self, m: NoteType, template: Template) -> None: def addTemplate(self, m: NoteType, template: Template, save=True) -> None:
if m["id"]: if m["id"]:
self.col.modSchema(check=True) self.col.modSchema(check=True)
m["tmpls"].append(template) m["tmpls"].append(template)
if m["id"]: if save and m["id"]:
self.save(m) self.save(m)
def remTemplate(self, m: NoteType, template: Template) -> None: def remTemplate(self, m: NoteType, template: Template, save=True) -> None:
assert len(m["tmpls"]) > 1 assert len(m["tmpls"]) > 1
self.col.modSchema(check=True) self.col.modSchema(check=True)
m["tmpls"].remove(template) m["tmpls"].remove(template)
self.save(m) if save:
self.save(m)
def moveTemplate(self, m: NoteType, template: Template, idx: int) -> None: def moveTemplate(
self, m: NoteType, template: Template, idx: int, save=True
) -> None:
self.col.modSchema(check=True) self.col.modSchema(check=True)
oldidx = m["tmpls"].index(template) oldidx = m["tmpls"].index(template)
@ -435,7 +438,8 @@ class ModelManager:
m["tmpls"].remove(template) m["tmpls"].remove(template)
m["tmpls"].insert(idx, template) m["tmpls"].insert(idx, template)
self.save(m) if save:
self.save(m)
# Model changing # Model changing
########################################################################## ##########################################################################

View file

@ -312,6 +312,23 @@ class RustBackend:
return PartiallyRenderedCard(qnodes, anodes) return PartiallyRenderedCard(qnodes, anodes)
def render_uncommitted_card(
self, note: BackendNote, card_ord: int, template: Dict
) -> PartiallyRenderedCard:
template_json = orjson.dumps(template)
out = self._run_command(
pb.BackendInput(
render_uncommitted_card=pb.RenderUncommittedCardIn(
note=note, template=template_json, card_ord=card_ord
)
)
).render_uncommitted_card
qnodes = proto_replacement_list_to_native(out.question_nodes) # type: ignore
anodes = proto_replacement_list_to_native(out.answer_nodes) # type: ignore
return PartiallyRenderedCard(qnodes, anodes)
def local_minutes_west(self, stamp: int) -> int: def local_minutes_west(self, stamp: int) -> int:
return self._run_command( return self._run_command(
pb.BackendInput(local_minutes_west=stamp) pb.BackendInput(local_minutes_west=stamp)

View file

@ -56,15 +56,10 @@ class TemplateRenderContext:
return TemplateRenderContext(card.col, card, card.note(), browser) return TemplateRenderContext(card.col, card, card.note(), browser)
@classmethod @classmethod
def from_card_layout(cls, note: Note, card_ord: int) -> TemplateRenderContext: def from_card_layout(
card = cls.synthesized_card(note.col, card_ord) cls, note: Note, card: Card, template: Dict
return TemplateRenderContext(note.col, card, note) ) -> TemplateRenderContext:
return TemplateRenderContext(note.col, card, note, template=template)
@classmethod
def synthesized_card(cls, col: anki.storage._Collection, ord: int):
c = Card(col)
c.ord = ord
return c
def __init__( def __init__(
self, self,
@ -72,7 +67,7 @@ class TemplateRenderContext:
card: Card, card: Card,
note: Note, note: Note,
browser: bool = False, browser: bool = False,
template: Optional[Any] = None, template: Optional[Dict] = None,
) -> None: ) -> None:
self._col = col.weakref() self._col = col.weakref()
self._card = card self._card = card
@ -146,7 +141,9 @@ class TemplateRenderContext:
def _partially_render(self) -> PartiallyRenderedCard: def _partially_render(self) -> PartiallyRenderedCard:
if self._template: if self._template:
# card layout screen # card layout screen
raise Exception("nyi") return self._col.backend.render_uncommitted_card(
self._note.to_backend_note(), self._card.ord, self._template
)
else: else:
# existing card (eg study mode) # existing card (eg study mode)
return self._col.backend.render_existing_card(self._card.id, self._browser) return self._col.backend.render_existing_card(self._card.id, self._browser)

View file

@ -2,10 +2,9 @@
# 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 collections
import json import json
import re import re
from typing import Optional from typing import List, Optional
import aqt import aqt
from anki.cards import Card from anki.cards import Card
@ -13,6 +12,7 @@ from anki.consts import *
from anki.lang import _, ngettext 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.utils import isMac, isWin, joinFields from anki.utils import isMac, isWin, joinFields
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
@ -27,41 +27,34 @@ from aqt.utils import (
saveGeom, saveGeom,
showInfo, showInfo,
showWarning, showWarning,
tooltip,
) )
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
# fixme: removeColons()
# fixme: previewing with empty fields
# fixme: deck name on new cards
# fixme: card count when removing
# fixme: i18n
# fixme: change tracking and tooltip in fields
class CardLayout(QDialog): class CardLayout(QDialog):
card: Optional[Card]
def __init__( def __init__(
self, self, mw: AnkiQt, note: Note, ord=0, parent: Optional[QWidget] = None,
mw: AnkiQt,
note: Note,
ord=0,
parent: Optional[QWidget] = None,
addMode=False,
): ):
QDialog.__init__(self, parent or mw, Qt.Window) QDialog.__init__(self, parent or mw, Qt.Window)
mw.setupDialogGC(self) mw.setupDialogGC(self)
self.mw = aqt.mw self.mw = aqt.mw
self.note = note self.note = note
self.ord = ord self.ord = ord
self.col = self.mw.col self.col = self.mw.col.weakref()
self.mm = self.mw.col.models self.mm = self.mw.col.models
self.model = note.model() self.model = note.model()
self.templates = self.model["tmpls"]
self.mm._remove_from_cache(self.model["id"])
self.mw.checkpoint(_("Card Types")) self.mw.checkpoint(_("Card Types"))
self.addMode = addMode self.changed = False
if addMode:
# save it to DB temporarily
self.emptyFields = []
for name, val in list(note.items()):
if val.strip():
continue
self.emptyFields.append(name)
note[name] = "(%s)" % name
note.flush()
self.removeColons()
self.setupTopArea() self.setupTopArea()
self.setupMainArea() self.setupMainArea()
self.setupButtons() self.setupButtons()
@ -74,7 +67,7 @@ class CardLayout(QDialog):
v1.setContentsMargins(12, 12, 12, 12) v1.setContentsMargins(12, 12, 12, 12)
self.setLayout(v1) self.setLayout(v1)
gui_hooks.card_layout_will_show(self) gui_hooks.card_layout_will_show(self)
self.redraw() self.redraw_everything()
restoreGeom(self, "CardLayout") restoreGeom(self, "CardLayout")
self.setWindowModality(Qt.ApplicationModal) self.setWindowModality(Qt.ApplicationModal)
self.show() self.show()
@ -82,32 +75,26 @@ class CardLayout(QDialog):
# as users tend to accidentally type into the template # as users tend to accidentally type into the template
self.setFocus() self.setFocus()
def redraw(self): def redraw_everything(self):
did = None self.ignore_change_signals = True
if hasattr(self.parent(), "deckChooser"):
did = self.parent().deckChooser.selectedId()
self.cards = self.col.previewCards(self.note, 2, did=did)
idx = self.ord
if idx >= len(self.cards):
self.ord = len(self.cards) - 1
self.redrawing = True
self.updateTopArea() self.updateTopArea()
self.updateMainArea() self.updateMainArea()
self.redrawing = False self.ignore_change_signals = False
self.onCardSelected(self.ord) self.update_current_ordinal_and_redraw(self.ord)
def setupShortcuts(self): def update_current_ordinal_and_redraw(self, idx):
for i in range(1, 9): if self.ignore_change_signals:
QShortcut( return
QKeySequence("Ctrl+%d" % i), self.ord = idx
self, self.playedAudio = {}
activated=lambda i=i: self.selectCard(i), self.fill_fields_from_template()
) # type: ignore self.renderPreview()
def selectCard(self, n): def _isCloze(self):
self.ord = n - 1 return self.model["type"] == MODEL_CLOZE
self.redraw()
# Top area
##########################################################################
def setupTopArea(self): def setupTopArea(self):
self.topArea = QWidget() self.topArea = QWidget()
@ -115,7 +102,10 @@ class CardLayout(QDialog):
self.topAreaForm.setupUi(self.topArea) self.topAreaForm.setupUi(self.topArea)
self.topAreaForm.templateOptions.setText(_("Options") + " " + downArrow()) self.topAreaForm.templateOptions.setText(_("Options") + " " + downArrow())
qconnect(self.topAreaForm.templateOptions.clicked, self.onMore) qconnect(self.topAreaForm.templateOptions.clicked, self.onMore)
qconnect(self.topAreaForm.templatesBox.currentIndexChanged, self.onCardSelected) qconnect(
self.topAreaForm.templatesBox.currentIndexChanged,
self.update_current_ordinal_and_redraw,
)
def updateTopArea(self): def updateTopArea(self):
cnt = self.mw.col.models.useCount(self.model) cnt = self.mw.col.models.useCount(self.model)
@ -130,17 +120,19 @@ class CardLayout(QDialog):
self.updateCardNames() self.updateCardNames()
def updateCardNames(self): def updateCardNames(self):
self.redrawing = True self.ignore_change_signals = True
combo = self.topAreaForm.templatesBox combo = self.topAreaForm.templatesBox
combo.clear() combo.clear()
combo.addItems(self._summarizedName(t) for t in self.model["tmpls"]) combo.addItems(
self._summarizedName(idx, tmpl) for (idx, tmpl) in enumerate(self.templates)
)
combo.setCurrentIndex(self.ord) combo.setCurrentIndex(self.ord)
combo.setEnabled(not self._isCloze()) combo.setEnabled(not self._isCloze())
self.redrawing = False self.ignore_change_signals = False
def _summarizedName(self, tmpl): def _summarizedName(self, idx: int, tmpl: Dict):
return "{}: {}: {} -> {}".format( return "{}: {}: {} -> {}".format(
tmpl["ord"] + 1, idx + 1,
tmpl["name"], tmpl["name"],
self._fieldsOnTemplate(tmpl["qfmt"]), self._fieldsOnTemplate(tmpl["qfmt"]),
self._fieldsOnTemplate(tmpl["afmt"]), self._fieldsOnTemplate(tmpl["afmt"]),
@ -148,8 +140,8 @@ class CardLayout(QDialog):
def _fieldsOnTemplate(self, fmt): def _fieldsOnTemplate(self, fmt):
matches = re.findall("{{[^#/}]+?}}", fmt) matches = re.findall("{{[^#/}]+?}}", fmt)
charsAllowed = 30 chars_allowed = 30
result: Dict[str, bool] = collections.OrderedDict() field_names: List[str] = []
for m in matches: for m in matches:
# strip off mustache # strip off mustache
m = re.sub(r"[{}]", "", m) m = re.sub(r"[{}]", "", m)
@ -159,19 +151,30 @@ class CardLayout(QDialog):
if m == "FrontSide": if m == "FrontSide":
continue continue
if m not in result: field_names.append(m)
result[m] = True chars_allowed -= len(m)
charsAllowed -= len(m) if chars_allowed <= 0:
if charsAllowed <= 0: break
break
s = "+".join(result.keys()) s = "+".join(field_names)
if charsAllowed <= 0: if chars_allowed <= 0:
s += "+..." s += "+..."
return s return s
def _isCloze(self): def setupShortcuts(self):
return self.model["type"] == MODEL_CLOZE for i in range(1, 9):
QShortcut(
QKeySequence("Ctrl+%d" % i),
self,
activated=lambda i=i: self.selectCard(i),
) # type: ignore
def selectCard(self, n):
self.ord = n - 1
self.redraw_everything()
# Main area
##########################################################################
def setupMainArea(self): def setupMainArea(self):
w = self.mainArea = QWidget() w = self.mainArea = QWidget()
@ -192,9 +195,9 @@ class CardLayout(QDialog):
tform.tlayout2.setContentsMargins(0, 11, 0, 0) tform.tlayout2.setContentsMargins(0, 11, 0, 0)
tform.tlayout3.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.saveCard) qconnect(tform.front.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.css.textChanged, self.saveCard) qconnect(tform.css.textChanged, self.write_edits_to_template_and_redraw)
qconnect(tform.back.textChanged, self.saveCard) qconnect(tform.back.textChanged, self.write_edits_to_template_and_redraw)
l.addWidget(left, 5) l.addWidget(left, 5)
# preview area # preview area
right = QWidget() right = QWidget()
@ -235,7 +238,7 @@ class CardLayout(QDialog):
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.card) play_clicked_audio(cmd, self.rendered_card)
def updateMainArea(self): def updateMainArea(self):
if self._isCloze(): if self._isCloze():
@ -243,23 +246,14 @@ class CardLayout(QDialog):
for g in self.pform.groupBox, self.pform.groupBox_2: for g in self.pform.groupBox, self.pform.groupBox_2:
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1)) g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
def onRemove(self): def ephemeral_card_for_rendering(self) -> Card:
if len(self.model["tmpls"]) < 2: card = Card(self.col)
return showInfo(_("At least one card type is required.")) card.ord = self.ord
idx = self.ord output = TemplateRenderContext.from_card_layout(
msg = _("Delete the '%(a)s' card type, and its %(b)s?") % dict( self.note, card, template=self.current_template()
a=self.model["tmpls"][idx]["name"], b=_("cards") ).render()
) card.set_render_output(output)
if not askUser(msg): return card
return
self.mm.remTemplate(self.model, self.cards[idx].template())
self.redraw()
def removeColons(self):
# colons in field names conflict with the template language
for fld in self.model["flds"]:
if ":" in fld["name"]:
self.mm.renameField(self.model, fld, fld["name"])
# Buttons # Buttons
########################################################################## ##########################################################################
@ -291,21 +285,15 @@ class CardLayout(QDialog):
l.addWidget(close) l.addWidget(close)
qconnect(close.clicked, self.reject) qconnect(close.clicked, self.reject)
# Cards # Reading/writing question/answer/css
########################################################################## ##########################################################################
def onCardSelected(self, idx): def current_template(self) -> Dict:
if self.redrawing: return self.templates[self.ord]
return
self.card = self.cards[idx]
self.ord = idx
self.playedAudio = {}
self.readCard()
self.renderPreview()
def readCard(self): def fill_fields_from_template(self):
t = self.card.template() t = self.current_template()
self.redrawing = True self.ignore_change_signals = True
self.tform.front.setPlainText(t["qfmt"]) self.tform.front.setPlainText(t["qfmt"])
self.tform.css.setPlainText(self.model["css"]) self.tform.css.setPlainText(self.model["css"])
self.tform.back.setPlainText(t["afmt"]) self.tform.back.setPlainText(t["afmt"])
@ -321,17 +309,16 @@ class CardLayout(QDialog):
self.tform.front.setTabStopDistance(tab_width) self.tform.front.setTabStopDistance(tab_width)
self.tform.css.setTabStopDistance(tab_width) self.tform.css.setTabStopDistance(tab_width)
self.tform.back.setTabStopDistance(tab_width) self.tform.back.setTabStopDistance(tab_width)
self.redrawing = False self.ignore_change_signals = False
def saveCard(self): def write_edits_to_template_and_redraw(self):
if self.redrawing: if self.ignore_change_signals:
return return
text = self.tform.front.toPlainText() self.changed = True
self.card.template()["qfmt"] = text t = self.current_template()
text = self.tform.css.toPlainText() t["qfmt"] = self.tform.front.toPlainText()
self.card.model()["css"] = text t["afmt"] = self.tform.back.toPlainText()
text = self.tform.back.toPlainText() self.model["css"] = self.tform.css.toPlainText()
self.card.template()["afmt"] = text
self.renderPreview() self.renderPreview()
# Preview # Preview
@ -342,7 +329,7 @@ class CardLayout(QDialog):
def renderPreview(self): def renderPreview(self):
# schedule a preview when timing stops # schedule a preview when timing stops
self.cancelPreviewTimer() self.cancelPreviewTimer()
self._previewTimer = self.mw.progress.timer(500, self._renderPreview, False) self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False)
def cancelPreviewTimer(self): def cancelPreviewTimer(self):
if self._previewTimer: if self._previewTimer:
@ -352,12 +339,13 @@ class CardLayout(QDialog):
def _renderPreview(self) -> None: def _renderPreview(self) -> None:
self.cancelPreviewTimer() self.cancelPreviewTimer()
c = self.card c = self.rendered_card = self.ephemeral_card_for_rendering()
ti = self.maybeTextInput ti = self.maybeTextInput
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(reload=True))) q = ti(self.mw.prepare_card_text_for_display(c.q()))
q = gui_hooks.card_will_show(q, c, "clayoutQuestion") q = gui_hooks.card_will_show(q, c, "clayoutQuestion")
a = ti(self.mw.prepare_card_text_for_display(c.a()), type="a") a = ti(self.mw.prepare_card_text_for_display(c.a()), type="a")
@ -398,21 +386,43 @@ class CardLayout(QDialog):
# Card operations # Card operations
###################################################################### ######################################################################
def onRename(self): def onRemove(self):
name = getOnlyText(_("New name:"), default=self.card.template()["name"]) if len(self.templates) < 2:
if not name: return showInfo(_("At least one card type is required."))
template = self.current_template()
msg = _("Delete the '%(a)s' card type, and its %(b)s?") % dict(
a=template["name"], b=_("cards")
)
if not askUser(msg):
return return
if name in [
c.template()["name"] for c in self.cards if c.template()["ord"] != self.ord self.changed = True
]: self.mm.remTemplate(self.model, template, save=False)
return showWarning(_("That name is already used."))
self.card.template()["name"] = name # ensure current ordinal is within bounds
self.redraw() idx = self.ord
if idx >= len(self.templates):
self.ord = len(self.templates) - 1
self.redraw_everything()
def onRename(self):
template = self.current_template()
name = getOnlyText(_("New name:"), default=template["name"])
if not name.strip():
return
self.changed = True
template["name"] = name
self.redraw_everything()
def onReorder(self): def onReorder(self):
n = len(self.cards) n = len(self.templates)
cur = self.card.template()["ord"] + 1 # type: ignore template = self.current_template()
pos = getOnlyText(_("Enter new card position (1...%s):") % n, default=str(cur)) current_pos = self.templates.index(template) + 1
pos = getOnlyText(
_("Enter new card position (1...%s):") % n, default=str(current_pos)
)
if not pos: if not pos:
return return
try: try:
@ -421,18 +431,19 @@ class CardLayout(QDialog):
return return
if pos < 1 or pos > n: if pos < 1 or pos > n:
return return
if pos == cur: if pos == current_pos:
return return
pos -= 1 new_idx = pos - 1
self.mm.moveTemplate(self.model, self.card.template(), pos) self.changed = True
self.ord = pos self.mm.moveTemplate(self.model, template, new_idx, save=False)
self.redraw() self.ord = new_idx
self.redraw_everything()
def _newCardName(self): def _newCardName(self):
n = len(self.cards) + 1 n = len(self.templates) + 1
while 1: while 1:
name = _("Card %d") % n name = _("Card %d") % n
if name not in [c.template()["name"] for c in self.cards]: if name not in [t["name"] for t in self.templates]:
break break
n += 1 n += 1
return name return name
@ -449,19 +460,20 @@ class CardLayout(QDialog):
) )
if not askUser(txt): if not askUser(txt):
return return
self.changed = True
name = self._newCardName() name = self._newCardName()
t = self.mm.newTemplate(name) t = self.mm.newTemplate(name)
old = self.card.template() old = self.current_template()
t["qfmt"] = old["qfmt"] t["qfmt"] = old["qfmt"]
t["afmt"] = old["afmt"] t["afmt"] = old["afmt"]
self.mm.addTemplate(self.model, t) self.mm.addTemplate(self.model, t, save=False)
self.ord = len(self.cards) self.ord = len(self.templates) - 1
self.redraw() self.redraw_everything()
def onFlip(self): def onFlip(self):
old = self.card.template() old = self.current_template()
self._flipQA(old, old) self._flipQA(old, old)
self.redraw() self.redraw_everything()
def _flipQA(self, src, dst): def _flipQA(self, src, dst):
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"]) m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
@ -474,6 +486,7 @@ adjust the template manually to switch the question and answer."""
) )
) )
return return
self.changed = True
dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"] dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"]
dst["qfmt"] = m.group(2).strip() dst["qfmt"] = m.group(2).strip()
return True return True
@ -496,7 +509,7 @@ adjust the template manually to switch the question and answer."""
m.addSeparator() m.addSeparator()
t = self.card.template() t = self.current_template()
if t["did"]: if t["did"]:
s = _(" (on)") s = _(" (on)")
else: else:
@ -513,7 +526,7 @@ adjust the template manually to switch the question and answer."""
d = QDialog() d = QDialog()
f = aqt.forms.browserdisp.Ui_Dialog() f = aqt.forms.browserdisp.Ui_Dialog()
f.setupUi(d) f.setupUi(d)
t = self.card.template() t = self.current_template()
f.qfmt.setText(t.get("bqfmt", "")) f.qfmt.setText(t.get("bqfmt", ""))
f.afmt.setText(t.get("bafmt", "")) f.afmt.setText(t.get("bafmt", ""))
if t.get("bfont"): if t.get("bfont"):
@ -524,7 +537,8 @@ adjust the template manually to switch the question and answer."""
d.exec_() d.exec_()
def onBrowserDisplayOk(self, f): def onBrowserDisplayOk(self, f):
t = self.card.template() t = self.current_template()
self.changed = True
t["bqfmt"] = f.qfmt.text().strip() t["bqfmt"] = f.qfmt.text().strip()
t["bafmt"] = f.afmt.text().strip() t["bafmt"] = f.afmt.text().strip()
if f.overrideFont.isChecked(): if f.overrideFont.isChecked():
@ -538,7 +552,7 @@ adjust the template manually to switch the question and answer."""
def onTargetDeck(self): def onTargetDeck(self):
from aqt.tagedit import TagEdit from aqt.tagedit import TagEdit
t = self.card.template() t = self.current_template()
d = QDialog(self) d = QDialog(self)
d.setWindowTitle("Anki") d.setWindowTitle("Anki")
d.setMinimumWidth(400) d.setMinimumWidth(400)
@ -548,7 +562,7 @@ adjust the template manually to switch the question and answer."""
"""\ """\
Enter deck to place new %s cards in, or leave blank:""" Enter deck to place new %s cards in, or leave blank:"""
) )
% self.card.template()["name"] % self.current_template()["name"]
) )
lab.setWordWrap(True) lab.setWordWrap(True)
l.addWidget(lab) l.addWidget(lab)
@ -563,6 +577,7 @@ Enter deck to place new %s cards in, or leave blank:"""
l.addWidget(bb) l.addWidget(bb)
d.setLayout(l) d.setLayout(l)
d.exec_() d.exec_()
self.changed = True
if not te.text().strip(): if not te.text().strip():
t["did"] = None t["did"] = None
else: else:
@ -606,42 +621,45 @@ Enter deck to place new %s cards in, or leave blank:"""
field, field,
) )
widg.setPlainText(t) widg.setPlainText(t)
self.saveCard() self.changed = True
self.write_edits_to_template_and_redraw()
# Closing & Help # Closing & Help
###################################################################### ######################################################################
def accept(self) -> None: def accept(self) -> None:
try: def save():
self.mm.save(self.model) self.mm.save(self.model)
except TemplateError as e:
# fixme: i18n
showWarning("Unable to save changes: " + str(e))
return
self.mw.reset() def on_done(fut):
self.cleanup() try:
return QDialog.accept(self) fut.result()
except TemplateError as e:
showWarning("Unable to save changes: " + str(e))
return
self.mw.reset()
tooltip("Changes saved.", parent=self.mw)
self.cleanup()
return QDialog.accept(self)
self.mw.taskman.with_progress(save, on_done)
def reject(self) -> None: def reject(self) -> None:
# discard mutations - in the future we should load a fresh if self.changed:
# copy at the start instead if not askUser("Discard changes?"):
self.mm._remove_from_cache(self.model["id"]) return
self.cleanup() self.cleanup()
return QDialog.reject(self) return QDialog.reject(self)
def cleanup(self) -> None: def cleanup(self) -> None:
self.cancelPreviewTimer() self.cancelPreviewTimer()
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
# fixme
if self.addMode:
# remove the filler fields we added
for name in self.emptyFields:
self.note[name] = ""
self.mw.col.db.execute("delete from notes where id = ?", self.note.id)
saveGeom(self, "CardLayout") saveGeom(self, "CardLayout")
self.pform.frontWeb = None self.pform.frontWeb = None
self.pform.backWeb = None self.pform.backWeb = None
self.model = None
self.rendered_card = None
self.mw = None
def onHelp(self): def onHelp(self):
openHelp("templates") openHelp("templates")

View file

@ -353,7 +353,7 @@ class Editor:
else: else:
ord = 0 ord = 0
CardLayout( CardLayout(
self.mw, self.note, ord=ord, parent=self.parentWindow, addMode=self.addMode self.mw, self.note, ord=ord, parent=self.parentWindow,
) )
if isWin: if isWin:
self.parentWindow.activateWindow() self.parentWindow.activateWindow()

View file

@ -18,6 +18,7 @@ class FieldDialog(QDialog):
self.col = self.mw.col self.col = self.mw.col
self.mm = self.mw.col.models self.mm = self.mw.col.models
self.model = nt self.model = nt
self.mm._remove_from_cache(self.model["id"])
self.mw.checkpoint(_("Fields")) self.mw.checkpoint(_("Fields"))
self.form = aqt.forms.fields.Ui_Dialog() self.form = aqt.forms.fields.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
@ -163,7 +164,6 @@ class FieldDialog(QDialog):
fld["rtl"] = f.rtl.isChecked() fld["rtl"] = f.rtl.isChecked()
def reject(self): def reject(self):
self.mm._remove_from_cache(self.model["id"])
QDialog.reject(self) QDialog.reject(self)
def accept(self): def accept(self):

View file

@ -188,7 +188,7 @@ class Models(QDialog):
from aqt.clayout import CardLayout from aqt.clayout import CardLayout
n = self._tmpNote() n = self._tmpNote()
CardLayout(self.mw, n, ord=0, parent=self, addMode=True) CardLayout(self.mw, n, ord=0, parent=self)
# Cleanup # Cleanup
########################################################################## ##########################################################################

View file

@ -21,7 +21,10 @@ use crate::{
media::sync::MediaSyncProgress, media::sync::MediaSyncProgress,
media::MediaManager, media::MediaManager,
notes::{Note, NoteID}, notes::{Note, NoteID},
notetype::{all_stock_notetypes, NoteType, NoteTypeID, NoteTypeSchema11, RenderCardOutput}, notetype::{
all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeID, NoteTypeSchema11,
RenderCardOutput,
},
sched::cutoff::local_minutes_west_for_stamp, sched::cutoff::local_minutes_west_for_stamp,
sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}, sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span},
search::SortMode, search::SortMode,
@ -216,9 +219,6 @@ impl Backend {
Ok(match ival { Ok(match ival {
Value::SchedTimingToday(_) => OValue::SchedTimingToday(self.sched_timing_today()?), Value::SchedTimingToday(_) => OValue::SchedTimingToday(self.sched_timing_today()?),
Value::DeckTree(input) => OValue::DeckTree(self.deck_tree(input)?), Value::DeckTree(input) => OValue::DeckTree(self.deck_tree(input)?),
Value::RenderExistingCard(input) => {
OValue::RenderExistingCard(self.render_existing_card(input)?)
}
Value::LocalMinutesWest(stamp) => { Value::LocalMinutesWest(stamp) => {
OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp)) OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp))
} }
@ -375,6 +375,12 @@ impl Backend {
self.set_preferences(prefs)?; self.set_preferences(prefs)?;
pb::Empty {} pb::Empty {}
}), }),
Value::RenderExistingCard(input) => {
OValue::RenderExistingCard(self.render_existing_card(input)?)
}
Value::RenderUncommittedCard(input) => {
OValue::RenderUncommittedCard(self.render_uncommitted_card(input)?)
}
}) })
} }
@ -456,6 +462,23 @@ impl Backend {
}) })
} }
fn render_uncommitted_card(
&self,
input: pb::RenderUncommittedCardIn,
) -> Result<pb::RenderCardOut> {
let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?;
let template = schema11.into();
let note = input
.note
.ok_or_else(|| AnkiError::invalid_input("missing note"))?
.into();
let ord = input.card_ord as u16;
self.with_col(|col| {
col.render_uncommitted_card(&note, &template, ord)
.map(Into::into)
})
}
fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> pb::ExtractAvTagsOut { fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> pb::ExtractAvTagsOut {
let (text, tags) = extract_av_tags(&input.text, input.question_side); let (text, tags) = extract_av_tags(&input.text, input.question_side);
let pt_tags = tags let pt_tags = tags

View file

@ -84,7 +84,7 @@ impl NoteType {
names.insert(name); names.insert(name);
break; break;
} }
t.name.push('_'); t.name.push('+');
} }
} }
names.clear(); names.clear();
@ -95,7 +95,7 @@ impl NoteType {
names.insert(name); names.insert(name);
break; break;
} }
t.name.push('_'); t.name.push('+');
} }
} }
} }