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
|
2016-06-22 06:42:41 +02:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-03-04 07:45:29 +01:00
|
|
|
import argparse
|
2016-05-12 06:45:35 +02:00
|
|
|
import builtins
|
2020-11-25 02:54:41 +01:00
|
|
|
import cProfile
|
2019-12-20 10:19:03 +01:00
|
|
|
import getpass
|
|
|
|
import locale
|
2019-12-23 14:37:27 +01:00
|
|
|
import os
|
2019-12-20 10:19:03 +01:00
|
|
|
import sys
|
|
|
|
import tempfile
|
2019-12-23 14:37:27 +01:00
|
|
|
import traceback
|
2021-10-03 10:59:42 +02:00
|
|
|
from typing import Any, Callable, Optional, cast
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
import anki.lang
|
2021-01-31 06:55:08 +01:00
|
|
|
from anki._backend import RustBackend
|
2021-07-11 06:51:25 +02:00
|
|
|
from anki.buildinfo import version as _version
|
|
|
|
from anki.collection import Collection
|
2012-12-21 10:04:26 +01:00
|
|
|
from anki.consts import HELP_SITE
|
2019-12-20 10:19:03 +01:00
|
|
|
from anki.utils import checksum, isLin, isMac
|
2021-08-28 20:37:31 +02:00
|
|
|
from aqt import gui_hooks
|
2019-12-20 10:19:03 +01:00
|
|
|
from aqt.qt import *
|
2020-11-21 03:16:26 +01:00
|
|
|
from aqt.utils import TR, locale_dir, tr
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-04 09:32:36 +02:00
|
|
|
if sys.version_info[0] < 3 or sys.version_info[1] < 9:
|
|
|
|
raise Exception("Anki requires Python 3.9+")
|
2021-07-11 06:51:25 +02:00
|
|
|
|
|
|
|
# ensure unicode filenames are supported
|
|
|
|
try:
|
|
|
|
"テスト".encode(sys.getfilesystemencoding())
|
|
|
|
except UnicodeEncodeError as exc:
|
|
|
|
raise Exception("Anki requires a UTF-8 locale.") from exc
|
|
|
|
|
|
|
|
# compat aliases
|
|
|
|
anki.version = _version # type: ignore
|
|
|
|
anki.Collection = Collection # type: ignore
|
|
|
|
|
2020-08-01 03:30:16 +02:00
|
|
|
# we want to be able to print unicode debug info to console without
|
|
|
|
# fear of a traceback on systems with the console set to ASCII
|
2020-08-06 01:05:26 +02:00
|
|
|
try:
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8") # type: ignore
|
|
|
|
sys.stderr.reconfigure(encoding="utf-8") # type: ignore
|
|
|
|
except AttributeError:
|
|
|
|
# on Windows without console, NullWriter doesn't support this
|
|
|
|
pass
|
2020-08-01 03:30:16 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
appVersion = _version
|
2020-02-29 01:37:46 +01:00
|
|
|
appWebsite = "https://apps.ankiweb.net/"
|
|
|
|
appDonate = "https://apps.ankiweb.net/support/"
|
2020-02-29 12:43:37 +01:00
|
|
|
appShared = "https://ankiweb.net/shared/"
|
2019-12-23 01:34:10 +01:00
|
|
|
appUpdate = "https://ankiweb.net/update/desktop"
|
|
|
|
appHelpSite = HELP_SITE
|
2019-12-19 00:58:16 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
from aqt.main import AnkiQt # isort:skip
|
2020-12-22 04:01:06 +01:00
|
|
|
from aqt.profiles import ProfileManager, AnkiRestart, VideoDriver # isort:skip
|
2019-12-19 00:58:16 +01:00
|
|
|
|
2020-11-25 02:54:41 +01:00
|
|
|
profiler: Optional[cProfile.Profile] = None
|
2019-12-23 01:34:10 +01:00
|
|
|
mw: Optional[AnkiQt] = None # set on init
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
moduleDir = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]
|
|
|
|
|
|
|
|
try:
|
|
|
|
import aqt.forms
|
2016-05-12 06:45:35 +02:00
|
|
|
except ImportError as e:
|
2012-12-21 08:51:59 +01:00
|
|
|
if "forms" in str(e):
|
2016-05-12 06:45:35 +02:00
|
|
|
print("If you're running from git, did you run build_ui.sh?")
|
|
|
|
print()
|
2012-12-21 08:51:59 +01:00
|
|
|
raise
|
|
|
|
|
2013-10-04 00:37:19 +02:00
|
|
|
|
2017-09-10 07:15:12 +02:00
|
|
|
# Dialog manager
|
2017-08-16 04:45:33 +02:00
|
|
|
##########################################################################
|
2017-09-10 07:15:12 +02:00
|
|
|
# ensures only one copy of the window is open at once, and provides
|
|
|
|
# a way for dialogs to clean up asynchronously when collection closes
|
|
|
|
|
|
|
|
# to integrate a new window:
|
|
|
|
# - add it to _dialogs
|
|
|
|
# - define close behaviour, by either:
|
|
|
|
# -- setting silentlyClose=True to have it close immediately
|
|
|
|
# -- define a closeWithCallback() method
|
|
|
|
# - have the window opened via aqt.dialogs.open(<name>, self)
|
2018-11-28 10:16:23 +01:00
|
|
|
# - have a method reopen(*args), called if the user ask to open the window a second time. Arguments passed are the same than for original opening.
|
2017-09-10 07:15:12 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
# - make preferences modal? cmd+q does wrong thing
|
2017-09-10 07:15:12 +02:00
|
|
|
|
|
|
|
|
2021-03-24 04:17:12 +01:00
|
|
|
from aqt import addcards, addons, browser, editcurrent, filtered_deck # isort:skip
|
2020-02-04 02:41:20 +01:00
|
|
|
from aqt import stats, about, preferences, mediasync # isort:skip
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-02-06 23:21:33 +01:00
|
|
|
class DialogManager:
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
_dialogs: dict[str, list] = {
|
2017-09-10 07:15:12 +02:00
|
|
|
"AddCards": [addcards.AddCards, None],
|
2020-12-26 18:07:37 +01:00
|
|
|
"AddonsDialog": [addons.AddonsDialog, None],
|
2017-09-10 07:15:12 +02:00
|
|
|
"Browser": [browser.Browser, None],
|
|
|
|
"EditCurrent": [editcurrent.EditCurrent, None],
|
2021-03-24 04:17:12 +01:00
|
|
|
"FilteredDeckConfigDialog": [filtered_deck.FilteredDeckConfigDialog, None],
|
2017-09-10 07:15:12 +02:00
|
|
|
"DeckStats": [stats.DeckStats, None],
|
2020-06-30 09:08:10 +02:00
|
|
|
"NewDeckStats": [stats.NewDeckStats, None],
|
2017-09-10 07:15:12 +02:00
|
|
|
"About": [about.show, None],
|
|
|
|
"Preferences": [preferences.Preferences, None],
|
2020-02-04 02:48:51 +01:00
|
|
|
"sync_log": [mediasync.MediaSyncDialog, None],
|
2017-09-10 07:15:12 +02:00
|
|
|
}
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 08:50:19 +01:00
|
|
|
def open(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
2012-12-21 08:51:59 +01:00
|
|
|
(creator, instance) = self._dialogs[name]
|
|
|
|
if instance:
|
2018-08-01 13:07:19 +02:00
|
|
|
if instance.windowState() & Qt.WindowMinimized:
|
|
|
|
instance.setWindowState(instance.windowState() & ~Qt.WindowMinimized)
|
2012-12-21 08:51:59 +01:00
|
|
|
instance.activateWindow()
|
|
|
|
instance.raise_()
|
2019-12-23 01:34:10 +01:00
|
|
|
if hasattr(instance, "reopen"):
|
2021-02-01 08:50:19 +01:00
|
|
|
instance.reopen(*args, **kwargs)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-02-01 08:50:19 +01:00
|
|
|
instance = creator(*args, **kwargs)
|
2012-12-21 08:51:59 +01:00
|
|
|
self._dialogs[name][1] = instance
|
2021-08-28 20:37:31 +02:00
|
|
|
gui_hooks.dialog_manager_did_open_dialog(self, name, instance)
|
2021-02-01 08:50:19 +01:00
|
|
|
return instance
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def markClosed(self, name: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self._dialogs[name] = [self._dialogs[name][0], None]
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def allClosed(self) -> bool:
|
2017-08-16 04:45:33 +02:00
|
|
|
return not any(x[1] for x in self._dialogs.values())
|
|
|
|
|
2020-03-07 17:29:37 +01:00
|
|
|
def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]:
|
2017-08-16 04:45:33 +02:00
|
|
|
# can we close immediately?
|
|
|
|
if self.allClosed():
|
|
|
|
onsuccess()
|
2020-03-07 17:29:37 +01:00
|
|
|
return None
|
2017-08-16 04:45:33 +02:00
|
|
|
|
|
|
|
# ask all windows to close and await a reply
|
|
|
|
for (name, (creator, instance)) in self._dialogs.items():
|
|
|
|
if not instance:
|
|
|
|
continue
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def callback() -> None:
|
2017-08-16 04:45:33 +02:00
|
|
|
if self.allClosed():
|
|
|
|
onsuccess()
|
|
|
|
else:
|
|
|
|
# still waiting for others to close
|
|
|
|
pass
|
|
|
|
|
2017-09-10 07:15:12 +02:00
|
|
|
if getattr(instance, "silentlyClose", False):
|
|
|
|
instance.close()
|
|
|
|
callback()
|
|
|
|
else:
|
|
|
|
instance.closeWithCallback(callback)
|
2017-08-16 04:45:33 +02:00
|
|
|
|
2013-04-11 12:23:32 +02:00
|
|
|
return True
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-03-07 17:35:09 +01:00
|
|
|
def register_dialog(
|
|
|
|
self, name: str, creator: Union[Callable, type], instance: Optional[Any] = None
|
2021-02-02 14:30:53 +01:00
|
|
|
) -> None:
|
2020-03-07 17:35:09 +01:00
|
|
|
"""Allows add-ons to register a custom dialog to be managed by Anki's dialog
|
2020-03-07 17:43:21 +01:00
|
|
|
manager, which ensures that only one copy of the window is open at once,
|
|
|
|
and that the dialog cleans up asynchronously when the collection closes
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-03-07 17:43:21 +01:00
|
|
|
Please note that dialogs added in this manner need to define a close behavior
|
|
|
|
by either:
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-03-07 17:43:21 +01:00
|
|
|
- setting `dialog.silentlyClose = True` to have it close immediately
|
|
|
|
- define a `dialog.closeWithCallback()` method that is called when closed
|
|
|
|
by the dialog manager
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-03-07 17:43:21 +01:00
|
|
|
TODO?: Implement more restrictive type check to ensure these requirements
|
|
|
|
are met
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-03-07 17:35:09 +01:00
|
|
|
Arguments:
|
|
|
|
name {str} -- Name/identifier of the dialog in question
|
|
|
|
creator {Union[Callable, type]} -- A class or function to create new
|
|
|
|
dialog instances with
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-03-07 17:35:09 +01:00
|
|
|
Keyword Arguments:
|
|
|
|
instance {Optional[Any]} -- An optional existing instance of the dialog
|
|
|
|
(default: {None})
|
|
|
|
"""
|
|
|
|
self._dialogs[name] = [creator, instance]
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
dialogs = DialogManager()
|
|
|
|
|
|
|
|
# Language handling
|
|
|
|
##########################################################################
|
|
|
|
# Qt requires its translator to be installed before any GUI widgets are
|
2020-11-18 04:22:51 +01:00
|
|
|
# loaded, and we need the Qt language to match the i18n language or
|
2012-12-21 08:51:59 +01:00
|
|
|
# translated shortcuts will not work.
|
|
|
|
|
2020-03-14 00:45:00 +01:00
|
|
|
# A reference to the Qt translator needs to be held to prevent it from
|
|
|
|
# being immediately deallocated.
|
2019-12-20 06:07:40 +01:00
|
|
|
_qtrans: Optional[QTranslator] = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-03-14 00:45:00 +01:00
|
|
|
def setupLangAndBackend(
|
2020-11-21 03:16:26 +01:00
|
|
|
pm: ProfileManager,
|
|
|
|
app: QApplication,
|
|
|
|
force: Optional[str] = None,
|
|
|
|
firstTime: bool = False,
|
2020-03-14 00:45:00 +01:00
|
|
|
) -> RustBackend:
|
2020-01-02 10:43:19 +01:00
|
|
|
global _qtrans
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
2019-12-23 01:34:10 +01:00
|
|
|
locale.setlocale(locale.LC_ALL, "")
|
2012-12-21 08:51:59 +01:00
|
|
|
except:
|
|
|
|
pass
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-02-16 06:14:08 +01:00
|
|
|
# add _ and ngettext globals used by legacy code
|
2021-02-02 14:30:53 +01:00
|
|
|
def fn__(arg) -> None: # type: ignore
|
2019-03-04 03:08:48 +01:00
|
|
|
print("".join(traceback.format_stack()[-2]))
|
2020-11-18 02:53:33 +01:00
|
|
|
print("_ global will break in the future; please see anki/lang.py")
|
|
|
|
return arg
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def fn_ngettext(a, b, c) -> None: # type: ignore
|
2019-03-07 09:34:22 +01:00
|
|
|
print("".join(traceback.format_stack()[-2]))
|
2020-11-18 02:53:33 +01:00
|
|
|
print("ngettext global will break in the future; please see anki/lang.py")
|
|
|
|
return b
|
2019-03-07 09:34:22 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
builtins.__dict__["_"] = fn__
|
|
|
|
builtins.__dict__["ngettext"] = fn_ngettext
|
2020-02-16 06:14:08 +01:00
|
|
|
|
|
|
|
# get lang and normalize into ja/zh-CN form
|
2020-11-21 03:16:26 +01:00
|
|
|
if firstTime:
|
|
|
|
lang = pm.meta["defaultLang"]
|
|
|
|
else:
|
|
|
|
lang = force or pm.meta["defaultLang"]
|
2020-02-16 06:14:08 +01:00
|
|
|
lang = anki.lang.lang_to_disk_lang(lang)
|
|
|
|
|
2020-11-21 03:16:26 +01:00
|
|
|
if not firstTime:
|
|
|
|
# set active language
|
2021-03-26 00:40:41 +01:00
|
|
|
anki.lang.set_lang(lang)
|
2020-02-16 06:14:08 +01:00
|
|
|
|
|
|
|
# switch direction for RTL languages
|
2020-07-21 03:52:25 +02:00
|
|
|
if anki.lang.is_rtl(lang):
|
2012-12-21 08:51:59 +01:00
|
|
|
app.setLayoutDirection(Qt.RightToLeft)
|
|
|
|
else:
|
|
|
|
app.setLayoutDirection(Qt.LeftToRight)
|
2020-02-16 06:14:08 +01:00
|
|
|
|
|
|
|
# load qt translations
|
2012-12-21 08:51:59 +01:00
|
|
|
_qtrans = QTranslator()
|
2021-02-04 11:28:25 +01:00
|
|
|
|
|
|
|
from aqt.utils import aqt_data_folder
|
|
|
|
|
|
|
|
if isMac and getattr(sys, "frozen", False):
|
|
|
|
qt_dir = os.path.abspath(
|
|
|
|
os.path.join(aqt_data_folder(), "..", "qt_translations")
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
qt_dir = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
|
2020-02-16 06:14:08 +01:00
|
|
|
qt_lang = lang.replace("-", "_")
|
2021-02-11 01:09:06 +01:00
|
|
|
if _qtrans.load(f"qtbase_{qt_lang}", qt_dir):
|
2012-12-21 08:51:59 +01:00
|
|
|
app.installTranslator(_qtrans)
|
|
|
|
|
2020-03-14 00:45:00 +01:00
|
|
|
return anki.lang.current_i18n
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# App initialisation
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class AnkiApp(QApplication):
|
|
|
|
|
|
|
|
# Single instance support on Win32/Linux
|
|
|
|
##################################################
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
appMsg = pyqtSignal(str)
|
|
|
|
|
2021-02-11 01:09:06 +01:00
|
|
|
KEY = f"anki{checksum(getpass.getuser())}"
|
2018-10-28 05:17:16 +01:00
|
|
|
TMOUT = 30000
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def __init__(self, argv: list[str]) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
QApplication.__init__(self, argv)
|
|
|
|
self._argv = argv
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def secondInstance(self) -> bool:
|
2013-10-04 00:37:19 +02:00
|
|
|
# we accept only one command line argument. if it's missing, send
|
|
|
|
# a blank screen to just raise the existing window
|
|
|
|
opts, args = parseArgs(self._argv)
|
|
|
|
buf = "raise"
|
|
|
|
if args and args[0]:
|
|
|
|
buf = os.path.abspath(args[0])
|
|
|
|
if self.sendMsg(buf):
|
2016-05-12 06:45:35 +02:00
|
|
|
print("Already running; reusing existing instance.")
|
2013-10-04 00:37:19 +02:00
|
|
|
return True
|
|
|
|
else:
|
|
|
|
# send failed, so we're the first instance or the
|
|
|
|
# previous instance died
|
2012-12-21 08:51:59 +01:00
|
|
|
QLocalServer.removeServer(self.KEY)
|
|
|
|
self._srv = QLocalServer(self)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self._srv.newConnection, self.onRecv)
|
2012-12-21 08:51:59 +01:00
|
|
|
self._srv.listen(self.KEY)
|
2013-10-04 00:37:19 +02:00
|
|
|
return False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def sendMsg(self, txt: str) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
sock = QLocalSocket(self)
|
|
|
|
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
|
|
|
|
if not sock.waitForConnected(self.TMOUT):
|
2013-10-04 00:37:19 +02:00
|
|
|
# first instance or previous instance dead
|
|
|
|
return False
|
2016-05-31 10:51:40 +02:00
|
|
|
sock.write(txt.encode("utf8"))
|
2012-12-21 08:51:59 +01:00
|
|
|
if not sock.waitForBytesWritten(self.TMOUT):
|
2014-08-26 08:25:22 +02:00
|
|
|
# existing instance running but hung
|
2019-12-23 01:34:10 +01:00
|
|
|
QMessageBox.warning(
|
|
|
|
None,
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_anki_is_running(),
|
|
|
|
tr.qt_misc_if_instance_is_not_responding(),
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2018-10-28 05:17:16 +01:00
|
|
|
|
|
|
|
sys.exit(1)
|
2012-12-21 08:51:59 +01:00
|
|
|
sock.disconnectFromServer()
|
2013-10-04 00:37:19 +02:00
|
|
|
return True
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onRecv(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
sock = self._srv.nextPendingConnection()
|
|
|
|
if not sock.waitForReadyRead(self.TMOUT):
|
|
|
|
sys.stderr.write(sock.errorString())
|
|
|
|
return
|
2021-03-17 05:51:59 +01:00
|
|
|
path = bytes(cast(bytes, sock.readAll())).decode("utf8")
|
2020-08-02 02:16:54 +02:00
|
|
|
self.appMsg.emit(path) # type: ignore
|
2012-12-21 08:51:59 +01:00
|
|
|
sock.disconnectFromServer()
|
|
|
|
|
|
|
|
# OS X file/url handler
|
|
|
|
##################################################
|
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def event(self, evt: QEvent) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
if evt.type() == QEvent.FileOpen:
|
2020-08-02 02:16:54 +02:00
|
|
|
self.appMsg.emit(evt.file() or "raise") # type: ignore
|
2012-12-21 08:51:59 +01:00
|
|
|
return True
|
|
|
|
return QApplication.event(self, evt)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def parseArgs(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
|
2012-12-21 08:51:59 +01:00
|
|
|
"Returns (opts, args)."
|
2013-04-16 12:54:23 +02:00
|
|
|
# py2app fails to strip this in some instances, then anki dies
|
|
|
|
# as there's no such profile
|
|
|
|
if isMac and len(argv) > 1 and argv[1].startswith("-psn"):
|
|
|
|
argv = [argv[0]]
|
2021-02-11 01:09:06 +01:00
|
|
|
parser = argparse.ArgumentParser(description=f"Anki {appVersion}")
|
2020-01-03 17:57:33 +01:00
|
|
|
parser.usage = "%(prog)s [OPTIONS] [file to import/add-on to install]"
|
2019-03-04 07:45:29 +01:00
|
|
|
parser.add_argument("-b", "--base", help="path to base folder", default="")
|
|
|
|
parser.add_argument("-p", "--profile", help="profile name to load", default="")
|
|
|
|
parser.add_argument("-l", "--lang", help="interface language (en, de, etc)")
|
2020-10-12 21:57:49 +02:00
|
|
|
parser.add_argument(
|
2021-01-11 05:11:18 +01:00
|
|
|
"-v", "--version", help="print the Anki version and exit", action="store_true"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--safemode", help="disable add-ons and automatic syncing", action="store_true"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--syncserver",
|
|
|
|
help="skip GUI and start a local sync server",
|
|
|
|
action="store_true",
|
2020-10-12 21:57:49 +02:00
|
|
|
)
|
2019-03-04 07:45:29 +01:00
|
|
|
return parser.parse_known_args(argv[1:])
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def setupGL(pm: aqt.profiles.ProfileManager) -> None:
|
2018-08-08 15:48:25 +02:00
|
|
|
if isMac:
|
|
|
|
return
|
|
|
|
|
2020-12-22 04:01:06 +01:00
|
|
|
driver = pm.video_driver()
|
2018-08-08 15:48:25 +02:00
|
|
|
|
|
|
|
# work around pyqt loading wrong GL library
|
|
|
|
if isLin:
|
|
|
|
import ctypes
|
2019-12-23 01:34:10 +01:00
|
|
|
|
|
|
|
ctypes.CDLL("libGL.so.1", ctypes.RTLD_GLOBAL)
|
2018-08-08 15:48:25 +02:00
|
|
|
|
|
|
|
# catch opengl errors
|
2021-02-02 14:30:53 +01:00
|
|
|
def msgHandler(category: Any, ctx: Any, msg: Any) -> None:
|
2020-05-31 20:50:57 +02:00
|
|
|
if category == QtDebugMsg:
|
|
|
|
category = "debug"
|
|
|
|
elif category == QtInfoMsg:
|
|
|
|
category = "info"
|
|
|
|
elif category == QtWarningMsg:
|
|
|
|
category = "warning"
|
|
|
|
elif category == QtCriticalMsg:
|
|
|
|
category = "critical"
|
|
|
|
elif category == QtDebugMsg:
|
|
|
|
category = "debug"
|
|
|
|
elif category == QtFatalMsg:
|
|
|
|
category = "fatal"
|
|
|
|
elif category == QtSystemMsg:
|
|
|
|
category = "system"
|
|
|
|
else:
|
|
|
|
category = "unknown"
|
2020-05-31 19:40:05 +02:00
|
|
|
context = ""
|
|
|
|
if ctx.file:
|
|
|
|
context += f"{ctx.file}:"
|
|
|
|
if ctx.line:
|
|
|
|
context += f"{ctx.line},"
|
|
|
|
if ctx.function:
|
|
|
|
context += f"{ctx.function}"
|
|
|
|
if context:
|
|
|
|
context = f"'{context}'"
|
2018-08-08 15:48:25 +02:00
|
|
|
if "Failed to create OpenGL context" in msg:
|
2019-12-23 01:34:10 +01:00
|
|
|
QMessageBox.critical(
|
|
|
|
None,
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_error(),
|
2021-03-26 05:38:15 +01:00
|
|
|
tr.qt_misc_error_loading_graphics_driver(
|
2020-12-22 04:01:06 +01:00
|
|
|
mode=driver.value,
|
|
|
|
context=context,
|
2020-11-21 03:16:26 +01:00
|
|
|
),
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2020-12-22 04:01:06 +01:00
|
|
|
pm.set_video_driver(driver.next())
|
2018-08-08 15:48:25 +02:00
|
|
|
return
|
|
|
|
else:
|
2020-05-31 20:50:57 +02:00
|
|
|
print(f"Qt {category}: {msg} {context}")
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2018-08-08 15:48:25 +02:00
|
|
|
qInstallMessageHandler(msgHandler)
|
|
|
|
|
2020-12-22 04:01:06 +01:00
|
|
|
if driver == VideoDriver.OpenGL:
|
|
|
|
pass
|
2018-08-08 15:48:25 +02:00
|
|
|
else:
|
2020-12-22 04:01:06 +01:00
|
|
|
if isWin:
|
|
|
|
os.environ["QT_OPENGL"] = driver.value
|
|
|
|
elif isMac:
|
|
|
|
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
|
|
|
|
elif isLin:
|
|
|
|
os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1"
|
2018-08-08 15:48:25 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-05 08:28:49 +02:00
|
|
|
PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE")
|
2020-05-04 00:47:03 +02:00
|
|
|
|
2020-05-05 08:28:49 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def write_profile_results() -> None:
|
2020-08-02 02:16:54 +02:00
|
|
|
|
2020-05-05 08:28:49 +02:00
|
|
|
profiler.disable()
|
2021-03-22 00:43:48 +01:00
|
|
|
profile = os.path.join(os.environ.get("BUILD_WORKSPACE_DIRECTORY", ""), "anki.prof")
|
|
|
|
profiler.dump_stats(profile)
|
2020-05-16 04:53:01 +02:00
|
|
|
profiler.dump_stats("anki.prof")
|
|
|
|
print("profile stats written to anki.prof")
|
2020-12-17 03:03:24 +01:00
|
|
|
print("use 'bazel run qt:profile' to explore")
|
2020-05-04 00:47:03 +02:00
|
|
|
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def run() -> None:
|
2014-07-07 03:41:56 +02:00
|
|
|
try:
|
|
|
|
_run()
|
2016-05-12 06:45:35 +02:00
|
|
|
except Exception as e:
|
2018-09-24 08:24:11 +02:00
|
|
|
traceback.print_exc()
|
2019-12-23 01:34:10 +01:00
|
|
|
QMessageBox.critical(
|
|
|
|
None,
|
|
|
|
"Startup Error",
|
2021-02-11 01:09:06 +01:00
|
|
|
f"Please notify support of this error:\n\n{traceback.format_exc()}",
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
|
|
|
|
2014-07-07 03:41:56 +02:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiApp]:
|
2017-09-23 17:01:29 +02:00
|
|
|
"""Start AnkiQt application or reuse an existing instance if one exists.
|
|
|
|
|
|
|
|
If the function is invoked with exec=False, the AnkiQt will not enter
|
|
|
|
the main event loop - instead the application object will be returned.
|
|
|
|
|
|
|
|
The 'exec' and 'argv' arguments will be useful for testing purposes.
|
|
|
|
|
|
|
|
If no 'argv' is supplied then 'sys.argv' will be used.
|
|
|
|
"""
|
2012-12-21 08:51:59 +01:00
|
|
|
global mw
|
2020-05-05 04:29:48 +02:00
|
|
|
global profiler
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-09-21 05:02:39 +02:00
|
|
|
if argv is None:
|
|
|
|
argv = sys.argv
|
|
|
|
|
2013-02-21 20:51:06 +01:00
|
|
|
# parse args
|
2017-09-21 05:02:39 +02:00
|
|
|
opts, args = parseArgs(argv)
|
2013-02-21 20:51:06 +01:00
|
|
|
|
2020-06-19 18:11:04 +02:00
|
|
|
if opts.version:
|
2021-01-11 05:11:18 +01:00
|
|
|
print(f"Anki {appVersion}")
|
2021-02-01 14:28:21 +01:00
|
|
|
return None
|
2021-01-11 05:11:18 +01:00
|
|
|
elif opts.syncserver:
|
|
|
|
from anki.syncserver import serve
|
|
|
|
|
|
|
|
serve()
|
2021-02-01 14:28:21 +01:00
|
|
|
return None
|
2020-06-19 18:11:04 +02:00
|
|
|
|
2020-05-05 08:28:49 +02:00
|
|
|
if PROFILE_CODE:
|
2020-05-04 00:47:03 +02:00
|
|
|
|
2020-05-05 04:15:23 +02:00
|
|
|
profiler = cProfile.Profile()
|
|
|
|
profiler.enable()
|
2020-05-04 00:47:03 +02:00
|
|
|
|
2021-09-14 01:53:23 +02:00
|
|
|
if (
|
|
|
|
getattr(sys, "frozen", False)
|
|
|
|
and os.getenv("QT_QPA_PLATFORM") == "wayland"
|
|
|
|
and not os.getenv("ANKI_WAYLAND")
|
|
|
|
):
|
|
|
|
# users need to opt in to wayland support, given the issues it has
|
2021-09-20 13:42:16 +02:00
|
|
|
print("Wayland support is disabled by default due to bugs.")
|
|
|
|
print("You can force it on with an env var: ANKI_WAYLAND=1")
|
2021-09-14 01:53:23 +02:00
|
|
|
os.environ["QT_QPA_PLATFORM"] = "xcb"
|
|
|
|
|
2020-11-21 03:16:26 +01:00
|
|
|
# default to specified/system language before getting user's preference so that we can localize some more strings
|
|
|
|
lang = anki.lang.get_def_lang(opts.lang)
|
2021-03-26 00:40:41 +01:00
|
|
|
anki.lang.set_lang(lang[1])
|
2020-11-21 03:16:26 +01:00
|
|
|
|
2018-08-08 15:48:25 +02:00
|
|
|
# profile manager
|
2019-12-24 11:33:39 +01:00
|
|
|
pm = None
|
|
|
|
try:
|
|
|
|
pm = ProfileManager(opts.base)
|
|
|
|
pmLoadResult = pm.setupMeta()
|
2020-05-10 23:28:06 +02:00
|
|
|
except AnkiRestart as error:
|
|
|
|
if error.exitcode:
|
|
|
|
sys.exit(error.exitcode)
|
2021-02-01 14:28:21 +01:00
|
|
|
return None
|
2019-12-24 11:33:39 +01:00
|
|
|
except:
|
|
|
|
# will handle below
|
2020-01-14 05:56:28 +01:00
|
|
|
traceback.print_exc()
|
|
|
|
pm = None
|
2018-07-28 08:38:22 +02:00
|
|
|
|
2019-12-24 11:33:39 +01:00
|
|
|
if pm:
|
|
|
|
# gl workarounds
|
|
|
|
setupGL(pm)
|
|
|
|
# apply user-provided scale factor
|
|
|
|
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
|
2016-08-01 04:16:06 +02:00
|
|
|
|
2017-11-27 02:01:15 +01:00
|
|
|
# opt in to full hidpi support?
|
|
|
|
if not os.environ.get("ANKI_NOHIGHDPI"):
|
|
|
|
QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
2021-02-05 06:26:12 +01:00
|
|
|
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
|
2019-12-17 09:43:32 +01:00
|
|
|
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
|
|
|
|
os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "PassThrough"
|
|
|
|
|
2019-08-16 22:35:39 +02:00
|
|
|
# Opt into software rendering. Useful for buggy systems.
|
|
|
|
if os.environ.get("ANKI_SOFTWAREOPENGL"):
|
|
|
|
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
|
|
|
|
|
2020-07-26 02:13:53 +02:00
|
|
|
if (
|
|
|
|
isWin
|
|
|
|
and (qtminor == 14 or (qtminor == 15 and qtpoint == 0))
|
|
|
|
and "QT_QPA_PLATFORM" not in os.environ
|
|
|
|
):
|
2020-07-24 03:57:37 +02:00
|
|
|
os.environ["QT_QPA_PLATFORM"] = "windows:altgr"
|
2020-07-24 02:32:50 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# create the app
|
|
|
|
QCoreApplication.setApplicationName("Anki")
|
2019-07-18 23:14:34 +02:00
|
|
|
QGuiApplication.setDesktopFileName("anki.desktop")
|
2019-07-20 13:10:59 +02:00
|
|
|
app = AnkiApp(argv)
|
2012-12-21 08:51:59 +01:00
|
|
|
if app.secondInstance():
|
|
|
|
# we've signaled the primary instance, so we should close
|
2021-02-01 14:28:21 +01:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-24 11:33:39 +01:00
|
|
|
if not pm:
|
|
|
|
QMessageBox.critical(
|
|
|
|
None,
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_error(),
|
|
|
|
tr.profiles_could_not_create_data_folder(),
|
2019-12-24 11:33:39 +01:00
|
|
|
)
|
2021-02-01 14:28:21 +01:00
|
|
|
return None
|
2019-12-24 11:33:39 +01:00
|
|
|
|
2013-04-15 06:46:07 +02:00
|
|
|
# disable icons on mac; this must be done before window created
|
|
|
|
if isMac:
|
|
|
|
app.setAttribute(Qt.AA_DontShowIconsInMenus)
|
|
|
|
|
2019-12-17 08:59:19 +01:00
|
|
|
# disable help button in title bar on qt versions that support it
|
|
|
|
if isWin and qtminor >= 10:
|
|
|
|
QApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton)
|
|
|
|
|
2018-10-11 07:49:04 +02:00
|
|
|
# proxy configured?
|
2020-08-31 04:05:36 +02:00
|
|
|
from urllib.request import getproxies, proxy_bypass
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-31 23:18:13 +02:00
|
|
|
disable_proxies = False
|
|
|
|
try:
|
|
|
|
if "http" in getproxies():
|
|
|
|
# if it's not set up to bypass localhost, we'll
|
|
|
|
# need to disable proxies in the webviews
|
|
|
|
if not proxy_bypass("127.0.0.1"):
|
|
|
|
disable_proxies = True
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
# proxy_bypass can't handle unicode in hostnames; assume we need
|
|
|
|
# to disable proxies
|
|
|
|
disable_proxies = True
|
|
|
|
|
|
|
|
if disable_proxies:
|
|
|
|
print("webview proxy use disabled")
|
|
|
|
proxy = QNetworkProxy()
|
|
|
|
proxy.setType(QNetworkProxy.NoProxy)
|
|
|
|
QNetworkProxy.setApplicationProxy(proxy)
|
2018-10-11 07:49:04 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# we must have a usable temp dir
|
|
|
|
try:
|
|
|
|
tempfile.gettempdir()
|
|
|
|
except:
|
|
|
|
QMessageBox.critical(
|
2019-12-23 01:34:10 +01:00
|
|
|
None,
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_error(),
|
|
|
|
tr.qt_misc_no_temp_folder(),
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-02-01 14:28:21 +01:00
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-05 06:44:07 +02:00
|
|
|
# make image resources available
|
|
|
|
from aqt.utils import aqt_data_folder
|
|
|
|
|
|
|
|
QDir.addSearchPath("icons", os.path.join(aqt_data_folder(), "qt", "icons"))
|
|
|
|
|
2019-12-24 11:23:36 +01:00
|
|
|
if pmLoadResult.firstTime:
|
2020-11-21 03:16:26 +01:00
|
|
|
pm.setDefaultLang(lang[0])
|
2019-12-24 11:23:36 +01:00
|
|
|
|
2019-12-19 00:38:36 +01:00
|
|
|
if pmLoadResult.loadError:
|
|
|
|
QMessageBox.warning(
|
2019-12-23 01:34:10 +01:00
|
|
|
None,
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.profiles_prefs_corrupt_title(),
|
|
|
|
tr.profiles_prefs_file_is_corrupt(),
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2018-08-08 15:48:25 +02:00
|
|
|
|
|
|
|
if opts.profile:
|
|
|
|
pm.openProfile(opts.profile)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-03-14 00:45:00 +01:00
|
|
|
# i18n & backend
|
2020-11-21 03:16:26 +01:00
|
|
|
backend = setupLangAndBackend(pm, app, opts.lang, pmLoadResult.firstTime)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-12-22 04:01:06 +01:00
|
|
|
driver = pm.video_driver()
|
|
|
|
if isLin and driver == VideoDriver.OpenGL:
|
2018-12-18 10:29:34 +01:00
|
|
|
from aqt.utils import gfxDriverIsBroken
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2018-12-18 10:29:34 +01:00
|
|
|
if gfxDriverIsBroken():
|
2020-12-22 04:01:06 +01:00
|
|
|
pm.set_video_driver(driver.next())
|
2019-12-23 01:34:10 +01:00
|
|
|
QMessageBox.critical(
|
|
|
|
None,
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.qt_misc_error(),
|
|
|
|
tr.qt_misc_incompatible_video_driver(),
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2018-12-18 10:29:34 +01:00
|
|
|
sys.exit(1)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# load the main window
|
|
|
|
import aqt.main
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-03-14 00:45:00 +01:00
|
|
|
mw = aqt.main.AnkiQt(app, pm, backend, opts, args)
|
2017-09-21 05:02:39 +02:00
|
|
|
if exec:
|
|
|
|
app.exec()
|
|
|
|
else:
|
|
|
|
return app
|
2020-05-04 00:47:03 +02:00
|
|
|
|
2020-05-05 08:28:49 +02:00
|
|
|
if PROFILE_CODE:
|
2020-05-16 04:53:01 +02:00
|
|
|
write_profile_results()
|
2021-02-01 14:28:21 +01:00
|
|
|
|
|
|
|
return None
|