2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2020-02-17 01:18:20 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-12-20 10:19:03 +01:00
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import sys
|
2021-01-25 14:45:47 +01:00
|
|
|
from enum import Enum
|
2021-03-16 13:40:37 +01:00
|
|
|
from functools import wraps
|
2021-02-01 11:23:48 +01:00
|
|
|
from typing import (
|
|
|
|
TYPE_CHECKING,
|
|
|
|
Any,
|
|
|
|
Callable,
|
|
|
|
List,
|
|
|
|
Literal,
|
|
|
|
Optional,
|
|
|
|
Sequence,
|
|
|
|
Tuple,
|
|
|
|
Union,
|
|
|
|
cast,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-01-25 23:19:19 +01:00
|
|
|
from markdown import markdown
|
2021-02-01 11:23:48 +01:00
|
|
|
from PyQt5.QtWidgets import (
|
|
|
|
QAction,
|
|
|
|
QDialog,
|
|
|
|
QDialogButtonBox,
|
|
|
|
QFileDialog,
|
|
|
|
QHeaderView,
|
|
|
|
QMenu,
|
|
|
|
QPushButton,
|
|
|
|
QSplitter,
|
|
|
|
QWidget,
|
|
|
|
)
|
2021-01-25 23:19:19 +01:00
|
|
|
|
2020-02-23 05:57:02 +01:00
|
|
|
import anki
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt
|
2021-02-01 11:23:48 +01:00
|
|
|
from anki import Collection
|
2021-01-31 06:55:08 +01:00
|
|
|
from anki.errors import InvalidInput
|
|
|
|
from anki.lang import TR # pylint: disable=unused-import
|
2019-12-23 01:34:10 +01:00
|
|
|
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
|
2019-12-20 10:19:03 +01:00
|
|
|
from aqt.qt import *
|
2020-01-23 22:55:14 +01:00
|
|
|
from aqt.theme import theme_manager
|
2019-12-20 10:19:03 +01:00
|
|
|
|
2020-05-27 01:14:02 +02:00
|
|
|
if TYPE_CHECKING:
|
2021-01-27 05:22:17 +01:00
|
|
|
TextFormat = Union[Literal["plain", "rich"]]
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-02 10:43:19 +01:00
|
|
|
def aqt_data_folder() -> str:
|
2020-11-01 05:26:58 +01:00
|
|
|
# running in place?
|
2020-11-03 23:44:47 +01:00
|
|
|
dir = os.path.join(os.path.dirname(__file__), "data")
|
2020-11-01 05:26:58 +01:00
|
|
|
if os.path.exists(dir):
|
2020-11-03 23:44:47 +01:00
|
|
|
return dir
|
|
|
|
# packaged install?
|
2021-02-04 11:28:25 +01:00
|
|
|
if isMac:
|
|
|
|
dir2 = os.path.join(sys.prefix, "..", "Resources", "aqt_data")
|
|
|
|
else:
|
|
|
|
dir2 = os.path.join(sys.prefix, "aqt_data")
|
2020-11-01 05:38:13 +01:00
|
|
|
if os.path.exists(dir2):
|
|
|
|
return dir2
|
2020-11-03 23:44:47 +01:00
|
|
|
|
|
|
|
# should only happen when running unit tests
|
|
|
|
print("warning, data folder not found")
|
|
|
|
return "."
|
2020-01-02 10:43:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
def locale_dir() -> str:
|
|
|
|
return os.path.join(aqt_data_folder(), "locale")
|
|
|
|
|
|
|
|
|
2021-02-03 04:31:46 +01:00
|
|
|
def tr(key: TR.V, **kwargs: Union[str, int, float]) -> str:
|
2020-02-23 05:57:02 +01:00
|
|
|
"Shortcut to access Fluent translations."
|
2020-02-24 09:37:02 +01:00
|
|
|
return anki.lang.current_i18n.translate(key, **kwargs)
|
2020-02-17 01:18:20 +01:00
|
|
|
|
|
|
|
|
2021-01-25 14:45:47 +01:00
|
|
|
class HelpPage(Enum):
|
|
|
|
NOTE_TYPE = "getting-started?id=note-types"
|
|
|
|
BROWSING = "browsing"
|
|
|
|
BROWSING_FIND_AND_REPLACE = "browsing?id=find-and-replace"
|
|
|
|
BROWSING_OTHER_MENU_ITEMS = "browsing?id=other-menu-items"
|
|
|
|
KEYBOARD_SHORTCUTS = "studying?id=keyboard-shortcuts"
|
|
|
|
EDITING = "editing"
|
|
|
|
ADDING_CARD_AND_NOTE = "editing?id=adding-cards-and-notes"
|
|
|
|
ADDING_A_NOTE_TYPE = "editing?id=adding-a-note-type"
|
|
|
|
LATEX = "math?id=latex"
|
|
|
|
PREFERENCES = "preferences"
|
|
|
|
INDEX = ""
|
|
|
|
TEMPLATES = "templates/intro"
|
|
|
|
FILTERED_DECK = "filtered-decks"
|
|
|
|
IMPORTING = "importing"
|
|
|
|
CUSTOMIZING_FIELDS = "editing?id=customizing-fields"
|
|
|
|
DECK_OPTIONS = "deck-options"
|
|
|
|
EDITING_FEATURES = "editing?id=features"
|
|
|
|
|
|
|
|
|
|
|
|
HelpPageArgument = Optional[Union[HelpPage, str]]
|
|
|
|
"""This type represents what can be used as argument expecting a specific help page. Anki code should use HelpPage as
|
|
|
|
argument. However, add-on may use string, and we want to accept this.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def openHelp(section: HelpPageArgument) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
link = aqt.appHelpSite
|
|
|
|
if section:
|
2021-01-25 14:45:47 +01:00
|
|
|
if isinstance(section, HelpPage):
|
|
|
|
link += section.value
|
|
|
|
else:
|
|
|
|
link += section
|
2012-12-21 08:51:59 +01:00
|
|
|
openLink(link)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def openLink(link: str) -> None:
|
2020-11-17 08:42:43 +01:00
|
|
|
tooltip(tr(TR.QT_MISC_LOADING), period=1000)
|
2017-06-27 04:04:42 +02:00
|
|
|
with noBundledLibs():
|
|
|
|
QDesktopServices.openUrl(QUrl(link))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-27 05:22:17 +01:00
|
|
|
def showWarning(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
|
|
|
parent: Optional[QDialog] = None,
|
|
|
|
help: HelpPageArgument = "",
|
|
|
|
title: str = "Anki",
|
|
|
|
textFormat: Optional[TextFormat] = None,
|
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small warning with an OK button."
|
2019-02-26 00:36:02 +01:00
|
|
|
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-27 05:22:17 +01:00
|
|
|
def showCritical(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
|
|
|
parent: Optional[QDialog] = None,
|
|
|
|
help: str = "",
|
|
|
|
title: str = "Anki",
|
|
|
|
textFormat: Optional[TextFormat] = None,
|
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small critical error with an OK button."
|
2019-02-26 00:36:02 +01:00
|
|
|
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
def show_invalid_search_error(err: Exception, parent: Optional[QDialog] = None) -> None:
|
2021-01-25 23:19:19 +01:00
|
|
|
"Render search errors in markdown, then display a warning."
|
|
|
|
text = str(err)
|
|
|
|
if isinstance(err, InvalidInput):
|
|
|
|
text = markdown(text)
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
showWarning(text, parent=parent)
|
2021-01-25 23:19:19 +01:00
|
|
|
|
|
|
|
|
2020-01-03 17:48:17 +01:00
|
|
|
def showInfo(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
|
|
|
parent: Union[Literal[False], QDialog] = False,
|
|
|
|
help: HelpPageArgument = "",
|
|
|
|
type: str = "info",
|
|
|
|
title: str = "Anki",
|
2021-01-27 05:22:17 +01:00
|
|
|
textFormat: Optional[TextFormat] = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
customBtns: Optional[List[QMessageBox.StandardButton]] = None,
|
2021-01-27 05:22:17 +01:00
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small info window with an OK button."
|
2021-02-01 11:23:48 +01:00
|
|
|
parent_widget: QWidget
|
2012-12-21 08:51:59 +01:00
|
|
|
if parent is False:
|
2021-02-01 11:23:48 +01:00
|
|
|
parent_widget = aqt.mw.app.activeWindow() or aqt.mw
|
|
|
|
else:
|
|
|
|
parent_widget = parent
|
2012-12-21 08:51:59 +01:00
|
|
|
if type == "warning":
|
|
|
|
icon = QMessageBox.Warning
|
|
|
|
elif type == "critical":
|
|
|
|
icon = QMessageBox.Critical
|
|
|
|
else:
|
|
|
|
icon = QMessageBox.Information
|
2021-02-01 11:23:48 +01:00
|
|
|
mb = QMessageBox(parent_widget) #
|
2019-02-26 00:36:02 +01:00
|
|
|
if textFormat == "plain":
|
|
|
|
mb.setTextFormat(Qt.PlainText)
|
|
|
|
elif textFormat == "rich":
|
|
|
|
mb.setTextFormat(Qt.RichText)
|
2019-02-27 05:16:35 +01:00
|
|
|
elif textFormat is not None:
|
2019-02-26 00:36:02 +01:00
|
|
|
raise Exception("unexpected textFormat type")
|
2012-12-21 08:51:59 +01:00
|
|
|
mb.setText(text)
|
|
|
|
mb.setIcon(icon)
|
2016-04-30 07:44:41 +02:00
|
|
|
mb.setWindowTitle(title)
|
2020-01-03 17:48:17 +01:00
|
|
|
if customBtns:
|
|
|
|
default = None
|
|
|
|
for btn in customBtns:
|
|
|
|
b = mb.addButton(btn)
|
|
|
|
if not default:
|
|
|
|
default = b
|
|
|
|
mb.setDefaultButton(default)
|
|
|
|
else:
|
|
|
|
b = mb.addButton(QMessageBox.Ok)
|
|
|
|
b.setDefault(True)
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
|
|
|
b = mb.addButton(QMessageBox.Help)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.clicked, lambda: openHelp(help))
|
2012-12-21 08:51:59 +01:00
|
|
|
b.setAutoDefault(False)
|
|
|
|
return mb.exec_()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
def showText(
|
2021-02-01 11:23:48 +01:00
|
|
|
txt: str,
|
|
|
|
parent: Optional[QWidget] = None,
|
|
|
|
type: str = "text",
|
|
|
|
run: bool = True,
|
|
|
|
geomKey: Optional[str] = None,
|
|
|
|
minWidth: int = 500,
|
|
|
|
minHeight: int = 400,
|
|
|
|
title: str = "Anki",
|
|
|
|
copyBtn: bool = False,
|
2021-02-02 06:47:51 +01:00
|
|
|
plain_text_edit: bool = False,
|
2021-02-01 11:23:48 +01:00
|
|
|
) -> Optional[Tuple[QDialog, QDialogButtonBox]]:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
|
|
|
diag = QDialog(parent)
|
2016-04-30 07:44:41 +02:00
|
|
|
diag.setWindowTitle(title)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(diag)
|
2012-12-21 08:51:59 +01:00
|
|
|
layout = QVBoxLayout(diag)
|
|
|
|
diag.setLayout(layout)
|
2021-02-02 06:47:51 +01:00
|
|
|
if plain_text_edit:
|
|
|
|
# used by the importer
|
|
|
|
text = QPlainTextEdit()
|
|
|
|
text.setReadOnly(True)
|
|
|
|
text.setWordWrapMode(QTextOption.NoWrap)
|
|
|
|
else:
|
|
|
|
text = QTextBrowser()
|
|
|
|
text.setOpenExternalLinks(True)
|
2012-12-21 08:51:59 +01:00
|
|
|
if type == "text":
|
|
|
|
text.setPlainText(txt)
|
|
|
|
else:
|
|
|
|
text.setHtml(txt)
|
|
|
|
layout.addWidget(text)
|
|
|
|
box = QDialogButtonBox(QDialogButtonBox.Close)
|
|
|
|
layout.addWidget(box)
|
2019-02-16 23:05:06 +01:00
|
|
|
if copyBtn:
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def onCopy() -> None:
|
2019-02-16 23:05:06 +01:00
|
|
|
QApplication.clipboard().setText(text.toPlainText())
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-11-17 08:42:43 +01:00
|
|
|
btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD))
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(btn.clicked, onCopy)
|
2019-02-16 23:05:06 +01:00
|
|
|
box.addButton(btn, QDialogButtonBox.ActionRole)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def onReject() -> None:
|
2014-06-18 20:47:45 +02:00
|
|
|
if geomKey:
|
|
|
|
saveGeom(diag, geomKey)
|
|
|
|
QDialog.reject(diag)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(box.rejected, onReject)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def onFinish() -> None:
|
2017-04-26 22:25:16 +02:00
|
|
|
if geomKey:
|
|
|
|
saveGeom(diag, geomKey)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(box.accepted, onFinish)
|
2016-04-30 07:44:41 +02:00
|
|
|
diag.setMinimumHeight(minHeight)
|
|
|
|
diag.setMinimumWidth(minWidth)
|
2014-06-18 20:47:45 +02:00
|
|
|
if geomKey:
|
|
|
|
restoreGeom(diag, geomKey)
|
2012-12-21 08:51:59 +01:00
|
|
|
if run:
|
|
|
|
diag.exec_()
|
2021-02-01 11:23:48 +01:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
return diag, box
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-25 14:45:47 +01:00
|
|
|
def askUser(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
|
|
|
parent: QDialog = None,
|
2021-01-25 14:45:47 +01:00
|
|
|
help: HelpPageArgument = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
defaultno: bool = False,
|
|
|
|
msgfunc: Optional[Callable] = None,
|
|
|
|
title: str = "Anki",
|
|
|
|
) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a yes/no question. Return true if yes."
|
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow()
|
|
|
|
if not msgfunc:
|
|
|
|
msgfunc = QMessageBox.question
|
|
|
|
sb = QMessageBox.Yes | QMessageBox.No
|
|
|
|
if help:
|
|
|
|
sb |= QMessageBox.Help
|
|
|
|
while 1:
|
|
|
|
if defaultno:
|
|
|
|
default = QMessageBox.No
|
|
|
|
else:
|
|
|
|
default = QMessageBox.Yes
|
2021-02-01 11:23:48 +01:00
|
|
|
r = msgfunc(parent, title, text, cast(QMessageBox.StandardButtons, sb), default)
|
2012-12-21 08:51:59 +01:00
|
|
|
if r == QMessageBox.Help:
|
|
|
|
|
|
|
|
openHelp(help)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
return r == QMessageBox.Yes
|
|
|
|
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class ButtonedDialog(QMessageBox):
|
2021-01-25 14:45:47 +01:00
|
|
|
def __init__(
|
2021-02-01 11:23:48 +01:00
|
|
|
self,
|
|
|
|
text: str,
|
|
|
|
buttons: List[str],
|
|
|
|
parent: Optional[QDialog] = None,
|
|
|
|
help: HelpPageArgument = None,
|
|
|
|
title: str = "Anki",
|
2021-01-25 14:45:47 +01:00
|
|
|
):
|
2019-03-04 06:59:53 +01:00
|
|
|
QMessageBox.__init__(self, parent)
|
2021-02-01 11:23:48 +01:00
|
|
|
self._buttons: List[QPushButton] = []
|
2016-04-30 07:44:41 +02:00
|
|
|
self.setWindowTitle(title)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.help = help
|
|
|
|
self.setIcon(QMessageBox.Warning)
|
|
|
|
self.setText(text)
|
|
|
|
for b in buttons:
|
2020-08-02 02:25:48 +02:00
|
|
|
self._buttons.append(self.addButton(b, QMessageBox.AcceptRole))
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
2020-11-17 08:42:43 +01:00
|
|
|
self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole)
|
|
|
|
buttons.append(tr(TR.ACTIONS_HELP))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def run(self) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.exec_()
|
|
|
|
but = self.clickedButton().text()
|
|
|
|
if but == "Help":
|
|
|
|
# FIXME stop dialog closing?
|
|
|
|
openHelp(self.help)
|
2017-07-31 07:48:34 +02:00
|
|
|
txt = self.clickedButton().text()
|
|
|
|
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
|
|
|
|
return txt.replace("&", "")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def setDefault(self, idx: int) -> None:
|
2020-08-02 02:25:48 +02:00
|
|
|
self.setDefaultButton(self._buttons[idx])
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-25 14:45:47 +01:00
|
|
|
def askUserDialog(
|
2021-02-01 11:23:48 +01:00
|
|
|
text: str,
|
|
|
|
buttons: List[str],
|
|
|
|
parent: Optional[QDialog] = None,
|
|
|
|
help: HelpPageArgument = None,
|
|
|
|
title: str = "Anki",
|
|
|
|
) -> ButtonedDialog:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw
|
2016-04-30 07:44:41 +02:00
|
|
|
diag = ButtonedDialog(text, buttons, parent, help, title=title)
|
2012-12-21 08:51:59 +01:00
|
|
|
return diag
|
|
|
|
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class GetTextDialog(QDialog):
|
|
|
|
def __init__(
|
|
|
|
self,
|
2021-02-01 11:23:48 +01:00
|
|
|
parent: Optional[QDialog],
|
|
|
|
question: str,
|
2021-01-25 14:45:47 +01:00
|
|
|
help: HelpPageArgument = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
edit: Optional[QLineEdit] = None,
|
|
|
|
default: str = "",
|
|
|
|
title: str = "Anki",
|
|
|
|
minWidth: int = 400,
|
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.__init__(self, parent)
|
|
|
|
self.setWindowTitle(title)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.question = question
|
|
|
|
self.help = help
|
|
|
|
self.qlabel = QLabel(question)
|
2016-04-30 07:44:41 +02:00
|
|
|
self.setMinimumWidth(minWidth)
|
2012-12-21 08:51:59 +01:00
|
|
|
v = QVBoxLayout()
|
|
|
|
v.addWidget(self.qlabel)
|
|
|
|
if not edit:
|
|
|
|
edit = QLineEdit()
|
|
|
|
self.l = edit
|
|
|
|
if default:
|
|
|
|
self.l.setText(default)
|
|
|
|
self.l.selectAll()
|
|
|
|
v.addWidget(self.l)
|
|
|
|
buts = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
|
|
|
if help:
|
|
|
|
buts |= QDialogButtonBox.Help
|
2020-08-02 02:25:48 +02:00
|
|
|
b = QDialogButtonBox(buts) # type: ignore
|
2012-12-21 08:51:59 +01:00
|
|
|
v.addWidget(b)
|
|
|
|
self.setLayout(v)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.button(QDialogButtonBox.Ok).clicked, self.accept)
|
|
|
|
qconnect(b.button(QDialogButtonBox.Cancel).clicked, self.reject)
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def accept(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
return QDialog.accept(self)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def reject(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
return QDialog.reject(self)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def helpRequested(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
openHelp(self.help)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
def getText(
|
2021-02-01 11:23:48 +01:00
|
|
|
prompt: str,
|
|
|
|
parent: Optional[QDialog] = None,
|
2021-01-25 14:45:47 +01:00
|
|
|
help: HelpPageArgument = None,
|
2021-02-01 11:23:48 +01:00
|
|
|
edit: Optional[QLineEdit] = None,
|
|
|
|
default: str = "",
|
|
|
|
title: str = "Anki",
|
|
|
|
geomKey: Optional[str] = None,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> Tuple[str, int]:
|
Rework reschedule tool
The old rescheduling dialog's two options have been split into two
separate menu items, "Forget", and "Set Due Date"
For cards that are not review cards, "Set Due Date" behaves like the
old reschedule option, changing the cards into a review card, and
and setting both the interval and due date to the provided number of
days.
When "Set Due Date" is applied to a review card, it no longer resets
the card's interval. Instead, it looks at how much the provided number
of days will change the original interval, and adjusts the interval by
that amount, so that cards that are answered earlier receive a smaller
next interval, and cards that are answered after a longer delay receive
a bonus.
For example, imagine a card was answered on day 5, and given an interval
of 10 days, so it has a due date of day 15.
- if on day 10 the due date is changed to day 12 (today+2), the card
is being scheduled 3 days earlier than it was supposed to be, so the
interval will be adjusted to 7 days.
- and if on day 10 the due date is changed to day 20, the interval will
be changed from 10 days to 15 days.
There is no separate option to reset the interval of a review card, but
it can be accomplished by forgetting the card(s), and then setting the
desired due date.
Other notes:
- Added the action to the review screen as well.
- Set the shortcut to Ctrl+Shift+D, and changed the existing Delete
Tags shortcut to Ctrl+Alt+Shift+A.
2021-02-07 11:58:16 +01:00
|
|
|
"Returns (string, succeeded)."
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
2019-12-23 01:34:10 +01:00
|
|
|
d = GetTextDialog(
|
|
|
|
parent, prompt, help=help, edit=edit, default=default, title=title, **kwargs
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.setWindowModality(Qt.WindowModal)
|
2017-05-03 10:55:24 +02:00
|
|
|
if geomKey:
|
|
|
|
restoreGeom(d, geomKey)
|
2012-12-21 08:51:59 +01:00
|
|
|
ret = d.exec_()
|
2017-05-03 10:55:24 +02:00
|
|
|
if geomKey and ret:
|
|
|
|
saveGeom(d, geomKey)
|
2016-05-12 06:45:35 +02:00
|
|
|
return (str(d.l.text()), ret)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getOnlyText(*args: Any, **kwargs: Any) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
(s, r) = getText(*args, **kwargs)
|
|
|
|
if r:
|
|
|
|
return s
|
|
|
|
else:
|
2016-05-12 06:45:35 +02:00
|
|
|
return ""
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# fixme: these utilities could be combined into a single base class
|
2021-02-01 11:23:48 +01:00
|
|
|
# unused by Anki, but used by add-ons
|
|
|
|
def chooseList(
|
|
|
|
prompt: str, choices: List[str], startrow: int = 0, parent: Any = None
|
|
|
|
) -> int:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow()
|
|
|
|
d = QDialog(parent)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(d)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.setWindowModality(Qt.WindowModal)
|
|
|
|
l = QVBoxLayout()
|
|
|
|
d.setLayout(l)
|
|
|
|
t = QLabel(prompt)
|
|
|
|
l.addWidget(t)
|
|
|
|
c = QListWidget()
|
|
|
|
c.addItems(choices)
|
|
|
|
c.setCurrentRow(startrow)
|
|
|
|
l.addWidget(c)
|
|
|
|
bb = QDialogButtonBox(QDialogButtonBox.Ok)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(bb.accepted, d.accept)
|
2012-12-21 08:51:59 +01:00
|
|
|
l.addWidget(bb)
|
|
|
|
d.exec_()
|
|
|
|
return c.currentRow()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getTag(
|
|
|
|
parent: QDialog, deck: Collection, question: str, **kwargs: Any
|
|
|
|
) -> Tuple[str, int]:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.tagedit import TagEdit
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
te = TagEdit(parent)
|
|
|
|
te.setCol(deck)
|
2019-12-23 01:34:10 +01:00
|
|
|
ret = getText(question, parent, edit=te, geomKey="getTag", **kwargs)
|
2012-12-21 08:51:59 +01:00
|
|
|
te.hideCompleter()
|
|
|
|
return ret
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-01-07 05:24:49 +01:00
|
|
|
def disable_help_button(widget: QWidget) -> None:
|
|
|
|
"Disable the help button in the window titlebar."
|
|
|
|
flags = cast(Qt.WindowType, widget.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
|
|
|
widget.setWindowFlags(flags)
|
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# File handling
|
|
|
|
######################################################################
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getFile(
|
|
|
|
parent: QDialog,
|
|
|
|
title: str,
|
2021-02-02 14:30:53 +01:00
|
|
|
# single file returned unless multi=True
|
2021-02-01 11:23:48 +01:00
|
|
|
cb: Optional[Callable[[Union[str, Sequence[str]]], None]],
|
|
|
|
filter: str = "*.*",
|
|
|
|
dir: Optional[str] = None,
|
|
|
|
key: Optional[str] = None,
|
|
|
|
multi: bool = False, # controls whether a single or multiple files is returned
|
|
|
|
) -> Optional[Union[Sequence[str], str]]:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Ask the user for a file."
|
|
|
|
assert not dir or not key
|
|
|
|
if not dir:
|
2021-02-11 01:09:06 +01:00
|
|
|
dirkey = f"{key}Directory"
|
2012-12-21 08:51:59 +01:00
|
|
|
dir = aqt.mw.pm.profile.get(dirkey, "")
|
|
|
|
else:
|
|
|
|
dirkey = None
|
|
|
|
d = QFileDialog(parent)
|
2019-02-18 07:10:43 +01:00
|
|
|
mode = QFileDialog.ExistingFiles if multi else QFileDialog.ExistingFile
|
|
|
|
d.setFileMode(mode)
|
2016-03-21 00:57:45 +01:00
|
|
|
if os.path.exists(dir):
|
|
|
|
d.setDirectory(dir)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.setWindowTitle(title)
|
|
|
|
d.setNameFilter(filter)
|
|
|
|
ret = []
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def accept() -> None:
|
2019-02-18 07:10:43 +01:00
|
|
|
files = list(d.selectedFiles())
|
2012-12-21 08:51:59 +01:00
|
|
|
if dirkey:
|
2019-02-18 07:10:43 +01:00
|
|
|
dir = os.path.dirname(files[0])
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.mw.pm.profile[dirkey] = dir
|
2019-02-18 07:10:43 +01:00
|
|
|
result = files if multi else files[0]
|
2012-12-21 08:51:59 +01:00
|
|
|
if cb:
|
2019-02-18 07:10:43 +01:00
|
|
|
cb(result)
|
|
|
|
ret.append(result)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(d.accepted, accept)
|
2018-07-23 05:57:17 +02:00
|
|
|
if key:
|
|
|
|
restoreState(d, key)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.exec_()
|
2018-07-23 05:57:17 +02:00
|
|
|
if key:
|
|
|
|
saveState(d, key)
|
2021-02-01 11:23:48 +01:00
|
|
|
return ret[0] if ret else None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def getSaveFile(
|
|
|
|
parent: QDialog,
|
|
|
|
title: str,
|
|
|
|
dir_description: str,
|
|
|
|
key: str,
|
|
|
|
ext: str,
|
|
|
|
fname: Optional[str] = None,
|
|
|
|
) -> str:
|
2013-05-18 18:24:53 +02:00
|
|
|
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
2013-05-21 04:53:02 +02:00
|
|
|
variable. The file dialog will default to open with FNAME."""
|
2021-02-11 01:09:06 +01:00
|
|
|
config_key = f"{dir_description}Directory"
|
2017-09-10 08:42:29 +02:00
|
|
|
|
2017-09-10 09:01:52 +02:00
|
|
|
defaultPath = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
|
|
|
|
base = aqt.mw.pm.profile.get(config_key, defaultPath)
|
2013-05-21 04:53:02 +02:00
|
|
|
path = os.path.join(base, fname)
|
2016-05-31 10:51:40 +02:00
|
|
|
file = QFileDialog.getSaveFileName(
|
2019-12-23 01:34:10 +01:00
|
|
|
parent,
|
|
|
|
title,
|
|
|
|
path,
|
2021-02-11 00:37:38 +01:00
|
|
|
f"{key} (*{ext})",
|
2019-12-23 01:34:10 +01:00
|
|
|
options=QFileDialog.DontConfirmOverwrite,
|
|
|
|
)[0]
|
2012-12-21 08:51:59 +01:00
|
|
|
if file:
|
|
|
|
# add extension
|
|
|
|
if not file.lower().endswith(ext):
|
|
|
|
file += ext
|
|
|
|
# save new default
|
|
|
|
dir = os.path.dirname(file)
|
2013-05-18 18:24:53 +02:00
|
|
|
aqt.mw.pm.profile[config_key] = dir
|
2012-12-21 08:51:59 +01:00
|
|
|
# check if it exists
|
|
|
|
if os.path.exists(file):
|
2020-11-17 08:42:43 +01:00
|
|
|
if not askUser(tr(TR.QT_MISC_THIS_FILE_EXISTS_ARE_YOU_SURE), parent):
|
2012-12-21 08:51:59 +01:00
|
|
|
return None
|
|
|
|
return file
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def saveGeom(widget: QDialog, key: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
key += "Geom"
|
2018-08-08 04:30:58 +02:00
|
|
|
if isMac and widget.windowState() & Qt.WindowFullScreen:
|
|
|
|
geom = None
|
|
|
|
else:
|
|
|
|
geom = widget.saveGeometry()
|
|
|
|
aqt.mw.pm.profile[key] = geom
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restoreGeom(
|
|
|
|
widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False
|
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
key += "Geom"
|
|
|
|
if aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreGeometry(aqt.mw.pm.profile[key])
|
|
|
|
if isMac and offset:
|
2013-05-17 08:17:04 +02:00
|
|
|
if qtminor > 6:
|
2012-12-21 08:51:59 +01:00
|
|
|
# bug in osx toolkit
|
|
|
|
s = widget.size()
|
2019-12-23 01:34:10 +01:00
|
|
|
widget.resize(s.width(), s.height() + offset * 2)
|
2019-02-11 22:49:35 +01:00
|
|
|
ensureWidgetInScreenBoundaries(widget)
|
2014-06-18 20:47:45 +02:00
|
|
|
else:
|
|
|
|
if adjustSize:
|
|
|
|
widget.adjustSize()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
2019-02-11 22:49:35 +01:00
|
|
|
handle = widget.window().windowHandle()
|
|
|
|
if not handle:
|
|
|
|
# window has not yet been shown, retry later
|
|
|
|
aqt.mw.progress.timer(50, lambda: ensureWidgetInScreenBoundaries(widget), False)
|
|
|
|
return
|
|
|
|
|
2019-02-14 04:47:44 +01:00
|
|
|
# ensure widget is smaller than screen bounds
|
2019-02-11 22:49:35 +01:00
|
|
|
geom = handle.screen().availableGeometry()
|
2019-02-14 04:47:44 +01:00
|
|
|
wsize = widget.size()
|
|
|
|
cappedWidth = min(geom.width(), wsize.width())
|
|
|
|
cappedHeight = min(geom.height(), wsize.height())
|
|
|
|
if cappedWidth > wsize.width() or cappedHeight > wsize.height():
|
|
|
|
widget.resize(QSize(cappedWidth, cappedHeight))
|
|
|
|
|
|
|
|
# ensure widget is inside top left
|
|
|
|
wpos = widget.pos()
|
|
|
|
x = max(geom.x(), wpos.x())
|
|
|
|
y = max(geom.y(), wpos.y())
|
|
|
|
# and bottom right
|
2019-12-23 01:34:10 +01:00
|
|
|
x = min(x, geom.width() + geom.x() - cappedWidth)
|
|
|
|
y = min(y, geom.height() + geom.y() - cappedHeight)
|
2019-02-14 04:47:44 +01:00
|
|
|
if x != wpos.x() or y != wpos.y():
|
2019-02-11 22:49:35 +01:00
|
|
|
widget.move(x, y)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def saveState(widget: QFileDialog, key: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
key += "State"
|
|
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restoreState(widget: Union[aqt.AnkiQt, QFileDialog], key: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
key += "State"
|
|
|
|
if aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreState(aqt.mw.pm.profile[key])
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def saveSplitter(widget: QSplitter, key: str) -> None:
|
2020-06-08 20:55:25 +02:00
|
|
|
key += "Splitter"
|
|
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restoreSplitter(widget: QSplitter, key: str) -> None:
|
2020-06-08 20:55:25 +02:00
|
|
|
key += "Splitter"
|
|
|
|
if aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreState(aqt.mw.pm.profile[key])
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def saveHeader(widget: QHeaderView, key: str) -> None:
|
2020-06-08 20:55:25 +02:00
|
|
|
key += "Header"
|
|
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restoreHeader(widget: QHeaderView, key: str) -> None:
|
2020-06-08 20:55:25 +02:00
|
|
|
key += "Header"
|
|
|
|
if aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreState(aqt.mw.pm.profile[key])
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def save_is_checked(widget: QWidget, key: str) -> None:
|
2020-05-31 05:57:11 +02:00
|
|
|
key += "IsChecked"
|
|
|
|
aqt.mw.pm.profile[key] = widget.isChecked()
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restore_is_checked(widget: QWidget, key: str) -> None:
|
2020-05-31 05:57:11 +02:00
|
|
|
key += "IsChecked"
|
|
|
|
if aqt.mw.pm.profile.get(key) is not None:
|
|
|
|
widget.setChecked(aqt.mw.pm.profile[key])
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def save_combo_index_for_session(widget: QComboBox, key: str) -> None:
|
2021-02-11 01:09:06 +01:00
|
|
|
textKey = f"{key}ComboActiveText"
|
|
|
|
indexKey = f"{key}ComboActiveIndex"
|
2020-06-01 17:47:46 +02:00
|
|
|
aqt.mw.pm.session[textKey] = widget.currentText()
|
|
|
|
aqt.mw.pm.session[indexKey] = widget.currentIndex()
|
2020-05-31 21:52:58 +02:00
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restore_combo_index_for_session(
|
|
|
|
widget: QComboBox, history: List[str], key: str
|
|
|
|
) -> None:
|
2021-02-11 01:09:06 +01:00
|
|
|
textKey = f"{key}ComboActiveText"
|
|
|
|
indexKey = f"{key}ComboActiveIndex"
|
2020-06-01 17:47:46 +02:00
|
|
|
text = aqt.mw.pm.session.get(textKey)
|
|
|
|
index = aqt.mw.pm.session.get(indexKey)
|
2020-05-31 21:52:58 +02:00
|
|
|
if text is not None and index is not None:
|
|
|
|
if index < len(history) and history[index] == text:
|
|
|
|
widget.setCurrentIndex(index)
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str:
|
2020-05-31 22:30:14 +02:00
|
|
|
name += "BoxHistory"
|
|
|
|
text_input = comboBox.lineEdit().text()
|
|
|
|
if text_input in history:
|
|
|
|
history.remove(text_input)
|
|
|
|
history.insert(0, text_input)
|
|
|
|
history = history[:50]
|
|
|
|
comboBox.clear()
|
|
|
|
comboBox.addItems(history)
|
2020-06-01 17:47:46 +02:00
|
|
|
aqt.mw.pm.session[name] = text_input
|
2020-05-31 22:30:14 +02:00
|
|
|
aqt.mw.pm.profile[name] = history
|
|
|
|
return text_input
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]:
|
2020-06-01 17:34:26 +02:00
|
|
|
name += "BoxHistory"
|
|
|
|
history = aqt.mw.pm.profile.get(name, [])
|
2020-06-01 17:47:46 +02:00
|
|
|
comboBox.addItems([""] + history)
|
|
|
|
if history:
|
|
|
|
session_input = aqt.mw.pm.session.get(name)
|
|
|
|
if session_input and session_input == history[0]:
|
|
|
|
comboBox.lineEdit().setText(session_input)
|
|
|
|
comboBox.lineEdit().selectAll()
|
2020-06-01 17:34:26 +02:00
|
|
|
return history
|
|
|
|
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def mungeQA(col: Collection, txt: str) -> str:
|
2020-01-24 02:06:11 +01:00
|
|
|
print("mungeQA() deprecated; use mw.prepare_card_text_for_display()")
|
2020-11-10 14:50:17 +01:00
|
|
|
txt = col.media.escape_media_filenames(txt)
|
2012-12-21 08:51:59 +01:00
|
|
|
return txt
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def openFolder(path: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if isWin:
|
2021-02-11 01:09:06 +01:00
|
|
|
subprocess.Popen(["explorer", f"file://{path}"])
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2017-06-27 04:04:42 +02:00
|
|
|
with noBundledLibs():
|
2021-02-11 01:09:06 +01:00
|
|
|
QDesktopServices.openUrl(QUrl(f"file://{path}"))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def shortcut(key: str) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
if isMac:
|
|
|
|
return re.sub("(?i)ctrl", "Command", key)
|
|
|
|
return key
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def maybeHideClose(bbox: QDialogButtonBox) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if isMac:
|
|
|
|
b = bbox.button(QDialogButtonBox.Close)
|
|
|
|
if b:
|
|
|
|
bbox.removeButton(b)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addCloseShortcut(widg: QDialog) -> None:
|
2012-12-22 01:11:29 +01:00
|
|
|
if not isMac:
|
|
|
|
return
|
|
|
|
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(widg._closeShortcut.activated, widg.reject)
|
2012-12-22 01:11:29 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def downArrow() -> str:
|
2015-09-28 15:09:30 +02:00
|
|
|
if isWin:
|
2016-05-12 06:45:35 +02:00
|
|
|
return "▼"
|
2015-09-28 15:09:30 +02:00
|
|
|
# windows 10 is lacking the smaller arrow on English installs
|
2016-05-12 06:45:35 +02:00
|
|
|
return "▾"
|
2015-09-28 15:09:30 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-03-14 13:08:37 +01:00
|
|
|
def top_level_widget(widget: QWidget) -> QWidget:
|
|
|
|
window = None
|
|
|
|
while widget := widget.parent():
|
|
|
|
window = widget
|
|
|
|
return window
|
|
|
|
|
|
|
|
|
|
|
|
def current_top_level_widget() -> Optional[QWidget]:
|
|
|
|
if widget := QApplication.focusWidget():
|
|
|
|
return top_level_widget(widget)
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Tooltips
|
|
|
|
######################################################################
|
|
|
|
|
2019-12-20 06:07:40 +01:00
|
|
|
_tooltipTimer: Optional[QTimer] = None
|
2019-12-20 08:55:19 +01:00
|
|
|
_tooltipLabel: Optional[QLabel] = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def tooltip(
|
|
|
|
msg: str,
|
|
|
|
period: int = 3000,
|
|
|
|
parent: Optional[aqt.AnkiQt] = None,
|
|
|
|
x_offset: int = 0,
|
|
|
|
y_offset: int = 100,
|
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
global _tooltipTimer, _tooltipLabel
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class CustomLabel(QLabel):
|
2017-08-25 04:14:59 +02:00
|
|
|
silentlyClose = True
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def mousePressEvent(self, evt: QMouseEvent) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
evt.accept()
|
|
|
|
self.hide()
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
closeTooltip()
|
|
|
|
aw = parent or aqt.mw.app.activeWindow() or aqt.mw
|
2019-12-23 01:34:10 +01:00
|
|
|
lab = CustomLabel(
|
2021-02-11 01:09:06 +01:00
|
|
|
f"""<table cellpadding=10>
|
2012-12-21 08:51:59 +01:00
|
|
|
<tr>
|
2021-02-11 01:09:06 +01:00
|
|
|
<td>{msg}</td>
|
2012-12-21 08:51:59 +01:00
|
|
|
</tr>
|
2021-02-11 01:09:06 +01:00
|
|
|
</table>""",
|
2019-12-23 01:34:10 +01:00
|
|
|
aw,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
lab.setFrameStyle(QFrame.Panel)
|
|
|
|
lab.setLineWidth(2)
|
|
|
|
lab.setWindowFlags(Qt.ToolTip)
|
2020-01-23 22:55:14 +01:00
|
|
|
if not theme_manager.night_mode:
|
|
|
|
p = QPalette()
|
|
|
|
p.setColor(QPalette.Window, QColor("#feffc4"))
|
|
|
|
p.setColor(QPalette.WindowText, QColor("#000000"))
|
|
|
|
lab.setPalette(p)
|
2020-06-07 22:06:23 +02:00
|
|
|
lab.move(aw.mapToGlobal(QPoint(0 + x_offset, aw.height() - y_offset)))
|
2012-12-21 08:51:59 +01:00
|
|
|
lab.show()
|
|
|
|
_tooltipTimer = aqt.mw.progress.timer(
|
2019-12-23 01:34:10 +01:00
|
|
|
period, closeTooltip, False, requiresCollection=False
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
_tooltipLabel = lab
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def closeTooltip() -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
global _tooltipLabel, _tooltipTimer
|
|
|
|
if _tooltipLabel:
|
|
|
|
try:
|
|
|
|
_tooltipLabel.deleteLater()
|
|
|
|
except:
|
|
|
|
# already deleted as parent window closed
|
|
|
|
pass
|
|
|
|
_tooltipLabel = None
|
|
|
|
if _tooltipTimer:
|
|
|
|
_tooltipTimer.stop()
|
|
|
|
_tooltipTimer = None
|
2013-02-20 07:12:07 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2013-02-20 07:12:07 +01:00
|
|
|
# true if invalid; print warning
|
2021-02-01 11:23:48 +01:00
|
|
|
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
|
2013-02-20 07:12:07 +01:00
|
|
|
bad = invalidFilename(str, dirsep)
|
|
|
|
if bad:
|
2020-11-17 12:47:47 +01:00
|
|
|
showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val=bad))
|
2013-02-20 07:12:07 +01:00
|
|
|
return True
|
|
|
|
return False
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
# Menus
|
|
|
|
######################################################################
|
2021-02-05 06:26:12 +01:00
|
|
|
# This code will be removed in the future, please don't rely on it.
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
class MenuList:
|
2021-02-01 11:23:48 +01:00
|
|
|
def __init__(self) -> None:
|
2021-02-05 06:26:12 +01:00
|
|
|
traceback.print_stack(file=sys.stdout)
|
|
|
|
print(
|
|
|
|
"MenuList will be removed; please copy it into your add-on's code if you need it."
|
|
|
|
)
|
2021-02-01 11:23:48 +01:00
|
|
|
self.children: List[MenuListChild] = []
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addItem(self, title: str, func: Callable) -> MenuItem:
|
2017-08-15 10:41:36 +02:00
|
|
|
item = MenuItem(title, func)
|
|
|
|
self.children.append(item)
|
|
|
|
return item
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addSeparator(self) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
self.children.append(None)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addMenu(self, title: str) -> SubMenu:
|
2017-08-15 10:41:36 +02:00
|
|
|
submenu = SubMenu(title)
|
|
|
|
self.children.append(submenu)
|
|
|
|
return submenu
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
self.children.append(child)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def renderTo(self, qmenu: QMenu) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
for child in self.children:
|
|
|
|
if child is None:
|
|
|
|
qmenu.addSeparator()
|
|
|
|
elif isinstance(child, QAction):
|
|
|
|
qmenu.addAction(child)
|
|
|
|
else:
|
|
|
|
child.renderTo(qmenu)
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def popupOver(self, widget: QPushButton) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
qmenu = QMenu()
|
|
|
|
self.renderTo(qmenu)
|
2019-12-23 01:34:10 +01:00
|
|
|
qmenu.exec_(widget.mapToGlobal(QPoint(0, 0)))
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
class SubMenu(MenuList):
|
2021-02-01 11:23:48 +01:00
|
|
|
def __init__(self, title: str) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
super().__init__()
|
|
|
|
self.title = title
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def renderTo(self, menu: QMenu) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
submenu = menu.addMenu(self.title)
|
|
|
|
super().renderTo(submenu)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
class MenuItem:
|
2021-02-01 11:23:48 +01:00
|
|
|
def __init__(self, title: str, func: Callable) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
self.title = title
|
|
|
|
self.func = func
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def renderTo(self, qmenu: QMenu) -> None:
|
2017-08-15 10:41:36 +02:00
|
|
|
a = qmenu.addAction(self.title)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(a.triggered, self.func)
|
2017-08-15 10:41:36 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def qtMenuShortcutWorkaround(qmenu: QMenu) -> None:
|
2019-02-05 05:37:07 +01:00
|
|
|
if qtminor < 10:
|
|
|
|
return
|
|
|
|
for act in qmenu.actions():
|
|
|
|
act.setShortcutVisibleInContextMenu(True)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2018-12-13 11:59:06 +01:00
|
|
|
######################################################################
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def supportText() -> str:
|
2019-02-24 14:50:39 +01:00
|
|
|
import platform
|
2020-02-03 02:17:10 +01:00
|
|
|
import time
|
2020-08-31 04:05:36 +02:00
|
|
|
|
2019-02-24 14:50:39 +01:00
|
|
|
from aqt import mw
|
|
|
|
|
|
|
|
if isWin:
|
2021-02-11 01:09:06 +01:00
|
|
|
platname = f"Windows {platform.win32_ver()[0]}"
|
2019-02-24 14:50:39 +01:00
|
|
|
elif isMac:
|
2021-02-11 01:09:06 +01:00
|
|
|
platname = f"Mac {platform.mac_ver()[0]}"
|
2019-02-24 14:50:39 +01:00
|
|
|
else:
|
|
|
|
platname = "Linux"
|
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def schedVer() -> str:
|
2019-02-24 14:50:39 +01:00
|
|
|
try:
|
2021-02-01 11:23:48 +01:00
|
|
|
return str(mw.col.schedVer())
|
2019-02-24 14:50:39 +01:00
|
|
|
except:
|
|
|
|
return "?"
|
|
|
|
|
2020-02-03 02:17:10 +01:00
|
|
|
lc = mw.pm.last_addon_update_check()
|
|
|
|
lcfmt = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(lc))
|
|
|
|
|
2019-02-24 14:50:39 +01:00
|
|
|
return """\
|
|
|
|
Anki {} Python {} Qt {} PyQt {}
|
|
|
|
Platform: {}
|
|
|
|
Flags: frz={} ao={} sv={}
|
2020-02-03 02:17:10 +01:00
|
|
|
Add-ons, last update check: {}
|
2019-12-23 01:34:10 +01:00
|
|
|
""".format(
|
|
|
|
versionWithBuild(),
|
|
|
|
platform.python_version(),
|
|
|
|
QT_VERSION_STR,
|
|
|
|
PYQT_VERSION_STR,
|
|
|
|
platname,
|
|
|
|
getattr(sys, "frozen", False),
|
|
|
|
mw.addonManager.dirty,
|
|
|
|
schedVer(),
|
2020-02-03 02:17:10 +01:00
|
|
|
lcfmt,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
|
|
|
|
2019-02-24 14:50:39 +01:00
|
|
|
|
2018-12-18 10:29:34 +01:00
|
|
|
######################################################################
|
|
|
|
|
|
|
|
# adapted from version detection in qutebrowser
|
2021-02-01 11:23:48 +01:00
|
|
|
def opengl_vendor() -> Optional[str]:
|
2018-12-18 10:29:34 +01:00
|
|
|
old_context = QOpenGLContext.currentContext()
|
|
|
|
old_surface = None if old_context is None else old_context.surface()
|
|
|
|
|
|
|
|
surface = QOffscreenSurface()
|
|
|
|
surface.create()
|
|
|
|
|
|
|
|
ctx = QOpenGLContext()
|
|
|
|
ok = ctx.create()
|
|
|
|
if not ok:
|
|
|
|
return None
|
|
|
|
|
|
|
|
ok = ctx.makeCurrent(surface)
|
|
|
|
if not ok:
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
if ctx.isOpenGLES():
|
|
|
|
# Can't use versionFunctions there
|
|
|
|
return None
|
|
|
|
|
|
|
|
vp = QOpenGLVersionProfile()
|
|
|
|
vp.setVersion(2, 0)
|
|
|
|
|
|
|
|
try:
|
|
|
|
vf = ctx.versionFunctions(vp)
|
|
|
|
except ImportError as e:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if vf is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return vf.glGetString(vf.GL_VENDOR)
|
|
|
|
finally:
|
|
|
|
ctx.doneCurrent()
|
|
|
|
if old_context and old_surface:
|
|
|
|
old_context.makeCurrent(old_surface)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 11:23:48 +01:00
|
|
|
def gfxDriverIsBroken() -> bool:
|
2018-12-18 10:29:34 +01:00
|
|
|
driver = opengl_vendor()
|
|
|
|
return driver == "nouveau"
|
2020-01-23 07:10:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
|
|
def startup_info() -> Any:
|
|
|
|
"Use subprocess.Popen(startupinfo=...) to avoid opening a console window."
|
|
|
|
if not sys.platform == "win32":
|
|
|
|
return None
|
|
|
|
si = subprocess.STARTUPINFO() # pytype: disable=module-attr
|
|
|
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # pytype: disable=module-attr
|
|
|
|
return si
|
2021-03-16 13:40:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
def ensure_editor_saved(func: Callable) -> Callable:
|
|
|
|
"""Ensure the current editor's note is saved before running the wrapped function.
|
|
|
|
|
|
|
|
Must be used on functions that may be invoked from a shortcut key while the
|
|
|
|
editor has focus. For functions that can't be activated while the editor has
|
|
|
|
focus, you don't need this.
|
|
|
|
|
|
|
|
Will look for the editor as self.editor.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
def decorated(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
|
|
self.editor.call_after_note_saved(lambda: func(self, *args, **kwargs))
|
|
|
|
|
|
|
|
return decorated
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_editor_saved_on_trigger(func: Callable) -> Callable:
|
|
|
|
"""Like ensure_editor_saved(), but tells Qt this function takes no args.
|
|
|
|
|
|
|
|
This ensures PyQt doesn't attempt to pass a `toggled` arg
|
|
|
|
into functions connected to a `triggered` signal.
|
|
|
|
"""
|
|
|
|
return pyqtSlot()(ensure_editor_saved(func)) # type: ignore
|