Exemple #1
0
    def _ui_bottom_row(self):
        hbox = QHBoxLayout()

        buttons = QDialogButtonBox(Qt.Horizontal, self)

        # Button to check if content needs to be changed
        check_btn = buttons.addButton("&Check", QDialogButtonBox.ActionRole)
        check_btn.setToolTip("Check")
        check_btn.clicked.connect(lambda _: self.onCheck())

        # Button to generate diff of proposed content changes
        diff_btn = buttons.addButton("&Diff", QDialogButtonBox.ActionRole)
        diff_btn.setToolTip("Show diff")
        diff_btn.clicked.connect(lambda _: self.onDiff())

        # Button to make the proposed changes
        fix_btn = buttons.addButton("&Fix", QDialogButtonBox.ActionRole)
        fix_btn.setToolTip("Fix")
        fix_btn.clicked.connect(lambda _: self.onFix())

        # Button to close this dialog
        close_btn = buttons.addButton("&Close", QDialogButtonBox.RejectRole)
        close_btn.clicked.connect(self.close)

        hbox.addWidget(buttons)
        return hbox
    def _ui_bottom_row(self):
        hbox = QHBoxLayout()

        buttons = QDialogButtonBox(Qt.Horizontal, self)

        # Button to check if content needs to be changed
        check_btn = buttons.addButton("&Dry-run", QDialogButtonBox.ActionRole)
        check_btn.setToolTip("Dry-run")
        check_btn.clicked.connect(lambda _: self.onCheck(mode="dryrun"))

        # Button to diff the proposed changes
        diff_btn = buttons.addButton("&Diff", QDialogButtonBox.ActionRole)
        diff_btn.setToolTip("Diff")
        diff_btn.clicked.connect(lambda _: self.onCheck(mode="diff"))

        # Button to make the proposed changes
        fix_btn = buttons.addButton("&Update", QDialogButtonBox.ActionRole)
        fix_btn.setToolTip("Update")
        fix_btn.clicked.connect(lambda _: self.onCheck(mode="update"))

        # Button to close this dialog
        close_btn = buttons.addButton("&Close", QDialogButtonBox.RejectRole)
        close_btn.clicked.connect(self.close)

        hbox.addWidget(buttons)
        return hbox
Exemple #3
0
    def _ui_bottom_row(self):
        hbox = QHBoxLayout()
        buttons = QDialogButtonBox(Qt.Horizontal, self)

        # Button to export changelog to a CSV file
        export_btn = buttons.addButton("&Export full history",
                                       QDialogButtonBox.ActionRole)
        export_btn.setToolTip("Export full history to CSV")
        export_btn.clicked.connect(lambda _: self.onExport())

        # Button to close this dialog
        close_btn = buttons.addButton("&Close", QDialogButtonBox.RejectRole)
        close_btn.clicked.connect(self.close)

        hbox.addWidget(buttons)
        return hbox
Exemple #4
0
    def onCheckMediaDB(self):
        self.progress.start(immediate=True)
        (nohave, unused, warnings) = self.col.media.check()
        self.progress.finish()
        # generate report
        report = ""
        if warnings:
            report += "\n".join(warnings) + "\n"
        if unused:
            if report:
                report += "\n\n\n"
            report += _(
                "In media folder but not used by any cards:")
            report += "\n" + "\n".join(unused)
        if nohave:
            if report:
                report += "\n\n\n"
            report += _(
                "Used on cards but missing from media folder:")
            report += "\n" + "\n".join(nohave)
        if not report:
            tooltip(_("No unused or missing files found."))
            return
        # show report and offer to delete
        diag = QDialog(self)
        diag.setWindowTitle("Anki")
        layout = QVBoxLayout(diag)
        diag.setLayout(layout)
        text = QTextEdit()
        text.setReadOnly(True)
        text.setPlainText(report)
        layout.addWidget(text)
        box = QDialogButtonBox(QDialogButtonBox.Close)
        layout.addWidget(box)
        if unused:
            b = QPushButton(_("Delete Unused Files"))
            b.setAutoDefault(False)
            box.addButton(b, QDialogButtonBox.ActionRole)
            b.clicked.connect(
                lambda c, u=unused, d=diag: self.deleteUnused(u, d))

        box.rejected.connect(diag.reject)
        diag.setMinimumHeight(400)
        diag.setMinimumWidth(500)
        restoreGeom(diag, "checkmediadb")
        diag.exec_()
        saveGeom(diag, "checkmediadb")
