anki/qt/aqt/utils.py
Damien Elmes 7fc32db0c9 fix: slowdowns after import; hard to read popup
QTextEdit() will pin the CPU at 100% for seconds to minutes when
fed a large string to display - work around it by switching to
QPlainTextEdit().

Also strip HTML before showing the user - easier to read, and less
text to display. And turn off word wrap, as it makes it easier to skim,
and further reduces the work the widget needs to do.

https://forums.ankiweb.net/t/big-issue-where-anki-gets-slow-when-you-import-this-deck/7050
2021-02-02 15:49:47 +10:00

972 lines
26 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import os
import re
import subprocess
import sys
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Callable,
List,
Literal,
Optional,
Sequence,
Tuple,
Union,
cast,
)
from markdown import markdown
from PyQt5.QtWidgets import (
QAction,
QDialog,
QDialogButtonBox,
QFileDialog,
QHeaderView,
QMenu,
QPushButton,
QSplitter,
QWidget,
)
import anki
import aqt
from anki import Collection
from anki.errors import InvalidInput
from anki.lang import TR # pylint: disable=unused-import
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
from aqt.qt import *
from aqt.theme import theme_manager
if TYPE_CHECKING:
from anki.lang import TRValue
TextFormat = Union[Literal["plain", "rich"]]
def aqt_data_folder() -> str:
# running in place?
dir = os.path.join(os.path.dirname(__file__), "data")
if os.path.exists(dir):
return dir
# packaged install?
dir2 = os.path.join(sys.prefix, "aqt_data")
if os.path.exists(dir2):
return dir2
# should only happen when running unit tests
print("warning, data folder not found")
return "."
def locale_dir() -> str:
return os.path.join(aqt_data_folder(), "locale")
def tr(key: TRValue, **kwargs: Union[str, int, float]) -> str:
"Shortcut to access Fluent translations."
return anki.lang.current_i18n.translate(key, **kwargs)
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.
"""
def openHelp(section: HelpPageArgument) -> None:
link = aqt.appHelpSite
if section:
if isinstance(section, HelpPage):
link += section.value
else:
link += section
openLink(link)
def openLink(link: str) -> None:
tooltip(tr(TR.QT_MISC_LOADING), period=1000)
with noBundledLibs():
QDesktopServices.openUrl(QUrl(link))
def showWarning(
text: str,
parent: Optional[QDialog] = None,
help: HelpPageArgument = "",
title: str = "Anki",
textFormat: Optional[TextFormat] = None,
) -> int:
"Show a small warning with an OK button."
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
def showCritical(
text: str,
parent: Optional[QDialog] = None,
help: str = "",
title: str = "Anki",
textFormat: Optional[TextFormat] = None,
) -> int:
"Show a small critical error with an OK button."
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
def show_invalid_search_error(err: Exception) -> None:
"Render search errors in markdown, then display a warning."
text = str(err)
if isinstance(err, InvalidInput):
text = markdown(text)
showWarning(text)
def showInfo(
text: str,
parent: Union[Literal[False], QDialog] = False,
help: HelpPageArgument = "",
type: str = "info",
title: str = "Anki",
textFormat: Optional[TextFormat] = None,
customBtns: Optional[List[QMessageBox.StandardButton]] = None,
) -> int:
"Show a small info window with an OK button."
parent_widget: QWidget
if parent is False:
parent_widget = aqt.mw.app.activeWindow() or aqt.mw
else:
parent_widget = parent
if type == "warning":
icon = QMessageBox.Warning
elif type == "critical":
icon = QMessageBox.Critical
else:
icon = QMessageBox.Information
mb = QMessageBox(parent_widget) #
if textFormat == "plain":
mb.setTextFormat(Qt.PlainText)
elif textFormat == "rich":
mb.setTextFormat(Qt.RichText)
elif textFormat is not None:
raise Exception("unexpected textFormat type")
mb.setText(text)
mb.setIcon(icon)
mb.setWindowTitle(title)
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)
if help:
b = mb.addButton(QMessageBox.Help)
qconnect(b.clicked, lambda: openHelp(help))
b.setAutoDefault(False)
return mb.exec_()
def showText(
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,
plain_text_edit: bool = False,
) -> Optional[Tuple[QDialog, QDialogButtonBox]]:
if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw
diag = QDialog(parent)
diag.setWindowTitle(title)
disable_help_button(diag)
layout = QVBoxLayout(diag)
diag.setLayout(layout)
if plain_text_edit:
# used by the importer
text = QPlainTextEdit()
text.setReadOnly(True)
text.setWordWrapMode(QTextOption.NoWrap)
else:
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() -> None:
QApplication.clipboard().setText(text.toPlainText())
btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD))
qconnect(btn.clicked, onCopy)
box.addButton(btn, QDialogButtonBox.ActionRole)
def onReject() -> None:
if geomKey:
saveGeom(diag, geomKey)
QDialog.reject(diag)
qconnect(box.rejected, onReject)
def onFinish() -> None:
if geomKey:
saveGeom(diag, geomKey)
qconnect(box.accepted, onFinish)
diag.setMinimumHeight(minHeight)
diag.setMinimumWidth(minWidth)
if geomKey:
restoreGeom(diag, geomKey)
if run:
diag.exec_()
return None
else:
return diag, box
def askUser(
text: str,
parent: QDialog = None,
help: HelpPageArgument = None,
defaultno: bool = False,
msgfunc: Optional[Callable] = None,
title: str = "Anki",
) -> bool:
"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
r = msgfunc(parent, title, text, cast(QMessageBox.StandardButtons, sb), default)
if r == QMessageBox.Help:
openHelp(help)
else:
break
return r == QMessageBox.Yes
class ButtonedDialog(QMessageBox):
def __init__(
self,
text: str,
buttons: List[str],
parent: Optional[QDialog] = None,
help: HelpPageArgument = None,
title: str = "Anki",
):
QMessageBox.__init__(self, parent)
self._buttons: List[QPushButton] = []
self.setWindowTitle(title)
self.help = help
self.setIcon(QMessageBox.Warning)
self.setText(text)
for b in buttons:
self._buttons.append(self.addButton(b, QMessageBox.AcceptRole))
if help:
self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole)
buttons.append(tr(TR.ACTIONS_HELP))
def run(self) -> str:
self.exec_()
but = self.clickedButton().text()
if but == "Help":
# FIXME stop dialog closing?
openHelp(self.help)
txt = self.clickedButton().text()
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
return txt.replace("&", "")
def setDefault(self, idx: int) -> None:
self.setDefaultButton(self._buttons[idx])
def askUserDialog(
text: str,
buttons: List[str],
parent: Optional[QDialog] = None,
help: HelpPageArgument = None,
title: str = "Anki",
) -> ButtonedDialog:
if not parent:
parent = aqt.mw
diag = ButtonedDialog(text, buttons, parent, help, title=title)
return diag
class GetTextDialog(QDialog):
def __init__(
self,
parent: Optional[QDialog],
question: str,
help: HelpPageArgument = None,
edit: Optional[QLineEdit] = None,
default: str = "",
title: str = "Anki",
minWidth: int = 400,
) -> None:
QDialog.__init__(self, parent)
self.setWindowTitle(title)
disable_help_button(self)
self.question = question
self.help = help
self.qlabel = QLabel(question)
self.setMinimumWidth(minWidth)
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
b = QDialogButtonBox(buts) # type: ignore
v.addWidget(b)
self.setLayout(v)
qconnect(b.button(QDialogButtonBox.Ok).clicked, self.accept)
qconnect(b.button(QDialogButtonBox.Cancel).clicked, self.reject)
if help:
qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested)
def accept(self) -> None:
return QDialog.accept(self)
def reject(self) -> None:
return QDialog.reject(self)
def helpRequested(self) -> None:
openHelp(self.help)
def getText(
prompt: str,
parent: Optional[QDialog] = None,
help: HelpPageArgument = None,
edit: Optional[QLineEdit] = None,
default: str = "",
title: str = "Anki",
geomKey: Optional[str] = None,
**kwargs: Any,
) -> Tuple[str, int]:
if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw
d = GetTextDialog(
parent, prompt, help=help, edit=edit, default=default, title=title, **kwargs
)
d.setWindowModality(Qt.WindowModal)
if geomKey:
restoreGeom(d, geomKey)
ret = d.exec_()
if geomKey and ret:
saveGeom(d, geomKey)
return (str(d.l.text()), ret)
def getOnlyText(*args: Any, **kwargs: Any) -> str:
(s, r) = getText(*args, **kwargs)
if r:
return s
else:
return ""
# fixme: these utilities could be combined into a single base class
# unused by Anki, but used by add-ons
def chooseList(
prompt: str, choices: List[str], startrow: int = 0, parent: Any = None
) -> int:
if not parent:
parent = aqt.mw.app.activeWindow()
d = QDialog(parent)
disable_help_button(d)
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)
qconnect(bb.accepted, d.accept)
l.addWidget(bb)
d.exec_()
return c.currentRow()
def getTag(
parent: QDialog, deck: Collection, question: str, **kwargs: Any
) -> Tuple[str, int]:
from aqt.tagedit import TagEdit
te = TagEdit(parent)
te.setCol(deck)
ret = getText(question, parent, edit=te, geomKey="getTag", **kwargs)
te.hideCompleter()
return ret
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)
# File handling
######################################################################
def getFile(
parent: QDialog,
title: str,
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]]:
"Ask the user for a file."
assert not dir or not key
if not dir:
dirkey = key + "Directory"
dir = aqt.mw.pm.profile.get(dirkey, "")
else:
dirkey = None
d = QFileDialog(parent)
mode = QFileDialog.ExistingFiles if multi else QFileDialog.ExistingFile
d.setFileMode(mode)
if os.path.exists(dir):
d.setDirectory(dir)
d.setWindowTitle(title)
d.setNameFilter(filter)
ret = []
def accept() -> None:
files = list(d.selectedFiles())
if dirkey:
dir = os.path.dirname(files[0])
aqt.mw.pm.profile[dirkey] = dir
result = files if multi else files[0]
if cb:
cb(result)
ret.append(result)
qconnect(d.accepted, accept)
if key:
restoreState(d, key)
d.exec_()
if key:
saveState(d, key)
return ret[0] if ret else None
def getSaveFile(
parent: QDialog,
title: str,
dir_description: str,
key: str,
ext: str,
fname: Optional[str] = None,
) -> str:
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
variable. The file dialog will default to open with FNAME."""
config_key = dir_description + "Directory"
defaultPath = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
base = aqt.mw.pm.profile.get(config_key, defaultPath)
path = os.path.join(base, fname)
file = QFileDialog.getSaveFileName(
parent,
title,
path,
"{0} (*{1})".format(key, ext),
options=QFileDialog.DontConfirmOverwrite,
)[0]
if file:
# add extension
if not file.lower().endswith(ext):
file += ext
# save new default
dir = os.path.dirname(file)
aqt.mw.pm.profile[config_key] = dir
# check if it exists
if os.path.exists(file):
if not askUser(tr(TR.QT_MISC_THIS_FILE_EXISTS_ARE_YOU_SURE), parent):
return None
return file
def saveGeom(widget: QDialog, key: str) -> None:
key += "Geom"
if isMac and widget.windowState() & Qt.WindowFullScreen:
geom = None
else:
geom = widget.saveGeometry()
aqt.mw.pm.profile[key] = geom
def restoreGeom(
widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False
) -> None:
key += "Geom"
if aqt.mw.pm.profile.get(key):
widget.restoreGeometry(aqt.mw.pm.profile[key])
if isMac and offset:
if qtminor > 6:
# bug in osx toolkit
s = widget.size()
widget.resize(s.width(), s.height() + offset * 2)
ensureWidgetInScreenBoundaries(widget)
else:
if adjustSize:
widget.adjustSize()
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
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
# ensure widget is smaller than screen bounds
geom = handle.screen().availableGeometry()
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
x = min(x, geom.width() + geom.x() - cappedWidth)
y = min(y, geom.height() + geom.y() - cappedHeight)
if x != wpos.x() or y != wpos.y():
widget.move(x, y)
def saveState(widget: QFileDialog, key: str) -> None:
key += "State"
aqt.mw.pm.profile[key] = widget.saveState()
def restoreState(widget: Union[aqt.AnkiQt, QFileDialog], key: str) -> None:
key += "State"
if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.pm.profile[key])
def saveSplitter(widget: QSplitter, key: str) -> None:
key += "Splitter"
aqt.mw.pm.profile[key] = widget.saveState()
def restoreSplitter(widget: QSplitter, key: str) -> None:
key += "Splitter"
if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.pm.profile[key])
def saveHeader(widget: QHeaderView, key: str) -> None:
key += "Header"
aqt.mw.pm.profile[key] = widget.saveState()
def restoreHeader(widget: QHeaderView, key: str) -> None:
key += "Header"
if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.pm.profile[key])
def save_is_checked(widget: QWidget, key: str) -> None:
key += "IsChecked"
aqt.mw.pm.profile[key] = widget.isChecked()
def restore_is_checked(widget: QWidget, key: str) -> None:
key += "IsChecked"
if aqt.mw.pm.profile.get(key) is not None:
widget.setChecked(aqt.mw.pm.profile[key])
def save_combo_index_for_session(widget: QComboBox, key: str) -> None:
textKey = key + "ComboActiveText"
indexKey = key + "ComboActiveIndex"
aqt.mw.pm.session[textKey] = widget.currentText()
aqt.mw.pm.session[indexKey] = widget.currentIndex()
def restore_combo_index_for_session(
widget: QComboBox, history: List[str], key: str
) -> None:
textKey = key + "ComboActiveText"
indexKey = key + "ComboActiveIndex"
text = aqt.mw.pm.session.get(textKey)
index = aqt.mw.pm.session.get(indexKey)
if text is not None and index is not None:
if index < len(history) and history[index] == text:
widget.setCurrentIndex(index)
def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str:
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)
aqt.mw.pm.session[name] = text_input
aqt.mw.pm.profile[name] = history
return text_input
def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]:
name += "BoxHistory"
history = aqt.mw.pm.profile.get(name, [])
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()
return history
def mungeQA(col: Collection, txt: str) -> str:
print("mungeQA() deprecated; use mw.prepare_card_text_for_display()")
txt = col.media.escape_media_filenames(txt)
return txt
def openFolder(path: str) -> None:
if isWin:
subprocess.Popen(["explorer", "file://" + path])
else:
with noBundledLibs():
QDesktopServices.openUrl(QUrl("file://" + path))
def shortcut(key: str) -> str:
if isMac:
return re.sub("(?i)ctrl", "Command", key)
return key
def maybeHideClose(bbox: QDialogButtonBox) -> None:
if isMac:
b = bbox.button(QDialogButtonBox.Close)
if b:
bbox.removeButton(b)
def addCloseShortcut(widg: QDialog) -> None:
if not isMac:
return
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
qconnect(widg._closeShortcut.activated, widg.reject)
def downArrow() -> str:
if isWin:
return ""
# windows 10 is lacking the smaller arrow on English installs
return ""
# Tooltips
######################################################################
_tooltipTimer: Optional[QTimer] = None
_tooltipLabel: Optional[QLabel] = None
def tooltip(
msg: str,
period: int = 3000,
parent: Optional[aqt.AnkiQt] = None,
x_offset: int = 0,
y_offset: int = 100,
) -> None:
global _tooltipTimer, _tooltipLabel
class CustomLabel(QLabel):
silentlyClose = True
def mousePressEvent(self, evt: QMouseEvent) -> None:
evt.accept()
self.hide()
closeTooltip()
aw = parent or aqt.mw.app.activeWindow() or aqt.mw
lab = CustomLabel(
"""\
<table cellpadding=10>
<tr>
<td>%s</td>
</tr>
</table>"""
% msg,
aw,
)
lab.setFrameStyle(QFrame.Panel)
lab.setLineWidth(2)
lab.setWindowFlags(Qt.ToolTip)
if not theme_manager.night_mode:
p = QPalette()
p.setColor(QPalette.Window, QColor("#feffc4"))
p.setColor(QPalette.WindowText, QColor("#000000"))
lab.setPalette(p)
lab.move(aw.mapToGlobal(QPoint(0 + x_offset, aw.height() - y_offset)))
lab.show()
_tooltipTimer = aqt.mw.progress.timer(
period, closeTooltip, False, requiresCollection=False
)
_tooltipLabel = lab
def closeTooltip() -> None:
global _tooltipLabel, _tooltipTimer
if _tooltipLabel:
try:
_tooltipLabel.deleteLater()
except:
# already deleted as parent window closed
pass
_tooltipLabel = None
if _tooltipTimer:
_tooltipTimer.stop()
_tooltipTimer = None
# true if invalid; print warning
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
bad = invalidFilename(str, dirsep)
if bad:
showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val=bad))
return True
return False
# Menus
######################################################################
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
class MenuList:
def __init__(self) -> None:
self.children: List[MenuListChild] = []
def addItem(self, title: str, func: Callable) -> MenuItem:
item = MenuItem(title, func)
self.children.append(item)
return item
def addSeparator(self) -> None:
self.children.append(None)
def addMenu(self, title: str) -> SubMenu:
submenu = SubMenu(title)
self.children.append(submenu)
return submenu
def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None:
self.children.append(child)
def renderTo(self, qmenu: QMenu) -> None:
for child in self.children:
if child is None:
qmenu.addSeparator()
elif isinstance(child, QAction):
qmenu.addAction(child)
else:
child.renderTo(qmenu)
def popupOver(self, widget: QPushButton) -> None:
qmenu = QMenu()
self.renderTo(qmenu)
qmenu.exec_(widget.mapToGlobal(QPoint(0, 0)))
class SubMenu(MenuList):
def __init__(self, title: str) -> None:
super().__init__()
self.title = title
def renderTo(self, menu: QMenu) -> None:
submenu = menu.addMenu(self.title)
super().renderTo(submenu)
class MenuItem:
def __init__(self, title: str, func: Callable) -> None:
self.title = title
self.func = func
def renderTo(self, qmenu: QMenu) -> None:
a = qmenu.addAction(self.title)
qconnect(a.triggered, self.func)
def qtMenuShortcutWorkaround(qmenu: QMenu) -> None:
if qtminor < 10:
return
for act in qmenu.actions():
act.setShortcutVisibleInContextMenu(True)
######################################################################
def supportText() -> str:
import platform
import time
from aqt import mw
if isWin:
platname = "Windows " + platform.win32_ver()[0]
elif isMac:
platname = "Mac " + platform.mac_ver()[0]
else:
platname = "Linux"
def schedVer() -> str:
try:
return str(mw.col.schedVer())
except:
return "?"
lc = mw.pm.last_addon_update_check()
lcfmt = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(lc))
return """\
Anki {} Python {} Qt {} PyQt {}
Platform: {}
Flags: frz={} ao={} sv={}
Add-ons, last update check: {}
""".format(
versionWithBuild(),
platform.python_version(),
QT_VERSION_STR,
PYQT_VERSION_STR,
platname,
getattr(sys, "frozen", False),
mw.addonManager.dirty,
schedVer(),
lcfmt,
)
######################################################################
# adapted from version detection in qutebrowser
def opengl_vendor() -> Optional[str]:
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)
def gfxDriverIsBroken() -> bool:
driver = opengl_vendor()
return driver == "nouveau"
######################################################################
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