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
from anki import version as _version
2013-10-04 00:37:19 +02:00
import getpass
2013-10-21 02:22:14 +02:00
import sys
2019-03-04 07:45:29 +01:00
import argparse
2013-10-21 02:22:14 +02:00
import tempfile
2016-05-12 06:45:35 +02:00
import builtins
2013-10-21 02:22:14 +02:00
import locale
import gettext
2012-12-21 08:51:59 +01:00
from aqt . qt import *
import anki . lang
2012-12-21 10:04:26 +01:00
from anki . consts import HELP_SITE
from anki . lang import langDir
2016-08-01 04:16:06 +02:00
from anki . utils import isMac , isLin
2012-12-21 08:51:59 +01:00
2013-10-01 20:50:19 +02:00
appVersion = _version
2012-12-21 08:51:59 +01:00
appWebsite = " http://ankisrs.net/ "
appChanges = " http://ankisrs.net/docs/changes.html "
appDonate = " http://ankisrs.net/support/ "
appShared = " https://ankiweb.net/shared/ "
appUpdate = " https://ankiweb.net/update/desktop "
2012-12-21 10:04:26 +01:00
appHelpSite = HELP_SITE
2012-12-21 08:51:59 +01:00
mw = None # set on init
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
from anki . utils import checksum
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
#- make preferences modal? cmd+q does wrong thing
from aqt import addcards , browser , editcurrent , stats , about , \
preferences
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
2017-09-10 07:15:12 +02:00
_dialogs = {
" AddCards " : [ addcards . AddCards , None ] ,
" Browser " : [ browser . Browser , None ] ,
" EditCurrent " : [ editcurrent . EditCurrent , None ] ,
" DeckStats " : [ stats . DeckStats , None ] ,
" About " : [ about . show , None ] ,
" Preferences " : [ preferences . Preferences , None ] ,
}
2012-12-21 08:51:59 +01:00
def open ( self , name , * args ) :
( 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_ ( )
2018-11-28 10:16:23 +01:00
if hasattr ( instance , " reopen " ) :
instance . reopen ( * args )
2012-12-21 08:51:59 +01:00
return instance
else :
instance = creator ( * args )
self . _dialogs [ name ] [ 1 ] = instance
return instance
2017-08-16 04:45:33 +02:00
def markClosed ( self , name ) :
2012-12-21 08:51:59 +01:00
self . _dialogs [ name ] = [ self . _dialogs [ name ] [ 0 ] , None ]
2017-08-16 04:45:33 +02:00
def allClosed ( self ) :
return not any ( x [ 1 ] for x in self . _dialogs . values ( ) )
def closeAll ( self , onsuccess ) :
# can we close immediately?
if self . allClosed ( ) :
onsuccess ( )
return
# ask all windows to close and await a reply
for ( name , ( creator , instance ) ) in self . _dialogs . items ( ) :
if not instance :
continue
def callback ( ) :
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
dialogs = DialogManager ( )
# Language handling
##########################################################################
# Qt requires its translator to be installed before any GUI widgets are
# loaded, and we need the Qt language to match the gettext language or
# translated shortcuts will not work.
_gtrans = None
_qtrans = None
def setupLang ( pm , app , force = None ) :
global _gtrans , _qtrans
try :
locale . setlocale ( locale . LC_ALL , ' ' )
except :
pass
lang = force or pm . meta [ " defaultLang " ]
dir = langDir ( )
# gettext
_gtrans = gettext . translation (
2012-12-21 10:04:26 +01:00
' anki ' , dir , languages = [ lang ] , fallback = True )
2019-03-07 09:34:22 +01:00
def fn__ ( arg ) :
print ( " accessing _ without importing from anki.lang will break in the future " )
2019-03-04 03:08:48 +01:00
print ( " " . join ( traceback . format_stack ( ) [ - 2 ] ) )
2019-03-07 09:34:22 +01:00
from anki . lang import _
return _ ( arg )
def fn_ngettext ( a , b , c ) :
print ( " accessing ngettext without importing from anki.lang will break in the future " )
print ( " " . join ( traceback . format_stack ( ) [ - 2 ] ) )
from anki . lang import ngettext
return ngettext ( a , b , c )
builtins . __dict__ [ ' _ ' ] = fn__
builtins . __dict__ [ ' ngettext ' ] = fn_ngettext
2012-12-21 08:51:59 +01:00
anki . lang . setLang ( lang , local = False )
if lang in ( " he " , " ar " , " fa " ) :
app . setLayoutDirection ( Qt . RightToLeft )
else :
app . setLayoutDirection ( Qt . LeftToRight )
# qt
_qtrans = QTranslator ( )
if _qtrans . load ( " qt_ " + lang , dir ) :
app . installTranslator ( _qtrans )
# App initialisation
##########################################################################
class AnkiApp ( QApplication ) :
# Single instance support on Win32/Linux
##################################################
2016-05-31 10:51:40 +02:00
appMsg = pyqtSignal ( str )
2013-10-04 00:37:19 +02:00
KEY = " anki " + checksum ( getpass . getuser ( ) )
2018-10-28 05:17:16 +01:00
TMOUT = 30000
2012-12-21 08:51:59 +01:00
def __init__ ( self , argv ) :
QApplication . __init__ ( self , argv )
self . _argv = argv
def secondInstance ( self ) :
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 )
2016-05-31 10:51:40 +02:00
self . _srv . newConnection . connect ( 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
def sendMsg ( self , txt ) :
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
2018-10-28 05:17:16 +01:00
QMessageBox . warning ( None , " Anki Already Running " ,
" If the existing instance of Anki is not responding, please close it using your task manager, or restart your computer. " )
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
def onRecv ( self ) :
sock = self . _srv . nextPendingConnection ( )
if not sock . waitForReadyRead ( self . TMOUT ) :
sys . stderr . write ( sock . errorString ( ) )
return
2016-05-31 10:51:40 +02:00
path = bytes ( sock . readAll ( ) ) . decode ( " utf8 " )
self . appMsg . emit ( path )
2012-12-21 08:51:59 +01:00
sock . disconnectFromServer ( )
# OS X file/url handler
##################################################
def event ( self , evt ) :
if evt . type ( ) == QEvent . FileOpen :
2016-05-31 10:51:40 +02:00
self . appMsg . emit ( evt . file ( ) or " raise " )
2012-12-21 08:51:59 +01:00
return True
return QApplication . event ( self , evt )
def parseArgs ( argv ) :
" 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 ] ]
2019-03-04 07:45:29 +01:00
parser = argparse . ArgumentParser ( description = " Anki " + appVersion )
parser . usage = " %(prog)s [OPTIONS] [file to import] "
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) " )
return parser . parse_known_args ( argv [ 1 : ] )
2012-12-21 08:51:59 +01:00
2018-08-08 15:48:25 +02:00
def setupGL ( pm ) :
if isMac :
return
mode = pm . glMode ( )
# work around pyqt loading wrong GL library
if isLin :
import ctypes
ctypes . CDLL ( ' libGL.so.1 ' , ctypes . RTLD_GLOBAL )
# catch opengl errors
def msgHandler ( type , ctx , msg ) :
if " Failed to create OpenGL context " in msg :
QMessageBox . critical ( None , " Error " , " Error loading ' %s ' graphics driver. Please start Anki again to try next driver. " % mode )
pm . nextGlMode ( )
return
else :
print ( " qt: " , msg )
qInstallMessageHandler ( msgHandler )
if mode == " auto " :
return
elif isLin :
os . environ [ " QT_XCB_FORCE_SOFTWARE_OPENGL " ] = " 1 "
else :
os . environ [ " QT_OPENGL " ] = mode
2012-12-21 08:51:59 +01:00
def run ( ) :
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 ( )
2014-07-07 03:41:56 +02:00
QMessageBox . critical ( None , " Startup Error " ,
" Please notify support of this error: \n \n " +
traceback . format_exc ( ) )
2017-09-21 05:02:39 +02:00
def _run ( argv = None , exec = True ) :
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
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
2018-08-08 15:48:25 +02:00
# profile manager
from aqt . profiles import ProfileManager
pm = ProfileManager ( opts . base )
2018-07-28 08:38:22 +02:00
2018-08-08 15:48:25 +02:00
# gl workarounds
setupGL ( pm )
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 )
2017-08-10 07:02:46 +02:00
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 )
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
return
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 )
2018-10-11 07:49:04 +02:00
# proxy configured?
from urllib . request import proxy_bypass , getproxies
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 " ) :
print ( " webview proxy use disabled " )
proxy = QNetworkProxy ( )
proxy . setType ( QNetworkProxy . NoProxy )
QNetworkProxy . setApplicationProxy ( proxy )
2012-12-21 08:51:59 +01:00
# we must have a usable temp dir
try :
tempfile . gettempdir ( )
except :
QMessageBox . critical (
None , " Error " , """ \
No usable temporary folder found . Make sure C : \\temp exists or TEMP in your \
environment points to a valid , writable folder . """ )
return
2018-08-08 15:48:25 +02:00
pm . setupMeta ( )
if opts . profile :
pm . openProfile ( opts . profile )
2012-12-21 08:51:59 +01:00
# i18n
setupLang ( pm , app , opts . lang )
2018-12-18 10:29:34 +01:00
if isLin and pm . glMode ( ) == " auto " :
from aqt . utils import gfxDriverIsBroken
if gfxDriverIsBroken ( ) :
pm . nextGlMode ( )
QMessageBox . critical ( None , " Error " , " Your video driver is incompatible. Please start Anki again, and Anki will switch to a slower, more compatible mode. " )
sys . exit ( 1 )
2012-12-21 08:51:59 +01:00
# load the main window
import aqt . main
2017-10-03 04:12:57 +02:00
mw = aqt . main . AnkiQt ( app , pm , opts , args )
2017-09-21 05:02:39 +02:00
if exec :
app . exec ( )
else :
return app