def onDelete(self) -> None: if len(self.models) < 2: showInfo(tr.notetypes_please_add_another_note_type_first(), parent=self) return idx = self.form.modelsList.currentRow() if self.models[idx].use_count: msg = tr.notetypes_delete_this_note_type_and_all() else: msg = tr.notetypes_delete_this_unused_note_type() if not askUser(msg, parent=self): return tracker = ChangeTracker(self.mw) if not tracker.mark_schema(): return nt = self.current_notetype() remove_notetype(parent=self, notetype_id=nt["id"]).success( lambda _: self.refresh_list()).run_in_background()
class CardLayout(QDialog): def __init__( self, mw: AnkiQt, note: Note, ord: int = 0, parent: Optional[QWidget] = None, fill_empty: bool = False, ) -> None: QDialog.__init__(self, parent or mw, Qt.Window) mw.setupDialogGC(self) self.mw = aqt.mw self.note = note self.ord = ord self.col = self.mw.col.weakref() self.mm = self.mw.col.models self.model = note.model() self.templates = self.model["tmpls"] self.fill_empty_action_toggled = fill_empty self.night_mode_is_enabled = self.mw.pm.night_mode() self.mobile_emulation_enabled = False self.have_autoplayed = False self.mm._remove_from_cache(self.model["id"]) self.mw.checkpoint(tr(TR.CARD_TEMPLATES_CARD_TYPES)) self.change_tracker = ChangeTracker(self.mw) self.setupTopArea() self.setupMainArea() self.setupButtons() self.setupShortcuts() self.setWindowTitle( without_unicode_isolation( tr(TR.CARD_TEMPLATES_CARD_TYPES_FOR, val=self.model["name"]))) disable_help_button(self) v1 = QVBoxLayout() v1.addWidget(self.topArea) v1.addWidget(self.mainArea) v1.addLayout(self.buttons) v1.setContentsMargins(12, 12, 12, 12) self.setLayout(v1) gui_hooks.card_layout_will_show(self) self.redraw_everything() restoreGeom(self, "CardLayout") restoreSplitter(self.mainArea, "CardLayoutMainArea") self.setWindowModality(Qt.ApplicationModal) self.show() # take the focus away from the first input area when starting up, # as users tend to accidentally type into the template self.setFocus() def redraw_everything(self) -> None: self.ignore_change_signals = True self.updateTopArea() self.ignore_change_signals = False self.update_current_ordinal_and_redraw(self.ord) def update_current_ordinal_and_redraw(self, idx: int) -> None: if self.ignore_change_signals: return self.ord = idx self.have_autoplayed = False self.fill_fields_from_template() self.renderPreview() def _isCloze(self) -> bool: return self.model["type"] == MODEL_CLOZE # Top area ########################################################################## def setupTopArea(self) -> None: self.topArea = QWidget() self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self.topAreaForm = aqt.forms.clayout_top.Ui_Form() self.topAreaForm.setupUi(self.topArea) self.topAreaForm.templateOptions.setText( f"{tr(TR.ACTIONS_OPTIONS)} {downArrow()}") qconnect(self.topAreaForm.templateOptions.clicked, self.onMore) qconnect( self.topAreaForm.templatesBox.currentIndexChanged, self.update_current_ordinal_and_redraw, ) self.topAreaForm.card_type_label.setText( tr(TR.CARD_TEMPLATES_CARD_TYPE)) def updateTopArea(self) -> None: self.updateCardNames() def updateCardNames(self) -> None: self.ignore_change_signals = True combo = self.topAreaForm.templatesBox combo.clear() combo.addItems( self._summarizedName(idx, tmpl) for (idx, tmpl) in enumerate(self.templates)) combo.setCurrentIndex(self.ord) combo.setEnabled(not self._isCloze()) self.ignore_change_signals = False def _summarizedName(self, idx: int, tmpl: Dict) -> str: return "{}: {}: {} -> {}".format( idx + 1, tmpl["name"], self._fieldsOnTemplate(tmpl["qfmt"]), self._fieldsOnTemplate(tmpl["afmt"]), ) def _fieldsOnTemplate(self, fmt: str) -> str: matches = re.findall("{{[^#/}]+?}}", fmt) chars_allowed = 30 field_names: List[str] = [] for m in matches: # strip off mustache m = re.sub(r"[{}]", "", m) # strip off modifiers m = m.split(":")[-1] # don't show 'FrontSide' if m == "FrontSide": continue field_names.append(m) chars_allowed -= len(m) if chars_allowed <= 0: break s = "+".join(field_names) if chars_allowed <= 0: s += "+..." return s def setupShortcuts(self) -> None: self.tform.front_button.setToolTip(shortcut("Ctrl+1")) self.tform.back_button.setToolTip(shortcut("Ctrl+2")) self.tform.style_button.setToolTip(shortcut("Ctrl+3")) QShortcut( # type: ignore QKeySequence("Ctrl+1"), self, activated=self.tform.front_button.click, ) QShortcut( # type: ignore QKeySequence("Ctrl+2"), self, activated=self.tform.back_button.click, ) QShortcut( # type: ignore QKeySequence("Ctrl+3"), self, activated=self.tform.style_button.click, ) # Main area setup ########################################################################## def setupMainArea(self) -> None: split = self.mainArea = QSplitter() split.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) split.setOrientation(Qt.Horizontal) left = QWidget() tform = self.tform = aqt.forms.template.Ui_Form() tform.setupUi(left) split.addWidget(left) split.setCollapsible(0, False) right = QWidget() self.pform = aqt.forms.preview.Ui_Form() pform = self.pform pform.setupUi(right) pform.preview_front.setText(tr(TR.CARD_TEMPLATES_FRONT_PREVIEW)) pform.preview_back.setText(tr(TR.CARD_TEMPLATES_BACK_PREVIEW)) pform.preview_box.setTitle(tr(TR.CARD_TEMPLATES_PREVIEW_BOX)) self.setup_edit_area() self.setup_preview() split.addWidget(right) split.setCollapsible(1, False) def setup_edit_area(self) -> None: tform = self.tform tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE)) tform.back_button.setText(tr(TR.CARD_TEMPLATES_BACK_TEMPLATE)) tform.style_button.setText(tr(TR.CARD_TEMPLATES_TEMPLATE_STYLING)) tform.groupBox.setTitle(tr(TR.CARD_TEMPLATES_TEMPLATE_BOX)) cnt = self.mw.col.models.useCount(self.model) self.tform.changes_affect_label.setText( self.col.tr(TR.CARD_TEMPLATES_CHANGES_WILL_AFFECT_NOTES, count=cnt)) qconnect(tform.edit_area.textChanged, self.write_edits_to_template_and_redraw) qconnect(tform.front_button.clicked, self.on_editor_toggled) qconnect(tform.back_button.clicked, self.on_editor_toggled) qconnect(tform.style_button.clicked, self.on_editor_toggled) self.current_editor_index = 0 self.tform.edit_area.setAcceptRichText(False) self.tform.edit_area.setFont(QFont("Courier")) if qtminor < 10: self.tform.edit_area.setTabStopWidth(30) else: tab_width = self.fontMetrics().width(" " * 4) self.tform.edit_area.setTabStopDistance(tab_width) widg = tform.search_edit widg.setPlaceholderText("Search") qconnect(widg.textChanged, self.on_search_changed) qconnect(widg.returnPressed, self.on_search_next) def setup_cloze_number_box(self) -> None: names = (tr(TR.CARD_TEMPLATES_CLOZE, val=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 on_change_cloze(self, idx: int) -> None: self.ord = self.cloze_numbers[idx] - 1 self.have_autoplayed = False self._renderPreview() def on_editor_toggled(self) -> None: if self.tform.front_button.isChecked(): self.current_editor_index = 0 self.pform.preview_front.setChecked(True) self.on_preview_toggled() self.add_field_button.setHidden(False) elif self.tform.back_button.isChecked(): self.current_editor_index = 1 self.pform.preview_back.setChecked(True) self.on_preview_toggled() self.add_field_button.setHidden(False) else: self.current_editor_index = 2 self.add_field_button.setHidden(True) self.fill_fields_from_template() def on_search_changed(self, text: str) -> None: editor = self.tform.edit_area if not editor.find(text): # try again from top cursor = editor.textCursor() cursor.movePosition(QTextCursor.Start) editor.setTextCursor(cursor) if not editor.find(text): tooltip("No matches found.") def on_search_next(self) -> None: text = self.tform.search_edit.text() self.on_search_changed(text) def setup_preview(self) -> None: pform = self.pform self.preview_web = AnkiWebView(title="card layout") pform.verticalLayout.addWidget(self.preview_web) pform.verticalLayout.setStretch(1, 99) pform.preview_front.isChecked() qconnect(pform.preview_front.clicked, self.on_preview_toggled) qconnect(pform.preview_back.clicked, self.on_preview_toggled) pform.preview_settings.setText( f"{tr(TR.CARD_TEMPLATES_PREVIEW_SETTINGS)} {downArrow()}") qconnect(pform.preview_settings.clicked, self.on_preview_settings) jsinc = [ "js/vendor/jquery.min.js", "js/vendor/css_browser_selector.min.js", "js/mathjax.js", "js/vendor/mathjax/tex-chtml.js", "js/reviewer.js", ] self.preview_web.stdHtml( self.mw.reviewer.revHtml(), css=["css/reviewer.css"], js=jsinc, context=self, ) self.preview_web.set_bridge_command(self._on_bridge_cmd, self) if self._isCloze(): nums = list(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) def on_fill_empty_action_toggled(self) -> None: self.fill_empty_action_toggled = not self.fill_empty_action_toggled self.on_preview_toggled() def on_night_mode_action_toggled(self) -> None: self.night_mode_is_enabled = not self.night_mode_is_enabled self.on_preview_toggled() def on_mobile_class_action_toggled(self) -> None: self.mobile_emulation_enabled = not self.mobile_emulation_enabled self.on_preview_toggled() def on_preview_settings(self) -> None: m = QMenu(self) a = m.addAction(tr(TR.CARD_TEMPLATES_FILL_EMPTY)) a.setCheckable(True) a.setChecked(self.fill_empty_action_toggled) qconnect(a.triggered, self.on_fill_empty_action_toggled) if not self.note_has_empty_field(): a.setVisible(False) a = m.addAction(tr(TR.CARD_TEMPLATES_NIGHT_MODE)) a.setCheckable(True) a.setChecked(self.night_mode_is_enabled) qconnect(a.triggered, self.on_night_mode_action_toggled) a = m.addAction(tr(TR.CARD_TEMPLATES_ADD_MOBILE_CLASS)) a.setCheckable(True) a.setChecked(self.mobile_emulation_enabled) qconnect(a.toggled, self.on_mobile_class_action_toggled) m.exec_(self.pform.preview_settings.mapToGlobal(QPoint(0, 0))) def on_preview_toggled(self) -> None: self.have_autoplayed = False self._renderPreview() def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): play_clicked_audio(cmd, self.rendered_card) def note_has_empty_field(self) -> bool: for field in self.note.fields: if not field.strip(): # ignores HTML, but this should suffice return True return False # Buttons ########################################################################## def setupButtons(self) -> None: l = self.buttons = QHBoxLayout() help = QPushButton(tr(TR.ACTIONS_HELP)) help.setAutoDefault(False) l.addWidget(help) qconnect(help.clicked, self.onHelp) l.addStretch() self.add_field_button = QPushButton(tr(TR.FIELDS_ADD_FIELD)) self.add_field_button.setAutoDefault(False) l.addWidget(self.add_field_button) qconnect(self.add_field_button.clicked, self.onAddField) if not self._isCloze(): flip = QPushButton(tr(TR.CARD_TEMPLATES_FLIP)) flip.setAutoDefault(False) l.addWidget(flip) qconnect(flip.clicked, self.onFlip) l.addStretch() save = QPushButton(tr(TR.ACTIONS_SAVE)) save.setAutoDefault(False) save.setShortcut(QKeySequence("Ctrl+Return")) l.addWidget(save) qconnect(save.clicked, self.accept) close = QPushButton(tr(TR.ACTIONS_CANCEL)) close.setAutoDefault(False) l.addWidget(close) qconnect(close.clicked, self.reject) # Reading/writing question/answer/css ########################################################################## def current_template(self) -> Dict: if self._isCloze(): return self.templates[0] return self.templates[self.ord] def fill_fields_from_template(self) -> None: t = self.current_template() self.ignore_change_signals = True if self.current_editor_index == 0: text = t["qfmt"] elif self.current_editor_index == 1: text = t["afmt"] else: text = self.model["css"] self.tform.edit_area.setPlainText(text) self.ignore_change_signals = False def write_edits_to_template_and_redraw(self) -> None: if self.ignore_change_signals: return self.change_tracker.mark_basic() text = self.tform.edit_area.toPlainText() if self.current_editor_index == 0: self.current_template()["qfmt"] = text elif self.current_editor_index == 1: self.current_template()["afmt"] = text else: self.model["css"] = text self.renderPreview() # Preview ########################################################################## _previewTimer: Optional[QTimer] = None def renderPreview(self) -> None: # schedule a preview when timing stops self.cancelPreviewTimer() self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False) def cancelPreviewTimer(self) -> None: if self._previewTimer: self._previewTimer.stop() self._previewTimer = None def _renderPreview(self) -> None: self.cancelPreviewTimer() c = self.rendered_card = self.note.ephemeral_card( self.ord, custom_note_type=self.model, custom_template=self.current_template(), fill_empty=self.fill_empty_action_toggled, ) ti = self.maybeTextInput bodyclass = theme_manager.body_classes_for_card_ord( c.ord, self.night_mode_is_enabled) 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 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 # use _showAnswer to avoid the longer delay self.preview_web.eval( f"_showAnswer({json.dumps(text)},'{bodyclass}');") self.preview_web.eval( f"_emulateMobile({json.dumps(self.mobile_emulation_enabled)});") if not self.have_autoplayed: self.have_autoplayed = True if c.autoplay(): AnkiWebView.setPlaybackRequiresGesture(False) if self.pform.preview_front.isChecked(): audio = c.question_av_tags() else: audio = c.answer_av_tags() av_player.play_tags(audio) else: AnkiWebView.setPlaybackRequiresGesture(True) av_player.clear_queue_and_maybe_interrupt() self.updateCardNames() def maybeTextInput(self, txt: str, type: str = "q") -> str: if "[[type:" not in txt: return txt origLen = len(txt) txt = txt.replace("<hr id=answer>", "") hadHR = origLen != len(txt) def answerRepl(match: Match) -> str: res = self.mw.reviewer.correct("exomple", "an example") if hadHR: res = f"<hr id=answer>{res}" return res repl: Union[str, Callable] if type == "q": repl = "<input id='typeans' type=text value='exomple' readonly='readonly'>" repl = f"<center>{repl}</center>" else: repl = answerRepl return re.sub(r"\[\[type:.+?\]\]", repl, txt) # Card operations ###################################################################### def onRemove(self) -> None: if len(self.templates) < 2: showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS)) return def get_count() -> int: return self.mm.template_use_count(self.model["id"], self.ord) def on_done(fut: Future) -> None: card_cnt = fut.result() template = self.current_template() cards = tr(TR.CARD_TEMPLATES_CARD_COUNT, count=card_cnt) msg = tr( TR.CARD_TEMPLATES_DELETE_THE_AS_CARD_TYPE_AND, template=template["name"], cards=cards, ) if not askUser(msg): return if not self.change_tracker.mark_schema(): return self.onRemoveInner(template) self.mw.taskman.with_progress(get_count, on_done) def onRemoveInner(self, template: Dict) -> None: self.mm.remove_template(self.model, template) # ensure current ordinal is within bounds idx = self.ord if idx >= len(self.templates): self.ord = len(self.templates) - 1 self.redraw_everything() def onRename(self) -> None: template = self.current_template() name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=template["name"]).replace('"', "") if not name.strip(): return template["name"] = name self.redraw_everything() def onReorder(self) -> None: n = len(self.templates) template = self.current_template() current_pos = self.templates.index(template) + 1 pos_txt = getOnlyText( tr(TR.CARD_TEMPLATES_ENTER_NEW_CARD_POSITION_1, val=n), default=str(current_pos), ) if not pos_txt: return try: pos = int(pos_txt) except ValueError: return if pos < 1 or pos > n: return if pos == current_pos: return new_idx = pos - 1 if not self.change_tracker.mark_schema(): return self.mm.reposition_template(self.model, template, new_idx) self.ord = new_idx self.redraw_everything() def _newCardName(self) -> str: n = len(self.templates) + 1 while 1: name = without_unicode_isolation(tr(TR.CARD_TEMPLATES_CARD, val=n)) if name not in [t["name"] for t in self.templates]: break n += 1 return name def onAddCard(self) -> None: cnt = self.mw.col.models.useCount(self.model) txt = tr(TR.CARD_TEMPLATES_THIS_WILL_CREATE_CARD_PROCEED, count=cnt) if not askUser(txt): return if not self.change_tracker.mark_schema(): return name = self._newCardName() t = self.mm.newTemplate(name) old = self.current_template() t["qfmt"] = old["qfmt"] t["afmt"] = old["afmt"] self.mm.add_template(self.model, t) self.ord = len(self.templates) - 1 self.redraw_everything() def onFlip(self) -> None: old = self.current_template() self._flipQA(old, old) self.redraw_everything() def _flipQA(self, src: Dict, dst: Dict) -> None: m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"]) if not m: showInfo(tr(TR.CARD_TEMPLATES_ANKI_COULDNT_FIND_THE_LINE_BETWEEN)) return self.change_tracker.mark_basic() dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"] dst["qfmt"] = m.group(2).strip() def onMore(self) -> None: m = QMenu(self) if not self._isCloze(): a = m.addAction(tr(TR.CARD_TEMPLATES_ADD_CARD_TYPE)) qconnect(a.triggered, self.onAddCard) a = m.addAction(tr(TR.CARD_TEMPLATES_REMOVE_CARD_TYPE)) qconnect(a.triggered, self.onRemove) a = m.addAction(tr(TR.CARD_TEMPLATES_RENAME_CARD_TYPE)) qconnect(a.triggered, self.onRename) a = m.addAction(tr(TR.CARD_TEMPLATES_REPOSITION_CARD_TYPE)) qconnect(a.triggered, self.onReorder) m.addSeparator() t = self.current_template() if t["did"]: s = tr(TR.CARD_TEMPLATES_ON) else: s = tr(TR.CARD_TEMPLATES_OFF) a = m.addAction(tr(TR.CARD_TEMPLATES_DECK_OVERRIDE) + s) qconnect(a.triggered, self.onTargetDeck) a = m.addAction(tr(TR.CARD_TEMPLATES_BROWSER_APPEARANCE)) qconnect(a.triggered, self.onBrowserDisplay) m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0))) def onBrowserDisplay(self) -> None: d = QDialog() disable_help_button(d) f = aqt.forms.browserdisp.Ui_Dialog() f.setupUi(d) t = self.current_template() f.qfmt.setText(t.get("bqfmt", "")) f.afmt.setText(t.get("bafmt", "")) if t.get("bfont"): f.overrideFont.setChecked(True) f.font.setCurrentFont(QFont(t.get("bfont", "Arial"))) f.fontSize.setValue(t.get("bsize", 12)) qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f)) d.exec_() def onBrowserDisplayOk(self, f: Ui_Dialog) -> None: t = self.current_template() self.change_tracker.mark_basic() t["bqfmt"] = f.qfmt.text().strip() t["bafmt"] = f.afmt.text().strip() if f.overrideFont.isChecked(): t["bfont"] = f.font.currentFont().family() t["bsize"] = f.fontSize.value() else: for key in ("bfont", "bsize"): if key in t: del t[key] def onTargetDeck(self) -> None: from aqt.tagedit import TagEdit t = self.current_template() d = QDialog(self) d.setWindowTitle("Anki") disable_help_button(d) d.setMinimumWidth(400) l = QVBoxLayout() lab = QLabel( tr(TR.CARD_TEMPLATES_ENTER_DECK_TO_PLACE_NEW, val="%s") % self.current_template()["name"]) lab.setWordWrap(True) l.addWidget(lab) te = TagEdit(d, type=1) te.setCol(self.col) l.addWidget(te) if t["did"]: te.setText(self.col.decks.get(t["did"])["name"]) te.selectAll() bb = QDialogButtonBox(QDialogButtonBox.Close) qconnect(bb.rejected, d.close) l.addWidget(bb) d.setLayout(l) d.exec_() self.change_tracker.mark_basic() if not te.text().strip(): t["did"] = None else: t["did"] = self.col.decks.id(te.text()) def onAddField(self) -> None: diag = QDialog(self) form = aqt.forms.addfield.Ui_Dialog() form.setupUi(diag) disable_help_button(diag) fields = [f["name"] for f in self.model["flds"]] form.fields.addItems(fields) form.fields.setCurrentRow(0) form.font.setCurrentFont(QFont("Arial")) form.size.setValue(20) if not diag.exec_(): return row = form.fields.currentIndex().row() if row >= 0: self._addField( fields[row], form.font.currentFont().family(), form.size.value(), ) def _addField(self, field: str, font: str, size: int) -> None: text = self.tform.edit_area.toPlainText() text += "\n<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\n" % ( font, size, field, ) self.tform.edit_area.setPlainText(text) self.change_tracker.mark_basic() self.write_edits_to_template_and_redraw() # Closing & Help ###################################################################### def accept(self) -> None: def save() -> None: self.mm.save(self.model) def on_done(fut: Future) -> None: try: fut.result() except TemplateError as e: showWarning(str(e)) return self.mw.reset() tooltip(tr(TR.CARD_TEMPLATES_CHANGES_SAVED), parent=self.parent()) self.cleanup() gui_hooks.sidebar_should_refresh_notetypes() return QDialog.accept(self) self.mw.taskman.with_progress(save, on_done) def reject(self) -> None: if self.change_tracker.changed(): if not askUser(tr(TR.CARD_TEMPLATES_DISCARD_CHANGES)): return self.cleanup() return QDialog.reject(self) def cleanup(self) -> None: self.cancelPreviewTimer() av_player.stop_and_clear_queue() saveGeom(self, "CardLayout") saveSplitter(self.mainArea, "CardLayoutMainArea") self.preview_web = None self.model = None self.rendered_card = None self.mw = None def onHelp(self) -> None: openHelp(HelpPage.TEMPLATES)
class FieldDialog(QDialog): def __init__(self, mw: AnkiQt, nt: NoteType, parent=None): QDialog.__init__(self, parent or mw) self.mw = mw self.col = self.mw.col self.mm = self.mw.col.models self.model = nt self.mm._remove_from_cache(self.model["id"]) self.mw.checkpoint(tr(TR.EDITING_FIELDS)) self.change_tracker = ChangeTracker(self.mw) self.form = aqt.forms.fields.Ui_Dialog() self.form.setupUi(self) self.setWindowTitle( without_unicode_isolation( tr(TR.FIELDS_FIELDS_FOR, val=self.model["name"]))) disable_help_button(self) self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False) self.form.buttonBox.button( QDialogButtonBox.Cancel).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) self.currentIdx = None self.oldSortField = self.model["sortf"] self.fillFields() self.setupSignals() self.form.fieldList.setDragDropMode(QAbstractItemView.InternalMove) self.form.fieldList.dropEvent = self.onDrop self.form.fieldList.setCurrentRow(0) self.exec_() ########################################################################## def fillFields(self): self.currentIdx = None self.form.fieldList.clear() for c, f in enumerate(self.model["flds"]): self.form.fieldList.addItem("{}: {}".format(c + 1, f["name"])) def setupSignals(self): f = self.form qconnect(f.fieldList.currentRowChanged, self.onRowChange) qconnect(f.fieldAdd.clicked, self.onAdd) qconnect(f.fieldDelete.clicked, self.onDelete) qconnect(f.fieldRename.clicked, self.onRename) qconnect(f.fieldPosition.clicked, self.onPosition) qconnect(f.sortField.clicked, self.onSortField) qconnect(f.buttonBox.helpRequested, self.onHelp) def onDrop(self, ev): fieldList = self.form.fieldList indicatorPos = fieldList.dropIndicatorPosition() dropPos = fieldList.indexAt(ev.pos()).row() idx = self.currentIdx if dropPos == idx: return if indicatorPos == QAbstractItemView.OnViewport: # to bottom. movePos = fieldList.count() - 1 elif indicatorPos == QAbstractItemView.AboveItem: movePos = dropPos elif indicatorPos == QAbstractItemView.BelowItem: movePos = dropPos + 1 # the item in idx is removed thus subtract 1. if idx < dropPos: movePos -= 1 self.moveField(movePos + 1) # convert to 1 based. def onRowChange(self, idx): if idx == -1: return self.saveField() self.loadField(idx) def _uniqueName(self, prompt, ignoreOrd=None, old=""): txt = getOnlyText(prompt, default=old).replace('"', "").strip() if not txt: return if txt[0] in "#^/": showWarning(tr(TR.FIELDS_NAME_FIRST_LETTER_NOT_VALID)) return for letter in """:{"}""": if letter in txt: showWarning(tr(TR.FIELDS_NAME_INVALID_LETTER)) return for f in self.model["flds"]: if ignoreOrd is not None and f["ord"] == ignoreOrd: continue if f["name"] == txt: showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED)) return return txt def onRename(self): idx = self.currentIdx f = self.model["flds"][idx] name = self._uniqueName(tr(TR.ACTIONS_NEW_NAME), self.currentIdx, f["name"]) if not name: return old_name = f["name"] self.change_tracker.mark_basic() self.mm.rename_field(self.model, f, name) gui_hooks.fields_did_rename_field(self, f, old_name) self.saveField() self.fillFields() self.form.fieldList.setCurrentRow(idx) def onAdd(self): name = self._uniqueName(tr(TR.FIELDS_FIELD_NAME)) if not name: return if not self.change_tracker.mark_schema(): return self.saveField() f = self.mm.newField(name) self.mm.add_field(self.model, f) self.fillFields() self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1) def onDelete(self): if len(self.model["flds"]) < 2: return showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD)) count = self.mm.useCount(self.model) c = tr(TR.BROWSING_NOTE_COUNT, count=count) if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val=c)): return if not self.change_tracker.mark_schema(): return f = self.model["flds"][self.form.fieldList.currentRow()] self.mm.remove_field(self.model, f) gui_hooks.fields_did_delete_field(self, f) self.fillFields() self.form.fieldList.setCurrentRow(0) def onPosition(self, delta=-1): idx = self.currentIdx l = len(self.model["flds"]) txt = getOnlyText(tr(TR.FIELDS_NEW_POSITION_1, val=l), default=str(idx + 1)) if not txt: return try: pos = int(txt) except ValueError: return if not 0 < pos <= l: return self.moveField(pos) def onSortField(self): if not self.change_tracker.mark_schema(): return False # don't allow user to disable; it makes no sense self.form.sortField.setChecked(True) self.mm.set_sort_index(self.model, self.form.fieldList.currentRow()) def moveField(self, pos): if not self.change_tracker.mark_schema(): return False self.saveField() f = self.model["flds"][self.currentIdx] self.mm.reposition_field(self.model, f, pos - 1) self.fillFields() self.form.fieldList.setCurrentRow(pos - 1) def loadField(self, idx): self.currentIdx = idx fld = self.model["flds"][idx] f = self.form f.fontFamily.setCurrentFont(QFont(fld["font"])) f.fontSize.setValue(fld["size"]) f.sticky.setChecked(fld["sticky"]) f.sortField.setChecked(self.model["sortf"] == fld["ord"]) f.rtl.setChecked(fld["rtl"]) def saveField(self): # not initialized yet? if self.currentIdx is None: return idx = self.currentIdx fld = self.model["flds"][idx] f = self.form font = f.fontFamily.currentFont().family() if fld["font"] != font: fld["font"] = font self.change_tracker.mark_basic() size = f.fontSize.value() if fld["size"] != size: fld["size"] = size self.change_tracker.mark_basic() sticky = f.sticky.isChecked() if fld["sticky"] != sticky: fld["sticky"] = sticky self.change_tracker.mark_basic() rtl = f.rtl.isChecked() if fld["rtl"] != rtl: fld["rtl"] = rtl self.change_tracker.mark_basic() def reject(self): if self.change_tracker.changed(): if not askUser("Discard changes?"): return QDialog.reject(self) def accept(self): self.saveField() def save(): self.mm.save(self.model) def on_done(fut): try: fut.result() except TemplateError as e: # fixme: i18n showWarning("Unable to save changes: " + str(e)) return self.mw.reset() tooltip("Changes saved.", parent=self.mw) QDialog.accept(self) self.mw.taskman.with_progress(save, on_done, self) def onHelp(self): openHelp(HelpPage.CUSTOMIZING_FIELDS)
class FieldDialog(QDialog): def __init__(self, mw: AnkiQt, nt: NoteType, parent=None): QDialog.__init__(self, parent or mw) self.mw = mw self.col = self.mw.col self.mm = self.mw.col.models self.model = nt self.mm._remove_from_cache(self.model["id"]) self.mw.checkpoint(_("Fields")) self.change_tracker = ChangeTracker(self.mw) self.form = aqt.forms.fields.Ui_Dialog() self.form.setupUi(self) self.setWindowTitle(_("Fields for %s") % self.model["name"]) self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False) self.form.buttonBox.button( QDialogButtonBox.Cancel).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) self.currentIdx = None self.oldSortField = self.model["sortf"] self.fillFields() self.setupSignals() self.form.fieldList.setDragDropMode(QAbstractItemView.InternalMove) self.form.fieldList.dropEvent = self.onDrop self.form.fieldList.setCurrentRow(0) self.exec_() ########################################################################## def fillFields(self): self.currentIdx = None self.form.fieldList.clear() for c, f in enumerate(self.model["flds"]): self.form.fieldList.addItem("{}: {}".format(c + 1, f["name"])) def setupSignals(self): f = self.form qconnect(f.fieldList.currentRowChanged, self.onRowChange) qconnect(f.fieldAdd.clicked, self.onAdd) qconnect(f.fieldDelete.clicked, self.onDelete) qconnect(f.fieldRename.clicked, self.onRename) qconnect(f.fieldPosition.clicked, self.onPosition) qconnect(f.sortField.clicked, self.onSortField) qconnect(f.buttonBox.helpRequested, self.onHelp) def onDrop(self, ev): fieldList = self.form.fieldList indicatorPos = fieldList.dropIndicatorPosition() dropPos = fieldList.indexAt(ev.pos()).row() idx = self.currentIdx if dropPos == idx: return if indicatorPos == QAbstractItemView.OnViewport: # to bottom. movePos = fieldList.count() - 1 elif indicatorPos == QAbstractItemView.AboveItem: movePos = dropPos elif indicatorPos == QAbstractItemView.BelowItem: movePos = dropPos + 1 # the item in idx is removed thus subtract 1. if idx < dropPos: movePos -= 1 self.moveField(movePos + 1) # convert to 1 based. def onRowChange(self, idx): if idx == -1: return self.saveField() self.loadField(idx) def _uniqueName(self, prompt, ignoreOrd=None, old=""): txt = getOnlyText(prompt, default=old) if not txt: return for f in self.model["flds"]: if ignoreOrd is not None and f["ord"] == ignoreOrd: continue if f["name"] == txt: showWarning(_("That field name is already used.")) return return txt def onRename(self): idx = self.currentIdx f = self.model["flds"][idx] name = self._uniqueName(_("New name:"), self.currentIdx, f["name"]) if not name: return self.change_tracker.mark_basic() self.mm.rename_field(self.model, f, name) self.saveField() self.fillFields() self.form.fieldList.setCurrentRow(idx) def onAdd(self): name = self._uniqueName(_("Field name:")) if not name: return if not self.change_tracker.mark_schema(): return self.saveField() f = self.mm.newField(name) self.mm.add_field(self.model, f) self.fillFields() self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1) def onDelete(self): if len(self.model["flds"]) < 2: return showWarning(_("Notes require at least one field.")) count = self.mm.useCount(self.model) c = ngettext("%d note", "%d notes", count) % count if not askUser(_("Delete field from %s?") % c): return if not self.change_tracker.mark_schema(): return f = self.model["flds"][self.form.fieldList.currentRow()] self.mm.remove_field(self.model, f) self.fillFields() self.form.fieldList.setCurrentRow(0) def onPosition(self, delta=-1): idx = self.currentIdx l = len(self.model["flds"]) txt = getOnlyText(_("New position (1...%d):") % l, default=str(idx + 1)) if not txt: return try: pos = int(txt) except ValueError: return if not 0 < pos <= l: return self.moveField(pos) def onSortField(self): if not self.change_tracker.mark_schema(): return False # don't allow user to disable; it makes no sense self.form.sortField.setChecked(True) self.mm.set_sort_index(self.model, self.form.fieldList.currentRow()) def moveField(self, pos): if not self.change_tracker.mark_schema(): return False self.saveField() f = self.model["flds"][self.currentIdx] self.mm.reposition_field(self.model, f, pos - 1) self.fillFields() self.form.fieldList.setCurrentRow(pos - 1) def loadField(self, idx): self.currentIdx = idx fld = self.model["flds"][idx] f = self.form f.fontFamily.setCurrentFont(QFont(fld["font"])) f.fontSize.setValue(fld["size"]) f.sticky.setChecked(fld["sticky"]) f.sortField.setChecked(self.model["sortf"] == fld["ord"]) f.rtl.setChecked(fld["rtl"]) def saveField(self): # not initialized yet? if self.currentIdx is None: return idx = self.currentIdx fld = self.model["flds"][idx] f = self.form font = f.fontFamily.currentFont().family() if fld["font"] != font: fld["font"] = font self.change_tracker.mark_basic() size = f.fontSize.value() if fld["size"] != size: fld["size"] = size self.change_tracker.mark_basic() sticky = f.sticky.isChecked() if fld["sticky"] != sticky: fld["sticky"] = sticky self.change_tracker.mark_basic() rtl = f.rtl.isChecked() if fld["rtl"] != rtl: fld["rtl"] = rtl self.change_tracker.mark_basic() def reject(self): if self.change_tracker.changed(): if not askUser("Discard changes?"): return QDialog.reject(self) def accept(self): self.saveField() def save(): self.mm.save(self.model) def on_done(fut): try: fut.result() except TemplateError as e: # fixme: i18n showWarning("Unable to save changes: " + str(e)) return self.mw.reset() tooltip("Changes saved.", parent=self.mw) QDialog.accept(self) self.mw.taskman.with_progress(save, on_done, self) def onHelp(self): openHelp("fields")
class FieldDialog(QDialog): def __init__(self, mw: AnkiQt, nt: NotetypeDict, parent: Optional[QWidget] = None) -> None: QDialog.__init__(self, parent or mw) self.mw = mw self.col = self.mw.col self.mm = self.mw.col.models self.model = nt self.mm._remove_from_cache(self.model["id"]) self.mw.checkpoint(tr.editing_fields()) self.change_tracker = ChangeTracker(self.mw) self.form = aqt.forms.fields.Ui_Dialog() self.form.setupUi(self) self.setWindowTitle( without_unicode_isolation( tr.fields_fields_for(val=self.model["name"]))) disable_help_button(self) self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False) self.form.buttonBox.button( QDialogButtonBox.Cancel).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) self.currentIdx: Optional[int] = None self.fillFields() self.setupSignals() self.form.fieldList.setDragDropMode(QAbstractItemView.InternalMove) self.form.fieldList.dropEvent = self.onDrop # type: ignore[assignment] self.form.fieldList.setCurrentRow(0) self.exec_() ########################################################################## def fillFields(self) -> None: self.currentIdx = None self.form.fieldList.clear() for c, f in enumerate(self.model["flds"]): self.form.fieldList.addItem(f"{c + 1}: {f['name']}") def setupSignals(self) -> None: f = self.form qconnect(f.fieldList.currentRowChanged, self.onRowChange) qconnect(f.fieldAdd.clicked, self.onAdd) qconnect(f.fieldDelete.clicked, self.onDelete) qconnect(f.fieldRename.clicked, self.onRename) qconnect(f.fieldPosition.clicked, self.onPosition) qconnect(f.sortField.clicked, self.onSortField) qconnect(f.buttonBox.helpRequested, self.onHelp) def onDrop(self, ev: QDropEvent) -> None: fieldList = self.form.fieldList indicatorPos = fieldList.dropIndicatorPosition() dropPos = fieldList.indexAt(ev.pos()).row() idx = self.currentIdx if dropPos == idx: return if indicatorPos == QAbstractItemView.OnViewport: # to bottom. movePos = fieldList.count() - 1 elif indicatorPos == QAbstractItemView.AboveItem: movePos = dropPos elif indicatorPos == QAbstractItemView.BelowItem: movePos = dropPos + 1 # the item in idx is removed thus subtract 1. if idx < dropPos: movePos -= 1 self.moveField(movePos + 1) # convert to 1 based. def onRowChange(self, idx: int) -> None: if idx == -1: return self.saveField() self.loadField(idx) def _uniqueName(self, prompt: str, ignoreOrd: Optional[int] = None, old: str = "") -> Optional[str]: txt = getOnlyText(prompt, default=old).replace('"', "").strip() if not txt: return None if txt[0] in "#^/": showWarning(tr.fields_name_first_letter_not_valid()) return None for letter in """:{"}""": if letter in txt: showWarning(tr.fields_name_invalid_letter()) return None for f in self.model["flds"]: if ignoreOrd is not None and f["ord"] == ignoreOrd: continue if f["name"] == txt: showWarning(tr.fields_that_field_name_is_already_used()) return None return txt def onRename(self) -> None: idx = self.currentIdx f = self.model["flds"][idx] name = self._uniqueName(tr.actions_new_name(), self.currentIdx, f["name"]) if not name: return old_name = f["name"] self.change_tracker.mark_basic() self.mm.rename_field(self.model, f, name) gui_hooks.fields_did_rename_field(self, f, old_name) self.saveField() self.fillFields() self.form.fieldList.setCurrentRow(idx) def onAdd(self) -> None: name = self._uniqueName(tr.fields_field_name()) if not name: return if not self.change_tracker.mark_schema(): return self.saveField() f = self.mm.newField(name) self.mm.add_field(self.model, f) self.fillFields() self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1) def onDelete(self) -> None: if len(self.model["flds"]) < 2: showWarning(tr.fields_notes_require_at_least_one_field()) return count = self.mm.useCount(self.model) c = tr.browsing_note_count(count=count) if not askUser(tr.fields_delete_field_from(val=c)): return if not self.change_tracker.mark_schema(): return f = self.model["flds"][self.form.fieldList.currentRow()] self.mm.remove_field(self.model, f) gui_hooks.fields_did_delete_field(self, f) self.fillFields() self.form.fieldList.setCurrentRow(0) def onPosition(self, delta: int = -1) -> None: idx = self.currentIdx l = len(self.model["flds"]) txt = getOnlyText(tr.fields_new_position_1(val=l), default=str(idx + 1)) if not txt: return try: pos = int(txt) except ValueError: return if not 0 < pos <= l: return self.moveField(pos) def onSortField(self) -> None: if not self.change_tracker.mark_schema(): return # don't allow user to disable; it makes no sense self.form.sortField.setChecked(True) self.mm.set_sort_index(self.model, self.form.fieldList.currentRow()) def moveField(self, pos: int) -> None: if not self.change_tracker.mark_schema(): return self.saveField() f = self.model["flds"][self.currentIdx] self.mm.reposition_field(self.model, f, pos - 1) self.fillFields() self.form.fieldList.setCurrentRow(pos - 1) def loadField(self, idx: int) -> None: self.currentIdx = idx fld = self.model["flds"][idx] f = self.form f.fontFamily.setCurrentFont(QFont(fld["font"])) f.fontSize.setValue(fld["size"]) f.sticky.setChecked(fld["sticky"]) f.sortField.setChecked(self.model["sortf"] == fld["ord"]) f.rtl.setChecked(fld["rtl"]) def saveField(self) -> None: # not initialized yet? if self.currentIdx is None: return idx = self.currentIdx fld = self.model["flds"][idx] f = self.form font = f.fontFamily.currentFont().family() if fld["font"] != font: fld["font"] = font self.change_tracker.mark_basic() size = f.fontSize.value() if fld["size"] != size: fld["size"] = size self.change_tracker.mark_basic() sticky = f.sticky.isChecked() if fld["sticky"] != sticky: fld["sticky"] = sticky self.change_tracker.mark_basic() rtl = f.rtl.isChecked() if fld["rtl"] != rtl: fld["rtl"] = rtl self.change_tracker.mark_basic() def reject(self) -> None: if self.change_tracker.changed(): if not askUser("Discard changes?"): return QDialog.reject(self) def accept(self) -> None: self.saveField() def on_done(changes: OpChanges) -> None: tooltip(tr.card_templates_changes_saved(), parent=self.parentWidget()) QDialog.accept(self) update_notetype_legacy( parent=self.mw, notetype=self.model).success(on_done).run_in_background() def onHelp(self) -> None: openHelp(HelpPage.CUSTOMIZING_FIELDS)
class CardLayout(QDialog): def __init__( self, mw: AnkiQt, note: Note, ord=0, parent: Optional[QWidget] = None, fill_empty: bool = False, ): QDialog.__init__(self, parent or mw, Qt.Window) mw.setupDialogGC(self) self.mw = aqt.mw self.note = note self.ord = ord self.col = self.mw.col.weakref() self.mm = self.mw.col.models self.model = note.model() self.templates = self.model["tmpls"] self._want_fill_empty_on = fill_empty self.have_autoplayed = False self.mm._remove_from_cache(self.model["id"]) self.mw.checkpoint(_("Card Types")) self.change_tracker = ChangeTracker(self.mw) self.setupTopArea() self.setupMainArea() self.setupButtons() self.setupShortcuts() self.setWindowTitle(_("Card Types for %s") % self.model["name"]) v1 = QVBoxLayout() v1.addWidget(self.topArea) v1.addWidget(self.mainArea) v1.addLayout(self.buttons) v1.setContentsMargins(12, 12, 12, 12) self.setLayout(v1) gui_hooks.card_layout_will_show(self) self.redraw_everything() restoreGeom(self, "CardLayout") self.setWindowModality(Qt.ApplicationModal) self.show() # take the focus away from the first input area when starting up, # as users tend to accidentally type into the template self.setFocus() def redraw_everything(self): self.ignore_change_signals = True self.updateTopArea() self.ignore_change_signals = False self.update_current_ordinal_and_redraw(self.ord) def update_current_ordinal_and_redraw(self, idx): if self.ignore_change_signals: return self.ord = idx self.have_autoplayed = False self.fill_fields_from_template() self.renderPreview() def _isCloze(self): return self.model["type"] == MODEL_CLOZE # Top area ########################################################################## def setupTopArea(self): self.topArea = QWidget() self.topAreaForm = aqt.forms.clayout_top.Ui_Form() self.topAreaForm.setupUi(self.topArea) self.topAreaForm.templateOptions.setText( _("Options") + " " + downArrow()) qconnect(self.topAreaForm.templateOptions.clicked, self.onMore) qconnect( self.topAreaForm.templatesBox.currentIndexChanged, self.update_current_ordinal_and_redraw, ) self.topAreaForm.card_type_label.setText( tr(TR.CARD_TEMPLATES_CARD_TYPE)) def updateTopArea(self): self.updateCardNames() def updateCardNames(self): self.ignore_change_signals = True combo = self.topAreaForm.templatesBox combo.clear() combo.addItems( self._summarizedName(idx, tmpl) for (idx, tmpl) in enumerate(self.templates)) combo.setCurrentIndex(self.ord) combo.setEnabled(not self._isCloze()) self.ignore_change_signals = False def _summarizedName(self, idx: int, tmpl: Dict): return "{}: {}: {} -> {}".format( idx + 1, tmpl["name"], self._fieldsOnTemplate(tmpl["qfmt"]), self._fieldsOnTemplate(tmpl["afmt"]), ) def _fieldsOnTemplate(self, fmt): matches = re.findall("{{[^#/}]+?}}", fmt) chars_allowed = 30 field_names: List[str] = [] for m in matches: # strip off mustache m = re.sub(r"[{}]", "", m) # strip off modifiers m = m.split(":")[-1] # don't show 'FrontSide' if m == "FrontSide": continue field_names.append(m) chars_allowed -= len(m) if chars_allowed <= 0: break s = "+".join(field_names) if chars_allowed <= 0: s += "+..." return s def setupShortcuts(self): self.tform.front_button.setToolTip(shortcut("Ctrl+1")) self.tform.back_button.setToolTip(shortcut("Ctrl+2")) self.tform.style_button.setToolTip(shortcut("Ctrl+3")) QShortcut( # type: ignore QKeySequence("Ctrl+1"), self, activated=self.tform.front_button.click, ) QShortcut( # type: ignore QKeySequence("Ctrl+2"), self, activated=self.tform.back_button.click, ) QShortcut( # type: ignore QKeySequence("Ctrl+3"), self, activated=self.tform.style_button.click, ) # Main area setup ########################################################################## def setupMainArea(self): w = self.mainArea = QWidget() l = QHBoxLayout() l.setContentsMargins(0, 0, 0, 0) l.setSpacing(3) left = QWidget() tform = self.tform = aqt.forms.template.Ui_Form() tform.setupUi(left) l.addWidget(left, 5) right = QWidget() self.pform = aqt.forms.preview.Ui_Form() pform = self.pform pform.setupUi(right) pform.preview_front.setText(tr(TR.CARD_TEMPLATES_FRONT_PREVIEW)) pform.preview_back.setText(tr(TR.CARD_TEMPLATES_BACK_PREVIEW)) pform.preview_box.setTitle(tr(TR.CARD_TEMPLATES_PREVIEW_BOX)) self.setup_edit_area() self.setup_preview() l.addWidget(right, 5) w.setLayout(l) def setup_edit_area(self): tform = self.tform tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE)) tform.back_button.setText(tr(TR.CARD_TEMPLATES_BACK_TEMPLATE)) tform.style_button.setText(tr(TR.CARD_TEMPLATES_TEMPLATE_STYLING)) tform.groupBox.setTitle(tr(TR.CARD_TEMPLATES_TEMPLATE_BOX)) cnt = self.mw.col.models.useCount(self.model) self.tform.changes_affect_label.setText( self.col.tr(TR.CARD_TEMPLATES_CHANGES_WILL_AFFECT_NOTES, count=cnt)) qconnect(tform.edit_area.textChanged, self.write_edits_to_template_and_redraw) qconnect(tform.front_button.clicked, self.on_editor_toggled) qconnect(tform.back_button.clicked, self.on_editor_toggled) qconnect(tform.style_button.clicked, self.on_editor_toggled) self.current_editor_index = 0 self.tform.edit_area.setAcceptRichText(False) self.tform.edit_area.setFont(QFont("Courier")) if qtminor < 10: self.tform.edit_area.setTabStopWidth(30) else: tab_width = self.fontMetrics().width(" " * 4) self.tform.edit_area.setTabStopDistance(tab_width) widg = tform.search_edit widg.setPlaceholderText("Search") qconnect(widg.textChanged, self.on_search_changed) qconnect(widg.returnPressed, self.on_search_next) 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 on_change_cloze(self, idx: int) -> None: self.ord = self.cloze_numbers[idx] - 1 self.have_autoplayed = False self._renderPreview() def on_editor_toggled(self): if self.tform.front_button.isChecked(): self.current_editor_index = 0 self.pform.preview_front.setChecked(True) self.on_preview_toggled() self.add_field_button.setHidden(False) elif self.tform.back_button.isChecked(): self.current_editor_index = 1 self.pform.preview_back.setChecked(True) self.on_preview_toggled() self.add_field_button.setHidden(False) else: self.current_editor_index = 2 self.add_field_button.setHidden(True) self.fill_fields_from_template() def on_search_changed(self, text: str): editor = self.tform.edit_area if not editor.find(text): # try again from top cursor = editor.textCursor() cursor.movePosition(QTextCursor.Start) editor.setTextCursor(cursor) if not editor.find(text): tooltip("No matches found.") def on_search_next(self): text = self.tform.search_edit.text() self.on_search_changed(text) def setup_preview(self): pform = self.pform self.preview_web = AnkiWebView(title="card layout") pform.verticalLayout.addWidget(self.preview_web) pform.verticalLayout.setStretch(1, 99) pform.preview_front.isChecked() qconnect(pform.preview_front.clicked, self.on_preview_toggled) qconnect(pform.preview_back.clicked, self.on_preview_toggled) if self._want_fill_empty_on: pform.fill_empty.setChecked(True) qconnect(pform.fill_empty.toggled, self.on_preview_toggled) if not self.note_has_empty_field(): pform.fill_empty.setHidden(True) pform.fill_empty.setText(tr(TR.CARD_TEMPLATES_FILL_EMPTY)) jsinc = [ "jquery.js", "browsersel.js", "mathjax/conf.js", "mathjax/MathJax.js", "reviewer.js", ] self.preview_web.stdHtml( self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self, ) self.preview_web.set_bridge_command(self._on_bridge_cmd, self) 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) def on_preview_toggled(self): self.have_autoplayed = False self._renderPreview() def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): play_clicked_audio(cmd, self.rendered_card) def note_has_empty_field(self) -> bool: for field in self.note.fields: if not field.strip(): # ignores HTML, but this should suffice return True return False # Buttons ########################################################################## def setupButtons(self): l = self.buttons = QHBoxLayout() help = QPushButton(_("Help")) help.setAutoDefault(False) l.addWidget(help) qconnect(help.clicked, self.onHelp) l.addStretch() self.add_field_button = QPushButton(_("Add Field")) self.add_field_button.setAutoDefault(False) l.addWidget(self.add_field_button) qconnect(self.add_field_button.clicked, self.onAddField) if not self._isCloze(): flip = QPushButton(_("Flip")) flip.setAutoDefault(False) l.addWidget(flip) qconnect(flip.clicked, self.onFlip) l.addStretch() save = QPushButton(_("Save")) save.setAutoDefault(False) l.addWidget(save) qconnect(save.clicked, self.accept) close = QPushButton(_("Cancel")) close.setAutoDefault(False) l.addWidget(close) qconnect(close.clicked, self.reject) # Reading/writing question/answer/css ########################################################################## def current_template(self) -> Dict: if self._isCloze(): return self.templates[0] return self.templates[self.ord] def fill_fields_from_template(self): t = self.current_template() self.ignore_change_signals = True if self.current_editor_index == 0: text = t["qfmt"] elif self.current_editor_index == 1: text = t["afmt"] else: text = self.model["css"] self.tform.edit_area.setPlainText(text) self.ignore_change_signals = False def write_edits_to_template_and_redraw(self): if self.ignore_change_signals: return self.change_tracker.mark_basic() text = self.tform.edit_area.toPlainText() if self.current_editor_index == 0: self.current_template()["qfmt"] = text elif self.current_editor_index == 1: self.current_template()["afmt"] = text else: self.model["css"] = text self.renderPreview() # Preview ########################################################################## _previewTimer = None def renderPreview(self): # schedule a preview when timing stops self.cancelPreviewTimer() self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False) def cancelPreviewTimer(self): if self._previewTimer: self._previewTimer.stop() self._previewTimer = None def _renderPreview(self) -> None: self.cancelPreviewTimer() c = self.rendered_card = self.ephemeral_card_for_rendering() ti = self.maybeTextInput 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 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 # use _showAnswer to avoid the longer delay self.preview_web.eval("_showAnswer(%s,'%s');" % (json.dumps(text), bodyclass)) if not self.have_autoplayed: self.have_autoplayed = True if c.autoplay(): if self.pform.preview_front.isChecked(): audio = c.question_av_tags() else: audio = c.answer_av_tags() av_player.play_tags(audio) else: av_player.clear_queue_and_maybe_interrupt() self.updateCardNames() def maybeTextInput(self, txt, type="q"): if "[[type:" not in txt: return txt origLen = len(txt) txt = txt.replace("<hr id=answer>", "") hadHR = origLen != len(txt) def answerRepl(match): res = self.mw.reviewer.correct("exomple", "an example") if hadHR: res = "<hr id=answer>" + res return res repl: Union[str, Callable] if type == "q": repl = "<input id='typeans' type=text value='exomple' readonly='readonly'>" repl = "<center>%s</center>" % repl else: repl = answerRepl return re.sub(r"\[\[type:.+?\]\]", repl, txt) def ephemeral_card_for_rendering(self) -> Card: card = Card(self.col) card.ord = self.ord card.did = 1 template = copy.copy(self.current_template()) # may differ in cloze case template["ord"] = card.ord output = TemplateRenderContext.from_card_layout( self.note, card, notetype=self.model, template=template, fill_empty=self.pform.fill_empty.isChecked(), ).render() card.set_render_output(output) return card # Card operations ###################################################################### def onRemove(self): if len(self.templates) < 2: return showInfo(_("At least one card type is required.")) def get_count(): return self.mm.template_use_count(self.model["id"], self.ord) def on_done(fut): card_cnt = fut.result() template = self.current_template() cards = ngettext("%d card", "%d cards", card_cnt) % card_cnt msg = _("Delete the '%(a)s' card type, and its %(b)s?") % dict( a=template["name"], b=cards) if not askUser(msg): return if not self.change_tracker.mark_schema(): return self.onRemoveInner(template) self.mw.taskman.with_progress(get_count, on_done) def onRemoveInner(self, template) -> None: self.mm.remove_template(self.model, template) # ensure current ordinal is within bounds 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 if not self.change_tracker.mark_schema(): return template["name"] = name self.redraw_everything() def onReorder(self): n = len(self.templates) template = self.current_template() current_pos = self.templates.index(template) + 1 pos = getOnlyText(_("Enter new card position (1...%s):") % n, default=str(current_pos)) if not pos: return try: pos = int(pos) except ValueError: return if pos < 1 or pos > n: return if pos == current_pos: return new_idx = pos - 1 if not self.change_tracker.mark_schema(): return self.mm.reposition_template(self.model, template, new_idx) self.ord = new_idx self.redraw_everything() def _newCardName(self): n = len(self.templates) + 1 while 1: name = _("Card %d") % n if name not in [t["name"] for t in self.templates]: break n += 1 return name def onAddCard(self): cnt = self.mw.col.models.useCount(self.model) txt = (ngettext( "This will create %d card. Proceed?", "This will create %d cards. Proceed?", cnt, ) % cnt) if not askUser(txt): return if not self.change_tracker.mark_schema(): return name = self._newCardName() t = self.mm.newTemplate(name) old = self.current_template() t["qfmt"] = old["qfmt"] t["afmt"] = old["afmt"] self.mm.add_template(self.model, t) self.ord = len(self.templates) - 1 self.redraw_everything() def onFlip(self): old = self.current_template() self._flipQA(old, old) self.redraw_everything() def _flipQA(self, src, dst): m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"]) if not m: showInfo( _("""\ Anki couldn't find the line between the question and answer. Please \ adjust the template manually to switch the question and answer.""")) return self.change_tracker.mark_basic() dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"] dst["qfmt"] = m.group(2).strip() return True def onMore(self): m = QMenu(self) if not self._isCloze(): a = m.addAction(_("Add Card Type...")) qconnect(a.triggered, self.onAddCard) a = m.addAction(_("Remove Card Type...")) qconnect(a.triggered, self.onRemove) a = m.addAction(_("Rename Card Type...")) qconnect(a.triggered, self.onRename) a = m.addAction(_("Reposition Card Type...")) qconnect(a.triggered, self.onReorder) m.addSeparator() t = self.current_template() if t["did"]: s = _(" (on)") else: s = _(" (off)") a = m.addAction(_("Deck Override...") + s) qconnect(a.triggered, self.onTargetDeck) a = m.addAction(_("Browser Appearance...")) qconnect(a.triggered, self.onBrowserDisplay) m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0))) def onBrowserDisplay(self): d = QDialog() f = aqt.forms.browserdisp.Ui_Dialog() f.setupUi(d) t = self.current_template() f.qfmt.setText(t.get("bqfmt", "")) f.afmt.setText(t.get("bafmt", "")) if t.get("bfont"): f.overrideFont.setChecked(True) f.font.setCurrentFont(QFont(t.get("bfont", "Arial"))) f.fontSize.setValue(t.get("bsize", 12)) qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f)) d.exec_() def onBrowserDisplayOk(self, f): t = self.current_template() self.change_tracker.mark_basic() t["bqfmt"] = f.qfmt.text().strip() t["bafmt"] = f.afmt.text().strip() if f.overrideFont.isChecked(): t["bfont"] = f.font.currentFont().family() t["bsize"] = f.fontSize.value() else: for key in ("bfont", "bsize"): if key in t: del t[key] def onTargetDeck(self): from aqt.tagedit import TagEdit t = self.current_template() d = QDialog(self) d.setWindowTitle("Anki") d.setMinimumWidth(400) l = QVBoxLayout() lab = QLabel( _("""\ Enter deck to place new %s cards in, or leave blank:""") % self.current_template()["name"]) lab.setWordWrap(True) l.addWidget(lab) te = TagEdit(d, type=1) te.setCol(self.col) l.addWidget(te) if t["did"]: te.setText(self.col.decks.get(t["did"])["name"]) te.selectAll() bb = QDialogButtonBox(QDialogButtonBox.Close) qconnect(bb.rejected, d.close) l.addWidget(bb) d.setLayout(l) d.exec_() self.change_tracker.mark_basic() if not te.text().strip(): t["did"] = None else: t["did"] = self.col.decks.id(te.text()) def onAddField(self): diag = QDialog(self) form = aqt.forms.addfield.Ui_Dialog() form.setupUi(diag) fields = [f["name"] for f in self.model["flds"]] form.fields.addItems(fields) form.fields.setCurrentRow(0) form.font.setCurrentFont(QFont("Arial")) form.size.setValue(20) if not diag.exec_(): return row = form.fields.currentIndex().row() if row >= 0: self._addField( fields[row], form.font.currentFont().family(), form.size.value(), ) def _addField(self, field, font, size): text = self.tform.edit_area.toPlainText() text += "\n<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\n" % ( font, size, field, ) self.tform.edit_area.setPlainText(text) self.change_tracker.mark_basic() self.write_edits_to_template_and_redraw() # Closing & Help ###################################################################### def accept(self) -> None: def save(): self.mm.save(self.model) def on_done(fut): try: fut.result() except TemplateError as e: showWarning("Unable to save changes: " + str(e)) return self.mw.reset() tooltip("Changes saved.", parent=self.parent()) self.cleanup() gui_hooks.sidebar_should_refresh_notetypes() return QDialog.accept(self) self.mw.taskman.with_progress(save, on_done) def reject(self) -> None: if self.change_tracker.changed(): if not askUser("Discard changes?"): return self.cleanup() return QDialog.reject(self) def cleanup(self) -> None: self.cancelPreviewTimer() av_player.stop_and_clear_queue() saveGeom(self, "CardLayout") self.preview_web = None self.model = None self.rendered_card = None self.mw = None def onHelp(self): openHelp("templates")