Exemple #5
0
def showText(txt, parent=None, type="text", run=True, geomKey=None, \
        minWidth=500, minHeight=400, title="Anki", copyBtn=False):
    if not parent:
        parent = aqt.mw.app.activeWindow() or aqt.mw
    diag = QDialog(parent)
    diag.setWindowTitle(title)
    layout = QVBoxLayout(diag)
    diag.setLayout(layout)
    text = QTextBrowser()
    text.setOpenExternalLinks(True)
    if type == "text":
        text.setPlainText(txt)
    else:
        text.setHtml(txt)
    layout.addWidget(text)
    box = QDialogButtonBox(QDialogButtonBox.Close)
    layout.addWidget(box)
    if copyBtn:
        def onCopy():
            QApplication.clipboard().setText(text.toPlainText())
        btn = QPushButton(_("Copy to Clipboard"))
        btn.clicked.connect(onCopy)
        box.addButton(btn, QDialogButtonBox.ActionRole)
    def onReject():
        if geomKey:
            saveGeom(diag, geomKey)
        QDialog.reject(diag)
    box.rejected.connect(onReject)
    def onFinish():
        if geomKey:
            saveGeom(diag, geomKey)
    box.accepted.connect(onFinish)
    diag.setMinimumHeight(minHeight)
    diag.setMinimumWidth(minWidth)
    if geomKey:
        restoreGeom(diag, geomKey)
    if run:
        diag.exec_()
    else:
        return diag, box
