Exemple #1
0
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)
""")
Exemple #2
0
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()
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
 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: &nbsp;</span><input type='radio' id='colorBackOrText' name='colorBackOrText' value='false' checked='true' /> Background &nbsp;&nbsp;<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()");
Exemple #6
0
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)
Exemple #7
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)
Exemple #8
0
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
Exemple #9
0
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
Exemple #10
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")
Exemple #11
0
 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' />&nbsp;<span style='font-weight:bold'>of " + str(len(cardDataList)) + "</span>&nbsp;&nbsp;";
     newPosField += "<input type='button' value='Apply' onclick='directMove()' />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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);
Exemple #12
0
    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: &nbsp;</span>";
        soonButtonConfig += "<input type='radio' id='soonCntButton' name='soonCntOrPct' value='cnt' " + isCntChecked + " /> Position &nbsp;&nbsp;";
        soonButtonConfig += "<input type='radio' id='soonPctButton' name='soonCntOrPct' value='pct' " + isPctChecked + " /> Percent&nbsp;";
        soonButtonConfig += "<input type='text' size='5' id='soonValue' value='" + str(self.schedSoonInt) + "'/>";
        soonButtonConfig += "<span style='font-weight:bold'>&nbsp;&nbsp;&nbsp;&nbsp;Randomize?&nbsp;</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: &nbsp;</span>";
        laterButtonConfig += "<input type='radio' id='laterCntButton' name='laterCntOrPct' value='cnt' " + isCntChecked + " /> Position &nbsp;&nbsp;";
        laterButtonConfig += "<input type='radio'  id='laterPctButton' name='laterCntOrPct' value='pct' " + isPctChecked + " /> Percent&nbsp;";
        laterButtonConfig += "<input type='text' size='5' id='laterValue' value='" + str(self.schedLaterInt) + "'/>";
        laterButtonConfig += "<span style='font-weight:bold'>&nbsp;&nbsp;&nbsp;&nbsp;Randomize?&nbsp;</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()");
Exemple #13
0
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
Exemple #14
0
    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: &nbsp;</span><input type='radio' id='colorBackOrText' name='colorBackOrText' value='false' " + colorBackground + "/> Background &nbsp;&nbsp;<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>&nbsp;&nbsp;" + ctrlCheckbox + "&nbsp;&nbsp;" + shiftCheckbox + "&nbsp;&nbsp;" + altCheckbox + "&nbsp;&nbsp;" + 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)