Esempio n. 1
0
 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_()
Esempio n. 2
0
 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_()
Esempio n. 3
0
    def __init__(
        self,
        mw: AnkiQt,
        nt: NotetypeDict,
        parent: Optional[QWidget] = None,
        open_at: int = 0,
    ) -> None:
        QDialog.__init__(self, parent or mw)
        mw.garbage_collect_on_dialog_finish(self)
        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.change_tracker = ChangeTracker(self.mw)

        self.setWindowTitle(
            without_unicode_isolation(
                tr.fields_fields_for(val=self.model["name"])))

        if os.getenv("ANKI_EXPERIMENTAL_FIELDS_WEB"):
            form = aqt.forms.fields_web.Ui_Dialog()
            form.setupUi(self)

            self.webview = form.webview
            self.webview.set_title("fields")

            self.show()
            self.refresh()
            self.webview.set_bridge_command(self._on_bridge_cmd, self)
            self.activateWindow()
            return

        self.form = aqt.forms.fields.Ui_Dialog()
        self.form.setupUi(self)
        self.webview = None

        disable_help_button(self)
        self.form.buttonBox.button(
            QDialogButtonBox.StandardButton.Help).setAutoDefault(False)
        self.form.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel).setAutoDefault(False)
        self.form.buttonBox.button(
            QDialogButtonBox.StandardButton.Save).setAutoDefault(False)
        self.currentIdx: Optional[int] = None
        self.fillFields()
        self.setupSignals()
        self.form.fieldList.setDragDropMode(
            QAbstractItemView.DragDropMode.InternalMove)
        self.form.fieldList.dropEvent = self.onDrop  # type: ignore[assignment]
        self.form.fieldList.setCurrentRow(open_at)
        self.exec()
Esempio n. 4
0
 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.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()
Esempio n. 5
0
 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")
     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()
Esempio n. 6
0
    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()
Esempio n. 7
0
def sticky_getter_and_setter(handled, message, context: Editor):
    if message == "get_stickies":
        model = context.note.model()
        stickies = [fld["sticky"] for fld in model["flds"]]

        return (True, stickies)

    cmd = message.split(":", 1)

    if cmd[0] in ["toggle_sticky"]:
        model = context.note.model()
        idx = int(cmd[1])

        fld = model["flds"][idx]

        change_tracker = ChangeTracker(context.mw)
        change_tracker.mark_basic()

        fld["sticky"] = not fld["sticky"]

        return (True, fld["sticky"])

    return handled
Esempio n. 8
0
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)
Esempio n. 9
0
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)
Esempio n. 10
0
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")
Esempio n. 11
0
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)
Esempio n. 12
0
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")