Exemple #6
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 #7
0
class TagDialogExtended__BasicOrTagEdit(QDialog):
    def __init__(self, parent, tags, alltags):
        QDialog.__init__(self, parent,
                         Qt.WindowType.Window)  # super().__init__(parent)
        self.basic_mode = gc("dialog type: basic_but_quick")
        self.parent = parent
        self.alltags = alltags
        self.gridLayout = QGridLayout(self)
        self.gridLayout.setObjectName("gridLayout")
        self.label = QLabel("Edit tags:")
        self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
        self.verticalLayout = QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.gridLayout.addLayout(self.verticalLayout, 1, 0, 1, 1)
        self.buttonBox = QDialogButtonBox(self)
        self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
        self.buttonBox.setStandardButtons(
            QDialogButtonBox.StandardButton.Cancel
            | QDialogButtonBox.StandardButton.Ok)
        self.shortcut = QShortcut(QKeySequence("Ctrl+Return"), self)
        self.shortcut.activated.connect(self.accept)
        self.helpButton = QPushButton(
            "add empty line", clicked=lambda: self.maybe_add_line(force=True))
        self.buttonBox.addButton(self.helpButton,
                                 QDialogButtonBox.ButtonRole.HelpRole)
        self.filterbutton = QPushButton("edit tag for current line",
                                        clicked=self.tagselector)
        self.buttonBox.addButton(self.filterbutton,
                                 QDialogButtonBox.ButtonRole.ResetRole)
        self.searchButton = QPushButton(
            "search", clicked=lambda: self.do_browser_search(extra_search=""))
        self.buttonBox.addButton(self.searchButton,
                                 QDialogButtonBox.ButtonRole.ResetRole)
        self.gridLayout.addWidget(self.buttonBox, 2, 0, 1, 1)
        self.buttonBox.accepted.connect(self.accept)
        self.buttonBox.rejected.connect(self.reject)
        self.setWindowTitle("Anki - Edit Tags")
        originalheight = self.height()
        restoreGeom(self, "TagDialogExtended")
        self.resize(self.width(), originalheight)
        if not tags:
            tags = [
                "",
            ]
        else:
            tags.append("")
        self.line_list = []
        for t in tags:
            self.maybe_add_line(t)
        self.cut = gc("in tag lines dialog: open filterdialog for single tag")
        if self.cut:
            self.filterbutton.setToolTip('shortcut: {}'.format(self.cut))
            self.selkey = QShortcut(QKeySequence(self.cut), self)
            self.selkey.activated.connect(self.tagselector)
        self.browser_scut = gc("in tag lines dialog: search browser for tag")
        if self.browser_scut:
            self.searchButton.setToolTip('shortcut: {}'.format(
                self.browser_scut))
            self.browser_scut_key = QShortcut(QKeySequence(self.browser_scut),
                                              self)
            self.browser_scut_key.activated.connect(
                lambda: self.do_browser_search(extra_search=""))
        # don't also set Ctrl+t,a/gc("editor: show filterdialog to add single tag") for
        # self.tagselector: What if the user has already set them to the same etc. I'd have
        # to do a lot of checking
        self.addnl = gc("in tag lines dialog: insert additional line")
        if self.addnl:
            self.helpButton.setToolTip('shortcut: {}'.format(self.addnl))
            self.addnlscut = QShortcut(QKeySequence(self.addnl), self)
            self.addnlscut.activated.connect(
                lambda: self.maybe_add_line(force=True))

    def current_tags_list(self):
        return [t.text() for t in self.line_list if t]

    def do_browser_search(self, extra_search=""):
        # Use the current line's text or the last line if the current one is an empty line
        note_tags = self.current_tags_list()
        searched_tag = shared_variables.focused_line.text() or (
            note_tags[-1] if len(note_tags) > 0 else "")
        if searched_tag:
            browser = dialogs.open('Browser', mw)
            browser.setFilter('tag:"{}*" {}'.format(searched_tag,
                                                    extra_search))
            self.accept()
        else:
            tooltip("empty tag was selected for search")

    def tagselector(self):
        text = shared_variables.focused_line.text()
        d = FilterDialog(parent=self,
                         values=self.alltags,
                         allownew=True,
                         prefill=text)
        if d.exec():
            shared_variables.focused_line.setText(d.selkey)
        else:
            shared_variables.focused_line.setFocus()

    def change_focus_by_one(self, Down=True):
        for index, edit in enumerate(self.line_list):
            if edit == shared_variables.focused_line:
                if Down:
                    if index == len(
                            self.line_list) - 1:  # if in last line go up
                        self.line_list[0].setFocus()
                        break
                    else:
                        newidx = index + 1
                        self.line_list[newidx].setFocus()
                        break
                else:  # go up
                    if index == 0:  # if in last line go up
                        newidx = len(self.line_list) - 1
                        self.line_list[newidx].setFocus()
                        break
                    else:
                        self.line_list[index - 1].setFocus()
                        break

    def maybe_add_line(self, tag="", force=False):
        if self.line_list and not self.line_list[-1].text(
        ) and not force:  # last lineedit is empty:
            self.line_list[-1].setFocus()
            self.line_list[-1].setText(tag)
        else:
            if self.basic_mode:
                te = MyBasicEdit(self)
                te.setText(tag)
                self.verticalLayout.addWidget(te)
                te.setFocus()
                self.line_list.append(te)
            else:
                te = MyTagEdit(self)
                te.setCol(mw.col)
                te.setText(tag)
                self.verticalLayout.addWidget(te)
                te.hideCompleter()
                te.setFocus()
                self.line_list.append(te)

    def accept(self):
        self.tagstring = ""
        for t in self.line_list:
            if not self.basic_mode:
                t.hideCompleter()
            text = t.text()
            if text:
                self.tagstring += text + " "
        saveGeom(self, "TagDialogExtended")
        QDialog.accept(self)

    def reject(self):
        saveGeom(self, "TagDialogExtended")
        QDialog.reject(self)
Exemple #8
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