def add_graphs_to_congrats(webview: AnkiWebView): page = basename(webview.page().url().path()) if page != "congrats.html": return graph_css = make_graph_css() graph_js = make_graph_js(get_active_congrats_graphs(), "deck:current") webview.eval(f""" const graphsContainer = document.createElement("div") graphsContainer.id = "graphsSection" graphsContainer.style = "text-align: center;" document.body.appendChild(graphsContainer) const loadGraphs = () => {{ {graph_css} {graph_js} }} const loadGraphScript = () => {{ const graphScript = document.createElement("script") graphScript.onload = loadGraphs graphScript.charset = "UTF-8" graphScript.src = "graphs.js" document.head.appendChild(graphScript) }} const protobufScript = document.createElement("script") protobufScript.onload = loadGraphScript protobufScript.charset = "UTF-8" protobufScript.src = "../js/vendor/protobuf.min.js" document.head.appendChild(protobufScript) """)
class ChangeNotetypeDialog(QDialog): TITLE = "changeNotetype" silentlyClose = True def __init__( self, parent: QWidget, mw: aqt.main.AnkiQt, note_ids: Sequence[NoteId], notetype_id: NotetypeId, ) -> None: QDialog.__init__(self, parent) self.mw = mw self._note_ids = note_ids self._setup_ui(notetype_id) self.show() def _setup_ui(self, notetype_id: NotetypeId) -> None: self.setWindowModality(Qt.ApplicationModal) self.mw.garbage_collect_on_dialog_finish(self) self.setMinimumWidth(400) disable_help_button(self) restoreGeom(self, self.TITLE) addCloseShortcut(self) self.web = AnkiWebView(title=self.TITLE) self.web.setVisible(False) self.web.load_ts_page("change-notetype") layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.web) self.setLayout(layout) self.web.eval(f"""anki.changeNotetypePage( document.getElementById('main'), {notetype_id}, {notetype_id});""") self.setWindowTitle(tr.browsing_change_notetype()) def reject(self) -> None: self.web = None saveGeom(self, self.TITLE) QDialog.reject(self) def save(self, data: bytes) -> None: input = ChangeNotetypeRequest() input.ParseFromString(data) if not self.mw.confirm_schema_modification(): return def on_done(op: OpChanges) -> None: tooltip( tr.browsing_notes_updated(count=len(input.note_ids)), parent=self.parentWidget(), ) self.reject() input.note_ids.extend(self._note_ids) change_notetype_of_notes( parent=self, input=input).success(on_done).run_in_background()
class CardInfoDialog(QDialog): TITLE = "browser card info" GEOMETRY_KEY = "revlog" silentlyClose = True def __init__( self, parent: QWidget | None, mw: aqt.AnkiQt, card: Card | None, on_close: Callable | None = None, geometry_key: str | None = None, window_title: str | None = None, ) -> None: super().__init__(parent) self.mw = mw self._on_close = on_close self.GEOMETRY_KEY = geometry_key or self.GEOMETRY_KEY if window_title: self.setWindowTitle(window_title) self._setup_ui(card.id if card else None) self.show() def _setup_ui(self, card_id: CardId | None) -> None: self.mw.garbage_collect_on_dialog_finish(self) disable_help_button(self) restoreGeom(self, self.GEOMETRY_KEY) addCloseShortcut(self) setWindowIcon(self) self.web = AnkiWebView(title=self.TITLE) self.web.setVisible(False) self.web.load_ts_page("card-info") layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.web) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) buttons.setContentsMargins(10, 0, 10, 10) layout.addWidget(buttons) qconnect(buttons.rejected, self.reject) self.setLayout(layout) self.web.eval( "const cardInfo = anki.cardInfo(document.getElementById('main'));" ) self.update_card(card_id) def update_card(self, card_id: CardId | None) -> None: self.web.eval( f"cardInfo.then((c) => c.$set({{ cardId: {json.dumps(card_id)} }}));" ) def reject(self) -> None: if self._on_close: self._on_close() self.web.cleanup() self.web = None saveGeom(self, self.GEOMETRY_KEY) return QDialog.reject(self)
class DeckOptionsDialog(QDialog): "The new deck configuration screen." TITLE = "deckOptions" silentlyClose = True def __init__(self, mw: aqt.main.AnkiQt, deck: DeckDict) -> None: QDialog.__init__(self, mw, Qt.WindowType.Window) self.mw = mw self._deck = deck self._setup_ui() self.show() def _setup_ui(self) -> None: self.setWindowModality(Qt.WindowModality.ApplicationModal) self.mw.garbage_collect_on_dialog_finish(self) self.setMinimumWidth(400) disable_help_button(self) restoreGeom(self, self.TITLE) addCloseShortcut(self) self.web = AnkiWebView(title=self.TITLE) self.web.setVisible(False) self.web.load_ts_page("deck-options") layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.web) self.setLayout(layout) self.web.eval( f"""const $deckOptions = anki.setupDeckOptions({self._deck["id"]});""" ) self.setWindowTitle( without_unicode_isolation( tr.actions_options_for(val=self._deck["name"]))) gui_hooks.deck_options_did_load(self) def reject(self) -> None: self.web.cleanup() self.web = None saveGeom(self, self.TITLE) QDialog.reject(self)
def showSetHighlightColorDialog(self): #Objective is a dialog to set highlight color used with 'h' key d = QDialog(self.mw) l = QVBoxLayout() l.setMargin(0) w = AnkiWebView() l.addWidget(w) #Add python object to take values back from javascript callback = IREHighlightColorCallback(); w.page().mainFrame().addToJavaScriptWindowObject("callback", callback); getHighlightColorScript = """ function getHighlightColor() { callback.setHighlightColor(document.getElementById('color').value.trim()); if(document.getElementById('colorBackOrText').checked) { callback.setColorText('false'); } else { callback.setColorText('true'); } }; """ #color text box colorTextField = "<span style='font-weight:bold'>Source highlighting color (IRead2 model only): </span><input type='text' id='color' value='" + self.highlightColor + "' />"; colorBackOrText = "<span style='font-weight:bold'>Apply color to: </span><input type='radio' id='colorBackOrText' name='colorBackOrText' value='false' checked='true' /> Background <input type='radio' name='colorBackOrText' value='true' /> Text<br />"; html = "<html><head><script>" + getHighlightColorScript + "</script></head><body>"; html += "<p>" + colorTextField; html += "<p>" + colorBackOrText; html += "</body></html>"; w.stdHtml(html); bb = QDialogButtonBox(QDialogButtonBox.Close|QDialogButtonBox.Save) bb.connect(bb, SIGNAL("accepted()"), d, SLOT("accept()")) bb.connect(bb, SIGNAL("rejected()"), d, SLOT("reject()")) bb.setOrientation(QtCore.Qt.Horizontal); l.addWidget(bb) d.setLayout(l) d.setWindowModality(Qt.WindowModal) d.resize(500, 200) choice = d.exec_(); if(choice == 1): w.eval("getHighlightColor()");
class DeckOptionsDialog(QDialog): "The new deck configuration screen." TITLE = "deckOptions" silentlyClose = True def __init__(self, mw: aqt.main.AnkiQt) -> None: QDialog.__init__(self, mw, Qt.Window) self.mw = mw self._setup_ui() self.show() def _setup_ui(self) -> None: self.setWindowModality(Qt.ApplicationModal) self.mw.garbage_collect_on_dialog_finish(self) self.setMinimumWidth(400) disable_help_button(self) restoreGeom(self, self.TITLE) addCloseShortcut(self) self.web = AnkiWebView(title=self.TITLE) self.web.setVisible(False) self.web.load_ts_page("deckoptions") layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.web) self.setLayout(layout) deck_id = self.mw.col.decks.get_current_id() self.web.eval(f"""const $deckOptions = anki.deckOptions( document.getElementById('main'), {deck_id});""") gui_hooks.deck_options_did_load(self) def reject(self) -> None: self.web = None saveGeom(self, self.TITLE) QDialog.reject(self)
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 Previewer(QDialog): _last_state = None _card_changed = False _last_render: Union[int, float] = 0 _timer = None _show_both_sides = False def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]): super().__init__(None, Qt.Window) self._open = True self._parent = parent self._close_callback = on_close self.mw = mw icon = QIcon() icon.addPixmap(QPixmap(":/icons/anki.png"), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) def card(self) -> Optional[Card]: raise NotImplementedError def card_changed(self) -> bool: raise NotImplementedError def open(self): self._state = "question" self._last_state = None self._create_gui() self._setup_web_view() self.render_card() self.show() def _create_gui(self): self.setWindowTitle(tr(TR.ACTIONS_PREVIEW)) qconnect(self.finished, self._on_finished) self.silentlyClose = True self.vbox = QVBoxLayout() self.vbox.setContentsMargins(0, 0, 0, 0) self._web = AnkiWebView(title="previewer") self.vbox.addWidget(self._web) self.bbox = QDialogButtonBox() self._replay = self.bbox.addButton(tr(TR.ACTIONS_REPLAY_AUDIO), QDialogButtonBox.ActionRole) self._replay.setAutoDefault(False) self._replay.setShortcut(QKeySequence("R")) self._replay.setToolTip(tr(TR.ACTIONS_SHORTCUT_KEY, val="R")) qconnect(self._replay.clicked, self._on_replay_audio) both_sides_button = QCheckBox(tr(TR.QT_MISC_BACK_SIDE_ONLY)) both_sides_button.setShortcut(QKeySequence("B")) both_sides_button.setToolTip(tr(TR.ACTIONS_SHORTCUT_KEY, val="B")) self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole) self._show_both_sides = self.mw.col.conf.get("previewBothSides", False) both_sides_button.setChecked(self._show_both_sides) qconnect(both_sides_button.toggled, self._on_show_both_sides) self.vbox.addWidget(self.bbox) self.setLayout(self.vbox) restoreGeom(self, "preview") def _on_finished(self, ok): saveGeom(self, "preview") self.mw.progress.timer(100, self._on_close, False) def _on_replay_audio(self): if self._state == "question": replay_audio(self.card(), True) elif self._state == "answer": replay_audio(self.card(), False) def close(self): self._on_close() super().close() def _on_close(self): self._open = False self._close_callback() def _setup_web_view(self): jsinc = [ "js/vendor/jquery.min.js", "js/vendor/browsersel.js", "js/mathjax.js", "js/vendor/mathjax/tex-chtml.js", "js/reviewer.js", ] self._web.stdHtml( self.mw.reviewer.revHtml(), css=["css/reviewer.css"], js=jsinc, context=self, ) self._web.set_bridge_command(self._on_bridge_cmd, self) def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): play_clicked_audio(cmd, self.card()) def render_card(self): self.cancel_timer() # Keep track of whether render() has ever been called # with cardChanged=True since the last successful render self._card_changed |= self.card_changed() # avoid rendering in quick succession elap_ms = int((time.time() - self._last_render) * 1000) delay = 300 if elap_ms < delay: self._timer = self.mw.progress.timer(delay - elap_ms, self._render_scheduled, False) else: self._render_scheduled() def cancel_timer(self): if self._timer: self._timer.stop() self._timer = None def _render_scheduled(self) -> None: self.cancel_timer() self._last_render = time.time() if not self._open: return c = self.card() func = "_showQuestion" if not c: txt = tr(TR.QT_MISC_PLEASE_SELECT_1_CARD) bodyclass = "" self._last_state = None else: if self._show_both_sides: self._state = "answer" elif self._card_changed: self._state = "question" currentState = self._state_and_mod() if currentState == self._last_state: # nothing has changed, avoid refreshing return # need to force reload even if answer txt = c.q(reload=True) if self._state == "answer": func = "_showAnswer" txt = c.a() txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt) bodyclass = theme_manager.body_classes_for_card_ord(c.ord) if c.autoplay(): AnkiWebView.setPlaybackRequiresGesture(False) if self._show_both_sides: # if we're showing both sides at once, remove any audio # from the answer that's appeared on the question already question_audio = c.question_av_tags() only_on_answer_audio = [ x for x in c.answer_av_tags() if x not in question_audio ] audio = question_audio + only_on_answer_audio elif self._state == "question": 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() txt = self.mw.prepare_card_text_for_display(txt) txt = gui_hooks.card_will_show( txt, c, "preview" + self._state.capitalize()) self._last_state = self._state_and_mod() self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass)) self._card_changed = False def _on_show_both_sides(self, toggle): self._show_both_sides = toggle self.mw.col.conf["previewBothSides"] = toggle self.mw.col.setMod() if self._state == "answer" and not toggle: self._state = "question" self.render_card() def _state_and_mod(self): c = self.card() n = c.note() n.load() return (self._state, c.id, n.mod) def state(self) -> str: return self._state
class Previewer(QDialog): _last_state: LastStateAndMod | None = None _card_changed = False _last_render: int | float = 0 _timer: QTimer | None = None _show_both_sides = False def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]) -> None: super().__init__(None, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self._open = True self._parent = parent self._close_callback = on_close self.mw = mw disable_help_button(self) setWindowIcon(self) def card(self) -> Card | None: raise NotImplementedError def card_changed(self) -> bool: raise NotImplementedError def open(self) -> None: self._state = "question" self._last_state = None self._create_gui() self._setup_web_view() self.render_card() self.show() def _create_gui(self) -> None: self.setWindowTitle(tr.actions_preview()) self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self) qconnect(self.close_shortcut.activated, self.close) qconnect(self.finished, self._on_finished) self.silentlyClose = True self.vbox = QVBoxLayout() self.vbox.setContentsMargins(0, 0, 0, 0) self._web = AnkiWebView(title="previewer") self.vbox.addWidget(self._web) self.bbox = QDialogButtonBox() self._replay = self.bbox.addButton( tr.actions_replay_audio(), QDialogButtonBox.ButtonRole.ActionRole) self._replay.setAutoDefault(False) self._replay.setShortcut(QKeySequence("R")) self._replay.setToolTip(tr.actions_shortcut_key(val="R")) qconnect(self._replay.clicked, self._on_replay_audio) both_sides_button = QCheckBox(tr.qt_misc_back_side_only()) both_sides_button.setShortcut(QKeySequence("B")) both_sides_button.setToolTip(tr.actions_shortcut_key(val="B")) self.bbox.addButton(both_sides_button, QDialogButtonBox.ButtonRole.ActionRole) self._show_both_sides = self.mw.col.get_config_bool( Config.Bool.PREVIEW_BOTH_SIDES) both_sides_button.setChecked(self._show_both_sides) qconnect(both_sides_button.toggled, self._on_show_both_sides) self.vbox.addWidget(self.bbox) self.setLayout(self.vbox) restoreGeom(self, "preview") def _on_finished(self, ok: int) -> None: saveGeom(self, "preview") self.mw.progress.timer(100, self._on_close, False) def _on_replay_audio(self) -> None: if self._state == "question": replay_audio(self.card(), True) elif self._state == "answer": replay_audio(self.card(), False) def close(self) -> None: self._on_close() super().close() def _on_close(self) -> None: self._open = False self._close_callback() self._web = None def _setup_web_view(self) -> None: self._web.stdHtml( self.mw.reviewer.revHtml(), css=["css/reviewer.css"], js=[ "js/mathjax.js", "js/vendor/mathjax/tex-chtml.js", "js/reviewer.js", ], context=self, ) self._web.set_bridge_command(self._on_bridge_cmd, self) def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): play_clicked_audio(cmd, self.card()) def _update_flag_and_mark_icons(self, card: Card | None) -> None: if card: flag = card.user_flag() marked = card.note(reload=True).has_tag(MARKED_TAG) else: flag = 0 marked = False self._web.eval(f"_drawFlag({flag}); _drawMark({json.dumps(marked)});") def render_card(self) -> None: self.cancel_timer() # Keep track of whether render() has ever been called # with cardChanged=True since the last successful render self._card_changed |= self.card_changed() # avoid rendering in quick succession elap_ms = int((time.time() - self._last_render) * 1000) delay = 300 if elap_ms < delay: self._timer = self.mw.progress.timer(delay - elap_ms, self._render_scheduled, False) else: self._render_scheduled() def cancel_timer(self) -> None: if self._timer: self._timer.stop() self._timer = None def _render_scheduled(self) -> None: self.cancel_timer() self._last_render = time.time() if not self._open: return c = self.card() self._update_flag_and_mark_icons(c) func = "_showQuestion" ans_txt = "" if not c: txt = tr.qt_misc_please_select_1_card() bodyclass = "" self._last_state = None else: if self._show_both_sides: self._state = "answer" elif self._card_changed: self._state = "question" currentState = self._state_and_mod() if currentState == self._last_state: # nothing has changed, avoid refreshing return # need to force reload even if answer txt = c.question(reload=True) ans_txt = c.answer() if self._state == "answer": func = "_showAnswer" txt = ans_txt txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt) bodyclass = theme_manager.body_classes_for_card_ord(c.ord) if c.autoplay(): if self._show_both_sides: # if we're showing both sides at once, remove any audio # from the answer that's appeared on the question already question_audio = c.question_av_tags() only_on_answer_audio = [ x for x in c.answer_av_tags() if x not in question_audio ] audio = question_audio + only_on_answer_audio elif self._state == "question": audio = c.question_av_tags() else: audio = c.answer_av_tags() av_player.play_tags(audio) else: av_player.clear_queue_and_maybe_interrupt() txt = self.mw.prepare_card_text_for_display(txt) txt = gui_hooks.card_will_show( txt, c, f"preview{self._state.capitalize()}") self._last_state = self._state_and_mod() js: str if self._state == "question": ans_txt = self.mw.col.media.escape_media_filenames(ans_txt) js = f"{func}({json.dumps(txt)}, {json.dumps(ans_txt)}, '{bodyclass}');" else: js = f"{func}({json.dumps(txt)}, '{bodyclass}');" self._web.eval(js) self._card_changed = False def _on_show_both_sides(self, toggle: bool) -> None: self._show_both_sides = toggle self.mw.col.set_config_bool(Config.Bool.PREVIEW_BOTH_SIDES, toggle) if self._state == "answer" and not toggle: self._state = "question" self.render_card() def _state_and_mod(self) -> tuple[str, int, int]: c = self.card() n = c.note() n.load() return (self._state, c.id, n.mod) def state(self) -> str: return self._state
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")
def showIRSchedulerDialog(self, currentCard): #Handle for dialog open without a current card from IRead2 model deckID = None; cardID = None; if(currentCard == None): deck = mw._selectedDeck(); deckID = deck['id']; else: deckID = currentCard.did; cardID = currentCard.id; #Get the card data for the deck. Make sure it is an Incremental Reading deck (has IRead2 cards) before showing dialog cardDataList = self.getCardDataList(deckID, cardID); hasIRead2Cards = False; for cd in cardDataList: if(cd['title'] != 'No Title'): hasIRead2Cards = True; if(hasIRead2Cards == False): showInfo(_("Please select an Incremental Reading deck.")) return; d = QDialog(self.mw) l = QVBoxLayout() l.setMargin(0) w = AnkiWebView() l.addWidget(w) #Add python object to take values back from javascript callback = IRSchedulerCallback(); #callback.setCard(currentCard); w.page().mainFrame().addToJavaScriptWindowObject("callback", callback); #Script functions move up / move down / delete / open getIRSchedulerDialogScript = """ var cardList = new Array(); """ index = 0; for cardData in cardDataList: index+=1; getIRSchedulerDialogScript += "card = new Object();"; getIRSchedulerDialogScript += "card.id = " + str(cardData['id']) + ";"; getIRSchedulerDialogScript += "card.title = '" + str(cardData['title']) + "';"; getIRSchedulerDialogScript += "card.isCurrent = " + str(cardData['isCurrent']) + ";"; getIRSchedulerDialogScript += "card.checkbox = document.createElement('input');"; getIRSchedulerDialogScript += "card.checkbox.type = 'checkbox';"; if(cardData['isCurrent'] == 'true'): getIRSchedulerDialogScript += "card.checkbox.setAttribute('checked', 'true');"; getIRSchedulerDialogScript += "cardList[cardList.length] = card;"; getIRSchedulerDialogScript += """ function buildCardData() { var container = document.getElementById('cardList'); container.innerHTML = ''; var list = document.createElement('div'); list.setAttribute('style','overflow:auto;'); var table = document.createElement('table'); list.appendChild(table); container.appendChild(list); var row; var col; var cardData; for(var i = 0; i < cardList.length; i++) { row = document.createElement('tr'); row.setAttribute('id','row' + i); cardData = cardList[i]; col = document.createElement('td'); col.setAttribute('style','width:4em;'); col.innerHTML = '' + i; row.appendChild(col); col = document.createElement('td'); col.setAttribute('style','width:10em;'); col.innerHTML = '' + cardData.id; row.appendChild(col); col = document.createElement('td'); col.setAttribute('style','width:30em;'); col.innerHTML = '' + cardData.title; row.appendChild(col); col = document.createElement('td'); col.setAttribute('style','width:2em;'); col.appendChild(cardData.checkbox); row.appendChild(col); table.appendChild(row); } } function reposition(origIndex, newIndex, isTopOfRange) { if(newIndex < 0 || newIndex > (cardList.length-1)) return -1; if(cardList[newIndex].checkbox.checked) return -1; if(isTopOfRange) { document.getElementById('newPos').value = newIndex; } var removedCards = cardList.splice(origIndex,1); cardList.splice(newIndex, 0, removedCards[0]); return newIndex; } function moveSelectedUp() { var topOfRange = -1; for(var i = 0; i < cardList.length; i++) { if(cardList[i].checkbox.checked) { if(topOfRange == -1) topOfRange = i; if(i == topOfRange) { if(document.getElementById('anchor').checked) continue; //Don't move end of range if anchored. else reposition(i, i - 1, true); } else reposition(i, i - 1, false); } } buildCardData(); } function moveSelectedDown() { var topOfRange = -1; var bottomOfRange = -1 for(var i = 0; i < cardList.length; i++) { if(cardList[i].checkbox.checked) { if(topOfRange == -1) topOfRange = i; bottomOfRange = i; } } for(var i = cardList.length-1; i > -1; i--) { if(cardList[i].checkbox.checked) { if(i == bottomOfRange && document.getElementById('anchor').checked) { continue; //Don't move end of range if anchored. } if(i == topOfRange) reposition(i, i + 1, true); else reposition(i, i + 1, false); } } buildCardData(); } function selectAll() { for(var i = 0; i < cardList.length; i++) { cardList[i].checkbox.checked = true; } } function selectNone() { for(var i = 0; i < cardList.length; i++) { cardList[i].checkbox.checked = false; } } function directMove() { var newIndex = document.getElementById('newPos').value; var topOfRange = -1; origIndex = -1; for(var i = 0; i < cardList.length; i++) { if(cardList[i].checkbox.checked) { if(topOfRange == -1) topOfRange = i; if(origIndex == -1) { origIndex = i; sizeOfMove = (newIndex - origIndex); } } } if(sizeOfMove < 0) { for(var i = 0; i < cardList.length; i++) { if(cardList[i].checkbox.checked) { if(i == topOfRange) reposition(i, i + sizeOfMove, true); else reposition(i, i + sizeOfMove, false); } } } else { for(var i = cardList.length-1; i > -1; i--) { if(cardList[i].checkbox.checked) { if(i == topOfRange) reposition(i, i + sizeOfMove, true); else reposition(i, i + sizeOfMove, false); } } } buildCardData(); } function updatePositions() { var cids = new Array(); for(var i=0; i < cardList.length; i++) { cids[cids.length] = parseInt(cardList[i].id); } callback.updatePositions(cids); }; """; #Incremental Reading list as a list of nested <div> tags (like a table, but more flexible) #position,title,series id, sequence number,card id (hidden) newPosField = "<span style='font-weight:bold'>Card Position: </span><input type='text' id='newPos' size='5' value='0' /> <span style='font-weight:bold'>of " + str(len(cardDataList)) + "</span> "; newPosField += "<input type='button' value='Apply' onclick='directMove()' /> <span style='font-weight:bold'>Pin Top/Bottom? </span><input type='checkbox' id='anchor'/>"; upDownButtons = "<input type='button' value='Move Up' onclick='moveSelectedUp()'/><input type='button' value='Move Down' onclick='moveSelectedDown()'/>"; upDownButtons += "<input type='button' value='Select All' onclick='selectAll()'/><input type='button' value='Select None' onclick='selectNone()'/>"; html = "<html><head><script>" + getIRSchedulerDialogScript + "</script></head><body onLoad='buildCardData()'>"; html += "<p>" + newPosField; html += "<p>" + upDownButtons; html += "<div id='cardList'></div>"; html += "</body></html>"; w.stdHtml(html); bb = QDialogButtonBox(QDialogButtonBox.Close|QDialogButtonBox.Save) bb.connect(bb, SIGNAL("accepted()"), d, SLOT("accept()")) bb.connect(bb, SIGNAL("rejected()"), d, SLOT("reject()")) bb.setOrientation(QtCore.Qt.Horizontal); l.addWidget(bb) d.setLayout(l) d.setWindowModality(Qt.WindowModal) d.resize(500, 500) choice = d.exec_(); if(choice == 1): w.eval("updatePositions()"); else: if(currentCard != None): self.repositionCard(currentCard, -1);
def callIRSchedulerOptionsDialog(self): d = QDialog(self.mw) l = QVBoxLayout() l.setMargin(0) w = AnkiWebView() l.addWidget(w) #Add python object to take values back from javascript callback = IROptionsCallback(); w.page().mainFrame().addToJavaScriptWindowObject("callback", callback); getScript = """ function updateIRSchedulerOptions() { //invoke the callback object var soonTypeCnt = document.getElementById('soonCntButton').checked; var laterTypeCnt = document.getElementById('laterCntButton').checked; var soonRandom = document.getElementById('soonRandom').checked; var laterRandom = document.getElementById('laterRandom').checked; var options = '' //Soon Button if(soonTypeCnt) options += 'cnt,'; else options += 'pct,'; options += document.getElementById('soonValue').value + ','; if(soonRandom) options += 'true,'; else options += 'false,'; //Later Button if(laterTypeCnt) options += 'cnt,'; else options += 'pct,'; options += document.getElementById('laterValue').value + ','; if(laterRandom) options += 'true'; else options += 'false'; callback.updateOptions(options); }; """ isCntChecked = ''; isPctChecked = ''; isRandomChecked = ''; if(self.schedSoonType == 'cnt'): isCntChecked = 'checked'; isPctChecked = ''; else: isCntChecked = ''; isPctChecked = 'checked'; if(self.schedSoonRandom): isRandomChecked = 'checked'; else: isRandomChecked = ''; soonButtonConfig = "<span style='font-weight:bold'>Soon Button: </span>"; soonButtonConfig += "<input type='radio' id='soonCntButton' name='soonCntOrPct' value='cnt' " + isCntChecked + " /> Position "; soonButtonConfig += "<input type='radio' id='soonPctButton' name='soonCntOrPct' value='pct' " + isPctChecked + " /> Percent "; soonButtonConfig += "<input type='text' size='5' id='soonValue' value='" + str(self.schedSoonInt) + "'/>"; soonButtonConfig += "<span style='font-weight:bold'> Randomize? </span><input type='checkbox' id='soonRandom' " + isRandomChecked + " /><br/>"; if(self.schedLaterType == 'cnt'): isCntChecked = 'checked'; isPctChecked = ''; else: isCntChecked = ''; isPctChecked = 'checked'; if(self.schedLaterRandom): isRandomChecked = 'checked'; else: isRandomChecked = ''; laterButtonConfig = "<span style='font-weight:bold'>Later Button: </span>"; laterButtonConfig += "<input type='radio' id='laterCntButton' name='laterCntOrPct' value='cnt' " + isCntChecked + " /> Position "; laterButtonConfig += "<input type='radio' id='laterPctButton' name='laterCntOrPct' value='pct' " + isPctChecked + " /> Percent "; laterButtonConfig += "<input type='text' size='5' id='laterValue' value='" + str(self.schedLaterInt) + "'/>"; laterButtonConfig += "<span style='font-weight:bold'> Randomize? </span><input type='checkbox' id='laterRandom' " + isRandomChecked + " /><br/>"; html = "<html><head><script>" + getScript + "</script></head><body>"; html += "<p>" + soonButtonConfig; html += "<p>" + laterButtonConfig; html += "</body></html>"; w.stdHtml(html); bb = QDialogButtonBox(QDialogButtonBox.Close|QDialogButtonBox.Save) bb.connect(bb, SIGNAL("accepted()"), d, SLOT("accept()")) bb.connect(bb, SIGNAL("rejected()"), d, SLOT("reject()")) bb.setOrientation(QtCore.Qt.Horizontal); l.addWidget(bb) d.setLayout(l) d.setWindowModality(Qt.WindowModal) d.resize(500, 140) choice = d.exec_(); if(choice == 1): w.eval("updateIRSchedulerOptions()");
class AnnotateDialog(QDialog): def __init__(self, editor, name, path="", src="", create_new=False): QDialog.__init__(self, editor.widget, Qt.Window) # Compatibility: 2.1.0+ mw.setupDialogGC(self) self.editor_wv = editor.web self.editor = editor self.image_name = name self.image_path = path self.image_src = src self.create_new = create_new self.close_queued = False if not create_new: self.check_editor_image_selected() self.setupUI() def closeEvent(self, evt): if self.close_queued: save_geom(self, "anno_dial") del mw.annodial evt.accept() else: self.ask_on_close(evt) def setupUI(self): mainLayout = QVBoxLayout() self.setLayout(mainLayout) self.web = AnkiWebView(parent=self, title="Annotate Image") url = QUrl.fromLocalFile(method_draw_path) self.web._page = myPage(self.web._onBridgeCmd) self.web.setPage(self.web._page) self.web.setUrl(url) self.web.set_bridge_command(self.on_bridge_cmd, self) mainLayout.addWidget(self.web, stretch=1) btnLayout = QHBoxLayout() btnLayout.addStretch(1) replaceAll = QCheckBox("Replace All") self.replaceAll = replaceAll ch = get_config("replace_all", hidden=True, notexist=False) replaceAll.setCheckState(checked(ch)) replaceAll.stateChanged.connect(self.check_changed) btnLayout.addWidget(replaceAll) okButton = QPushButton("Save") okButton.clicked.connect(self.save) btnLayout.addWidget(okButton) cancelButton = QPushButton("Discard") cancelButton.clicked.connect(self.discard) btnLayout.addWidget(cancelButton) resetButton = QPushButton("Reset") resetButton.clicked.connect(self.reset) btnLayout.addWidget(resetButton) mainLayout.addLayout(btnLayout) self.setWindowTitle("Annotate Image") self.setMinimumWidth(100) self.setMinimumHeight(100) self.setGeometry(0, 0, 640, 640) geom = load_geom("anno_dial") if geom: self.restoreGeometry(geom) if not self.close_queued: # When image isn't selected js side self.show() def check_changed(self, state: int): set_config("replace_all", bool(state), hidden=True) def discard(self): self.close_queued = True self.close() def save(self): self.close_queued = True self.web.eval("ankiAddonSaveImg()") def reset(self): self.load_img() def on_bridge_cmd(self, cmd): if cmd == "img_src": if not self.create_new: self.load_img() elif cmd.startswith("svg_save:"): if self.create_new: svg_str = cmd[len("svg_save:"):] self.create_svg(svg_str) else: svg_str = cmd[len("svg_save:"):] self.save_svg(svg_str) def check_editor_image_selected(self): def check_image_selected(selected): if selected == False: self.close_queued = True self.close() tooltip("Image wasn't selected properly.\nPlease try again.") # Compatibility: 2.1.0+ self.editor_wv.evalWithCallback( "addonAnno.imageIsSelected()", check_image_selected) def load_img(self): img_path = self.image_path img_path_str = self.image_path.resolve().as_posix() img_format = img_path_str.split(".")[-1].lower() if img_format not in MIME_TYPE: tooltip("Image Not Supported", parent=self.editor.widget) return if img_format == "svg": img_data = base64.b64encode(img_path.read_text().encode("utf-8")).decode( "ascii" ) else: mime_str = MIME_TYPE[img_format] encoded_img_data = base64.b64encode(img_path.read_bytes()).decode() img_data = "data:{};base64,{}".format(mime_str, encoded_img_data) self.web.eval("ankiAddonSetImg('{}', '{}')".format( img_data, img_format)) def create_svg(self, svg_str): "When creating an image from nothing" # Compatibility: 2.1.0+ if COMPAT["write_data"]: new_name = mw.col.media.write_data( "svg_drawing.svg", svg_str.encode("utf-8")) else: new_name = mw.col.media.writeData( "svg_drawing.svg", svg_str.encode("utf-8")) img_el = '"<img src=\\"{}\\">"'.format(new_name) # Compatilibility: 2.1.0+ self.editor_wv.eval( "document.execCommand('inserthtml', false, {})".format(img_el)) self.create_new = False self.image_path = Path(mw.col.media.dir()) / new_name tooltip("Image Created", parent=self.editor.widget) if self.close_queued: self.close() def save_svg(self, svg_str): "When editing existing image" image_path = self.image_path.resolve().as_posix() img_name = self.image_name desired_name = ".".join(img_name.split(".")[:-1]) desired_name = desired_name[:15] if len( desired_name) > 15 else desired_name desired_name += ".svg" # remove whitespace and double quote as it messes with replace_all_img_src desired_name = desired_name.replace( " ", "").replace('"', "").replace("$", "") if not desired_name: desired_name = "blank" # Compatibility: 2.1.0+ if COMPAT["write_data"]: new_name = mw.col.media.write_data( desired_name, svg_str.encode("utf-8")) else: new_name = mw.col.media.writeData( desired_name, svg_str.encode("utf-8")) if self.replaceAll.checkState(): self.editor.saveNow(lambda s=self, i=img_name, n=new_name: s.replace_all_img_src(i, n)) else: self.replace_img_src(new_name) tooltip("Image Saved", parent=self.editor.widget) if self.close_queued: self.close() def replace_img_src(self, name: str): namestr = base64.b64encode(str(name).encode("utf-8")).decode("ascii") # Compatibility: 2.1.0+ self.editor_wv.eval("addonAnno.changeSrc('{}')".format(namestr)) def ask_on_close(self, evt): # Compatibility: 2.1.0+ opts = ["Cancel", "Discard", "Save"] diag = askUserDialog("Discard Changes?", opts, parent=self) diag.setDefault(0) ret = diag.run() if ret == opts[0]: evt.ignore() elif ret == opts[1]: evt.accept() elif ret == opts[2]: self.save() evt.ignore() def replace_all_img_src(self, orig_name: str, new_name: str): # Only run if mw.col.backend.find_and_replace exist (2.1.27+) browser = aqt.dialogs._dialogs["Browser"][1] if browser: browser.model.beginReset() cnt = self._replace_all_img_src(orig_name, new_name) mw.requireReset() if browser: browser.model.endReset() tooltip(f"Images across {cnt} note(s) modified", parent=self.editor.widget) def _replace_all_img_src(self, orig_name: str, new_name: str): "new_name doesn't have whitespace, dollar sign, nor double quote" orig_name = re.escape(orig_name) new_name = new_name # Compatibility: 2.1.0+ n = mw.col.findNotes("<img") # src element quoted case reg1 = r"""(?P<first><img[^>]* src=)(?:"{name}")|(?:'{name}')(?P<second>[^>]*>)""".format( name=orig_name ) # unquoted case reg2 = r"""(?P<first><img[^>]* src=){name}(?P<second>(?: [^>]*>)|>)""".format( name=orig_name ) img_regs = [reg1] if " " not in orig_name: img_regs.append(reg2) if COMPAT["find_replace"]: repl = """${first}"%s"${second}""" % new_name else: repl = """\\g<first>"%s"\\g<second>""" % new_name replaced_cnt = 0 for reg in img_regs: if COMPAT["find_replace"]: replaced_cnt += mw.col.backend.find_and_replace( nids=n, search=reg, replacement=repl, regex=True, match_case=False, field_name=None, ) else: replaced_cnt += anki.find.findReplace( col=mw.col, nids=n, src=reg, dst=repl, regex=True, fold=False) return replaced_cnt
def showAddCardQuickKeysDialog(self): #set values from lastDialogQuickKey or use default if (len(self.lastDialogQuickKey.keys()) < 1): self.setDefaultDialogValues(self.lastDialogQuickKey) d = QDialog(self.mw) l = QVBoxLayout() l.setMargin(0) w = AnkiWebView() l.addWidget(w) #Add python object to take values back from javascript quickKeyModel = QuickKeyModel() w.page().mainFrame().addToJavaScriptWindowObject( "quickKeyModel", quickKeyModel) #deck combo box deckComboBox = "<span style='font-weight:bold'>Deck: </span><select id='decks'>" allDecks = mw.col.decks.all() allDecks.sort(key=lambda dck: dck['name'], reverse=False) for deck in allDecks: isSelected = '' if (self.lastDialogQuickKey.get('deckName', None) == deck['name']): isSelected = 'selected' deckComboBox = deckComboBox + ( "<option value='" + str(deck['id']) + "' " + isSelected + ">" + deck['name'] + "</option>") deckComboBox = deckComboBox + "</select>" #model combo box fieldChooserByModel = {} modelComboBox = "<span style='font-weight:bold'>Model: </span><select id='models'>" allModels = mw.col.models.all() allModels.sort(key=lambda mod: mod['name'], reverse=False) for model in allModels: isSelected = '' if (self.lastDialogQuickKey.get('modelName', None) == model['name']): isSelected = 'selected' modelComboBox = modelComboBox + ( "<option value='" + str(model['id']) + "' " + isSelected + ">" + model['name'] + "</option>") listOfFields = model['flds'] fieldComboBox = "" for field in listOfFields: fieldComboBox = fieldComboBox + ("<option value='" + field['name'] + "'>" + field['name'] + "</option>") fieldChooserByModel[str(model['id'])] = fieldComboBox modelComboBox = modelComboBox + "</select>" ctrl = '' if (self.lastDialogQuickKey.get('ctrl', 1) == 1): ctrl = 'checked' shift = '' if (self.lastDialogQuickKey.get('shift', 0) == 1): shift = 'checked' alt = '' if (self.lastDialogQuickKey.get('alt', 0) == 1): alt = 'checked' #Ctrl checkbox ctrlCheckbox = "<span style='font-weight:bold'>Ctrl: </span><input type='checkbox' id='ctrl' " + ctrl + " />" #Shift checkbox shiftCheckbox = "<span style='font-weight:bold'>Shift: </span><input type='checkbox' id='shift' " + shift + "/>" #Alt checkbox altCheckbox = "<span style='font-weight:bold'>Alt: </span><input type='checkbox' id='alt' " + alt + "/>" #shortcut key combo box keyComboBox = "<span style='font-weight:bold'>Key: </span><select id='keys'>" isSelected = '' for val in range(0, 10): if (str(val) == str(self.lastDialogQuickKey.get('keyName', '0'))): isSelected = 'selected' keyComboBox = keyComboBox + ("<option value='" + str(val) + "' " + isSelected + ">" + str(val) + "</option>") isSelected = '' for code in range(ord('a'), ord('z') + 1): if (str(chr(code)) == str( self.lastDialogQuickKey.get('keyName', '0'))): isSelected = 'selected' keyComboBox = keyComboBox + ("<option value='" + chr(code) + "' " + isSelected + ">" + chr(code) + "</option>") isSelected = '' keyComboBox = keyComboBox + "</select>" #color text box colorValue = self.lastDialogQuickKey.get('color', 'yellow') colorTextField = "<span style='font-weight:bold'>Source highlighting color (IRead2 model only): </span><input type='text' id='color' value='" + colorValue + "' />" #radio buttons to chose if hilight or color text colorBackground = 'checked' colorText = '' if (self.lastDialogQuickKey.get('colorText', 'false') == 'true'): colorText = 'checked' colorBackground = '' colorBackOrText = "<span style='font-weight:bold'>Apply color to: </span><input type='radio' id='colorBackOrText' name='colorBackOrText' value='false' " + colorBackground + "/> Background <input type='radio' name='colorBackOrText' value='true' " + colorText + " /> Text<br />" #show editor checkbox doShowEditor = '' if (self.lastDialogQuickKey.get('showEditor', 1) == 1): doShowEditor = 'checked' showEditorCheckbox = "<span style='font-weight:bold'>Show Add Cards dialog?: </span><input type='checkbox' id='showEditor' " + doShowEditor + " />" #show current card editor checkbox doShowEditCurrent = '' if (self.lastDialogQuickKey.get('showEditCurrent', 0) == 1): doShowEditCurrent = 'checked' showEditCurrentCheckbox = "<span style='font-weight:bold'>Show Edit Current dialog?: </span><input type='checkbox' id='showEditCurrent' " + doShowEditCurrent + "/>" #remove shortcut checkbox doEnable = '' if (self.lastDialogQuickKey.get('enabled', 1) == 1): doEnable = 'checked' enabledCheckbox = "<span style='font-weight:bold'>Enable (uncheck to disable): </span><input type='checkbox' id='enabled' " + doEnable + " />" #javascript to populate field box based on selected model javascript = "var fieldsByModel = {};\n" for model in mw.col.models.all(): listOfFields = model['flds'] javascript += "fieldsByModel['" + model['name'] + "'] = [" for field in listOfFields: javascript += "'" + re.escape(field['name']) + "'," javascript = javascript[:-1] javascript += "];\n" javascript += """ function setFieldsForModel(mName) { var list = fieldsByModel[mName]; var options = ''; for(var i=0; i < list.length; i++) { var isSelected = ''; if(list[i] == pasteToFieldValue) isSelected = 'selected'; options += '<option value=\\'' + list[i] + '\\' ' + isSelected + '>' + list[i] + '</option>'; } document.getElementById('fields').innerHTML = options; } """ javascript += "var pasteToFieldValue = '" + str( self.lastDialogQuickKey.get('fieldName', '')) + "';\n" html = "<html><head><script>" + javascript + "</script></head><body>" html += deckComboBox + "<p>" html += modelComboBox html += "<p><span style='font-weight:bold'>Paste Text to Field: </span><select id='fields'>" html += fieldComboBox + "</select>" html += "<p><span style='font-weight:bold'>Key Combination:</span> " + ctrlCheckbox + " " + shiftCheckbox + " " + altCheckbox + " " + keyComboBox #html += "<p>" + keyComboBox; html += "<p>" + colorTextField html += "<p>" + colorBackOrText html += "<p>" + showEditorCheckbox html += "<p>" + showEditCurrentCheckbox html += "<p>" + enabledCheckbox html += "</body></html>" #print html; w.stdHtml(html) #Dynamically add the javascript hook to call the setFieldsForModel function addHooksScript = """ document.getElementById('models').onchange=function() { var sel = document.getElementById('models'); setFieldsForModel(sel.options[sel.selectedIndex].text); }; function getValues() { var sel = document.getElementById('decks'); quickKeyModel.setDeck(sel.options[sel.selectedIndex].text); sel = document.getElementById('models'); quickKeyModel.setModel(sel.options[sel.selectedIndex].text); sel = document.getElementById('fields'); quickKeyModel.setField(sel.options[sel.selectedIndex].text); sel = document.getElementById('ctrl'); quickKeyModel.setCtrl(sel.checked); sel = document.getElementById('shift'); quickKeyModel.setShift(sel.checked); sel = document.getElementById('alt'); quickKeyModel.setAlt(sel.checked); sel = document.getElementById('keys'); quickKeyModel.setKey(sel.options[sel.selectedIndex].text); quickKeyModel.setSourceHighlightColor(document.getElementById('color').value.trim()); sel = document.getElementById('colorBackOrText'); if(sel.checked) { quickKeyModel.setColorText('false'); } else { quickKeyModel.setColorText('true'); } sel = document.getElementById('showEditor'); quickKeyModel.setShowEditor(sel.checked); sel = document.getElementById('showEditCurrent'); quickKeyModel.setShowEditCurrent(sel.checked); sel = document.getElementById('enabled'); quickKeyModel.setEnabled(sel.checked); }; //Set the fields for the selected model var sel = document.getElementById('models'); setFieldsForModel(sel.options[sel.selectedIndex].text); """ w.eval(addHooksScript) bb = QDialogButtonBox(QDialogButtonBox.Close | QDialogButtonBox.Save) bb.connect(bb, SIGNAL("accepted()"), d, SLOT("accept()")) bb.connect(bb, SIGNAL("rejected()"), d, SLOT("reject()")) bb.setOrientation(QtCore.Qt.Horizontal) l.addWidget(bb) d.setLayout(l) d.setWindowModality(Qt.WindowModal) d.resize(700, 500) choice = d.exec_() w.eval("getValues()") #move values to a map so they can be serialized to file later (Qt objects don't pickle well) keyModel = {} keyModel['deckName'] = quickKeyModel.deckName keyModel['modelName'] = quickKeyModel.modelName keyModel['fieldName'] = quickKeyModel.fieldName #Ctrl + Shift + Alt + Key ctrl = 0 if (quickKeyModel.ctrl == 'true'): ctrl = 1 keyModel['ctrl'] = ctrl shift = 0 if (quickKeyModel.shift == 'true'): shift = 1 keyModel['shift'] = shift alt = 0 if (quickKeyModel.alt == 'true'): alt = 1 keyModel['alt'] = alt keyModel['keyName'] = quickKeyModel.keyName keyModel['color'] = quickKeyModel.color keyModel['colorText'] = quickKeyModel.colorText doShowEditor = 0 if (quickKeyModel.showEditor == 'true'): doShowEditor = 1 keyModel['showEditor'] = doShowEditor doShowEditCurrent = 0 if (quickKeyModel.showEditCurrent == 'true'): doShowEditCurrent = 1 keyModel['showEditCurrent'] = doShowEditCurrent keyModel['enabled'] = 1 if (quickKeyModel.enabled) else 0 #Save the last selected values in the dialog for later use self.lastDialogQuickKey = keyModel #If SAVE chosen, then save the model as a new shortcut if (choice == 1): self.setQuickKey(keyModel)