mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Rework error dialog
- Hide traceback - Include full add-on info in 'copy debug info' button, like about screen - Link to troubleshooting page - Use non-modal pop-up in the common case, to avoid potential conflicts with other modals. Closes #2830
This commit is contained in:
parent
e5170f341b
commit
063b6f60fd
6 changed files with 121 additions and 104 deletions
|
@ -1,4 +1,10 @@
|
|||
-errors-support-site = [support site](https://help.ankiweb.net)
|
||||
errors-standard-popup2 =
|
||||
Anki encountered a problem. Please follow the troubleshooting steps.
|
||||
errors-may-be-addon = The problem may be caused by an add-on.
|
||||
errors-troubleshooting-button = Troubleshooting
|
||||
errors-copy-debug-info-button = Copy Debug Info
|
||||
errors-copied-to-clipboard = Copied to clipboard
|
||||
errors-standard-popup =
|
||||
# Error
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ message HelpPageLinkRequest {
|
|||
CARD_TYPE_MISSING_CLOZE = 20;
|
||||
CARD_TYPE_EXTRANEOUS_CLOZE = 21;
|
||||
CARD_TYPE_TEMPLATE_ERROR = 22;
|
||||
TROUBLESHOOTING = 23;
|
||||
}
|
||||
HelpPage page = 1;
|
||||
}
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import platform
|
||||
import time
|
||||
|
||||
import aqt.forms
|
||||
from anki.lang import without_unicode_isolation
|
||||
from anki.utils import version_with_build
|
||||
from aqt.addons import AddonManager, AddonMeta
|
||||
from aqt.errors import addon_debug_info
|
||||
from aqt.qt import *
|
||||
from aqt.utils import disable_help_button, supportText, tooltip, tr
|
||||
|
||||
|
@ -33,61 +32,15 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
abt = aqt.forms.about.Ui_About()
|
||||
abt.setupUi(dialog)
|
||||
|
||||
# Copy debug info
|
||||
######################################################################
|
||||
|
||||
def addon_fmt(addmgr: AddonManager, addon: AddonMeta) -> str:
|
||||
if addon.installed_at:
|
||||
installed = time.strftime(
|
||||
"%Y-%m-%dT%H:%M", time.localtime(addon.installed_at)
|
||||
)
|
||||
else:
|
||||
installed = "0"
|
||||
if addon.provided_name:
|
||||
name = addon.provided_name
|
||||
else:
|
||||
name = "''"
|
||||
user = addmgr.getConfig(addon.dir_name)
|
||||
default = addmgr.addonConfigDefaults(addon.dir_name)
|
||||
if user == default:
|
||||
modified = "''"
|
||||
else:
|
||||
modified = "mod"
|
||||
return f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]"
|
||||
|
||||
def onCopy() -> None:
|
||||
addmgr = mw.addonManager
|
||||
active = []
|
||||
activeids = []
|
||||
inactive = []
|
||||
for addon in addmgr.all_addon_meta():
|
||||
if addon.enabled:
|
||||
active.append(addon_fmt(addmgr, addon))
|
||||
if addon.ankiweb_id():
|
||||
activeids.append(addon.dir_name)
|
||||
else:
|
||||
inactive.append(addon_fmt(addmgr, addon))
|
||||
newline = "\n"
|
||||
info = f"""
|
||||
{supportText()}
|
||||
|
||||
===Add-ons (active)===
|
||||
(add-on provided name [Add-on folder, installed at, version, is config changed])
|
||||
{newline.join(sorted(active))}
|
||||
|
||||
===IDs of active AnkiWeb add-ons===
|
||||
{" ".join(activeids)}
|
||||
|
||||
===Add-ons (inactive)===
|
||||
(add-on provided name [Add-on folder, installed at, version, is config changed])
|
||||
{newline.join(sorted(inactive))}
|
||||
"""
|
||||
info = f" {' '.join(info.splitlines(True))}"
|
||||
QApplication.clipboard().setText(info)
|
||||
def on_copy() -> None:
|
||||
txt = supportText()
|
||||
if mw.addonManager.dirty:
|
||||
txt += "\n" + addon_debug_info()
|
||||
QApplication.clipboard().setText(txt)
|
||||
tooltip(tr.about_copied_to_clipboard(), parent=dialog)
|
||||
|
||||
btn = QPushButton(tr.about_copy_debug_info())
|
||||
qconnect(btn.clicked, onCopy)
|
||||
qconnect(btn.clicked, on_copy)
|
||||
abt.buttonBox.addButton(btn, QDialogButtonBox.ButtonRole.ActionRole)
|
||||
abt.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setFocus()
|
||||
|
||||
|
|
132
qt/aqt/errors.py
132
qt/aqt/errors.py
|
@ -3,18 +3,18 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Optional, TextIO, cast
|
||||
|
||||
from markdown import markdown
|
||||
|
||||
import aqt
|
||||
from anki.collection import HelpPage
|
||||
from anki.errors import BackendError, Interrupted
|
||||
from aqt.addons import AddonManager, AddonMeta
|
||||
from aqt.qt import *
|
||||
from aqt.utils import showText, showWarning, supportText, tr
|
||||
from aqt.utils import openHelp, showWarning, supportText, tooltip, tr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aqt.main import AnkiQt
|
||||
|
@ -150,13 +150,14 @@ def is_chromium_cert_error(error: str) -> bool:
|
|||
if not os.environ.get("DEBUG"):
|
||||
|
||||
def excepthook(etype, val, tb) -> None: # type: ignore
|
||||
sys.stderr.write(
|
||||
"Caught exception:\n%s\n"
|
||||
% ("".join(traceback.format_exception(etype, val, tb)))
|
||||
)
|
||||
sys.stderr.write("%s\n" % ("".join(traceback.format_exception(etype, val, tb))))
|
||||
|
||||
sys.excepthook = excepthook
|
||||
|
||||
# so we can be non-modal/non-blocking, without Python deallocating the message
|
||||
# box ahead of time
|
||||
_mbox: QMessageBox | None = None
|
||||
|
||||
|
||||
class ErrorHandler(QObject):
|
||||
"Catch stderr and write into buffer."
|
||||
|
@ -206,7 +207,7 @@ class ErrorHandler(QObject):
|
|||
if self.fatal_error_encountered:
|
||||
# suppress follow-up errors caused by the poisoned lock
|
||||
return
|
||||
error = html.escape(self.pool)
|
||||
error = self.pool
|
||||
self.pool = ""
|
||||
self.mw.progress.clear()
|
||||
if "AbortSchemaModification" in error:
|
||||
|
@ -230,40 +231,111 @@ class ErrorHandler(QObject):
|
|||
if is_chromium_cert_error(error):
|
||||
return
|
||||
|
||||
debug_text = supportText() + "\n" + error
|
||||
|
||||
if "PanicException" in error:
|
||||
self.fatal_error_encountered = True
|
||||
txt = markdown(
|
||||
"**A fatal error occurred, and Anki must close. Please report this message on the forums.**"
|
||||
)
|
||||
error = f"{supportText() + self._addonText(error)}\n{error}"
|
||||
elif self.mw.addonManager.dirty:
|
||||
# Older translations include a link to the old discussions site; rewrite it to a newer one
|
||||
message = tr.errors_addons_active_popup().replace(
|
||||
"https://help.ankiweb.net/discussions/add-ons/",
|
||||
"https://forums.ankiweb.net/c/add-ons/11",
|
||||
)
|
||||
txt = markdown(message)
|
||||
error = f"{supportText() + self._addonText(error)}\n{error}"
|
||||
# ensure no collection-related timers like backup fire
|
||||
self.mw.col = None
|
||||
user_text = "A fatal error occurred, and Anki must close. Please report this message on the forums."
|
||||
else:
|
||||
txt = markdown(tr.errors_standard_popup())
|
||||
error = f"{supportText()}\n{error}"
|
||||
user_text = tr.errors_standard_popup2()
|
||||
if self.mw.addonManager.dirty:
|
||||
user_text += "\n\n" + self._addonText(error)
|
||||
debug_text += addon_debug_info()
|
||||
|
||||
def show_troubleshooting():
|
||||
openHelp(HelpPage.TROUBLESHOOTING)
|
||||
|
||||
def copy_debug_info():
|
||||
QApplication.clipboard().setText(debug_text)
|
||||
tooltip(tr.errors_copied_to_clipboard(), parent=_mbox)
|
||||
|
||||
global _mbox
|
||||
_mbox = QMessageBox()
|
||||
_mbox.setWindowTitle("Anki")
|
||||
_mbox.setText(user_text)
|
||||
_mbox.setIcon(QMessageBox.Icon.Warning)
|
||||
_mbox.setTextFormat(Qt.TextFormat.PlainText)
|
||||
|
||||
troubleshooting = _mbox.addButton(
|
||||
tr.errors_troubleshooting_button(), QMessageBox.ButtonRole.ActionRole
|
||||
)
|
||||
debug_info = _mbox.addButton(
|
||||
tr.errors_copy_debug_info_button(), QMessageBox.ButtonRole.ActionRole
|
||||
)
|
||||
cancel = _mbox.addButton(QMessageBox.StandardButton.Cancel)
|
||||
cancel.setText(tr.actions_close())
|
||||
|
||||
troubleshooting.disconnect()
|
||||
troubleshooting.clicked.connect(show_troubleshooting)
|
||||
debug_info.disconnect()
|
||||
debug_info.clicked.connect(copy_debug_info)
|
||||
|
||||
# show dialog
|
||||
txt = f"{txt}<div style='white-space: pre-wrap'>{error}</div>"
|
||||
showText(txt, type="html", copyBtn=True)
|
||||
if self.fatal_error_encountered:
|
||||
_mbox.exec()
|
||||
sys.exit(1)
|
||||
else:
|
||||
_mbox.show()
|
||||
|
||||
def _addonText(self, error: str) -> str:
|
||||
matches = re.findall(r"addons21(/|\\)(.*?)(/|\\)", error)
|
||||
if not matches:
|
||||
return ""
|
||||
return tr.errors_may_be_addon()
|
||||
# reverse to list most likely suspect first, dict to deduplicate:
|
||||
addons = [
|
||||
aqt.mw.addonManager.addonName(i[1])
|
||||
for i in dict.fromkeys(reversed(matches))
|
||||
]
|
||||
# highlight importance of first add-on:
|
||||
addons[0] = f"<b>{addons[0]}</b>"
|
||||
addons_str = ", ".join(addons)
|
||||
return f"{tr.addons_possibly_involved(addons=addons_str)}\n"
|
||||
return tr.addons_possibly_involved(addons=addons_str)
|
||||
|
||||
|
||||
def addon_fmt(addmgr: AddonManager, addon: AddonMeta) -> str:
|
||||
if addon.installed_at:
|
||||
installed = time.strftime("%Y-%m-%dT%H:%M", time.localtime(addon.installed_at))
|
||||
else:
|
||||
installed = "0"
|
||||
if addon.provided_name:
|
||||
name = addon.provided_name
|
||||
else:
|
||||
name = "''"
|
||||
user = addmgr.getConfig(addon.dir_name)
|
||||
default = addmgr.addonConfigDefaults(addon.dir_name)
|
||||
if user == default:
|
||||
modified = "''"
|
||||
else:
|
||||
modified = "mod"
|
||||
return (
|
||||
f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]"
|
||||
)
|
||||
|
||||
|
||||
def addon_debug_info() -> str:
|
||||
from aqt import mw
|
||||
|
||||
addmgr = mw.addonManager
|
||||
active = []
|
||||
activeids = []
|
||||
inactive = []
|
||||
for addon in addmgr.all_addon_meta():
|
||||
if addon.enabled:
|
||||
active.append(addon_fmt(addmgr, addon))
|
||||
if addon.ankiweb_id():
|
||||
activeids.append(addon.dir_name)
|
||||
else:
|
||||
inactive.append(addon_fmt(addmgr, addon))
|
||||
newline = "\n"
|
||||
info = f"""\
|
||||
===Add-ons (active)===
|
||||
(add-on provided name [Add-on folder, installed at, version, is config changed])
|
||||
{newline.join(sorted(active))}
|
||||
|
||||
===IDs of active AnkiWeb add-ons===
|
||||
{" ".join(activeids)}
|
||||
|
||||
===Add-ons (inactive)===
|
||||
(add-on provided name [Add-on folder, installed at, version, is config changed])
|
||||
{newline.join(sorted(inactive))}
|
||||
"""
|
||||
return info
|
||||
|
|
|
@ -1098,39 +1098,23 @@ def add_ellipsis_to_action_label(*actions: QAction) -> None:
|
|||
|
||||
def supportText() -> str:
|
||||
import platform
|
||||
import time
|
||||
|
||||
from aqt import mw
|
||||
|
||||
platname = platform.platform()
|
||||
|
||||
def schedVer() -> str:
|
||||
try:
|
||||
if mw.col.v3_scheduler():
|
||||
return "3"
|
||||
else:
|
||||
return str(mw.col.sched_ver())
|
||||
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 {}
|
||||
Anki {} {} {}
|
||||
Python {} Qt {} PyQt {}
|
||||
Platform: {}
|
||||
Flags: frz={} ao={} sv={}
|
||||
Add-ons, last update check: {}
|
||||
""".format(
|
||||
version_with_build(),
|
||||
"(src)" if not getattr(sys, "frozen", False) else "",
|
||||
"(ao)" if mw.addonManager.dirty else "",
|
||||
platform.python_version(),
|
||||
qVersion(),
|
||||
PYQT_VERSION_STR,
|
||||
platname,
|
||||
getattr(sys, "frozen", False),
|
||||
mw.addonManager.dirty,
|
||||
schedVer(),
|
||||
lcfmt,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ pub fn help_page_link_suffix(page: HelpPage) -> &'static str {
|
|||
HelpPage::CardTypeExtraneousCloze => {
|
||||
"templates/errors.html#cloze-filter-outside-cloze-notetype"
|
||||
}
|
||||
HelpPage::Troubleshooting => "troubleshooting.html",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue