2015-05-28 13:32:09 +12:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2016-02-07 00:34:22 +13:00
|
|
|
import faulthandler
|
2016-03-30 05:39:33 +13:00
|
|
|
import os
|
2019-09-06 10:29:18 +12:00
|
|
|
import platform
|
2015-05-28 13:32:09 +12:00
|
|
|
import sys
|
2019-09-03 17:43:55 +12:00
|
|
|
import re
|
2016-02-06 00:25:25 +13:00
|
|
|
|
2017-06-21 01:24:15 +12:00
|
|
|
import manuskript.ui.views.webView
|
2019-09-03 17:43:55 +12:00
|
|
|
from PyQt5.Qt import qVersion
|
2019-09-06 10:29:18 +12:00
|
|
|
from PyQt5.QtCore import QLocale, QTranslator, QSettings, Qt
|
|
|
|
from PyQt5.QtGui import QIcon, QColor, QPalette
|
|
|
|
from PyQt5.QtWidgets import QApplication, qApp, QMessageBox, QStyleFactory
|
2016-02-29 11:48:53 +13:00
|
|
|
|
2016-03-30 05:39:33 +13:00
|
|
|
from manuskript.functions import appPath, writablePath
|
2017-09-29 08:26:04 +13:00
|
|
|
from manuskript.version import getVersion
|
2015-05-28 13:32:09 +12:00
|
|
|
|
2015-06-25 04:39:38 +12:00
|
|
|
faulthandler.enable()
|
2015-06-22 05:37:13 +12:00
|
|
|
|
2019-09-03 17:43:55 +12:00
|
|
|
def warnAboutBuggyLibraries(app):
|
|
|
|
"""Some bugs are out of our reach to fix. The user needs to be warned and perhaps take action themselves."""
|
|
|
|
|
|
|
|
# (Py)Qt 5.11 and 5.12 have a bug that can cause crashes when simply setting up
|
|
|
|
# various UI elements. This has been reported and verified to happen in the
|
|
|
|
# Export (Compile) screen, but due to the nature of the bug, we cannot be sure
|
|
|
|
# it won't cause random crashes in other parts of the application. (PR-612)
|
|
|
|
|
|
|
|
if re.match("^5\\.1[12](\\.?|$)", qVersion()):
|
|
|
|
warning1 = "The version of PyQt you are using ({}) is known to have a bug that can cause Manuskript to crash."
|
|
|
|
warning2 = "It is recommended that you upgrade to the latest version of PyQt."
|
|
|
|
|
|
|
|
# Don't translate for debug log.
|
|
|
|
print("WARNING:", warning1.format(qVersion()), warning2)
|
|
|
|
|
|
|
|
msg = QMessageBox(QMessageBox.Warning,
|
|
|
|
app.tr("You may experience crashes."),
|
|
|
|
"<p><b>" +
|
|
|
|
app.tr(warning1).format(qVersion()) +
|
|
|
|
"</b></p>" +
|
|
|
|
"<p>" +
|
|
|
|
app.tr(warning2) +
|
|
|
|
"</p>",
|
|
|
|
QMessageBox.Ignore | QMessageBox.Abort)
|
|
|
|
|
|
|
|
# Dialogs without a choice on them are just asking to be ignored...
|
|
|
|
# But with the option to 'Abort'...? Maybe someone will actually read it.
|
|
|
|
if msg.exec() == QMessageBox.Abort:
|
|
|
|
sys.exit(1)
|
|
|
|
|
2017-11-21 03:42:30 +13:00
|
|
|
def prepare(tests=False):
|
2015-05-28 13:32:09 +12:00
|
|
|
app = QApplication(sys.argv)
|
2017-11-22 11:11:08 +13:00
|
|
|
app.setOrganizationName("manuskript"+("_tests" if tests else ""))
|
2015-05-28 13:32:09 +12:00
|
|
|
app.setOrganizationDomain("www.theologeek.ch")
|
2017-11-22 11:11:08 +13:00
|
|
|
app.setApplicationName("manuskript"+("_tests" if tests else ""))
|
2017-09-29 08:26:04 +13:00
|
|
|
app.setApplicationVersion(getVersion())
|
2017-11-19 12:20:49 +13:00
|
|
|
|
2017-09-29 08:26:04 +13:00
|
|
|
print("Running manuskript version {}.".format(getVersion()))
|
2016-02-07 04:42:22 +13:00
|
|
|
icon = QIcon()
|
2017-05-10 08:54:35 +12:00
|
|
|
for i in [16, 32, 64, 128, 256, 512]:
|
2016-02-07 04:42:22 +13:00
|
|
|
icon.addFile(appPath("icons/Manuskript/icon-{}px.png".format(i)))
|
|
|
|
qApp.setWindowIcon(icon)
|
|
|
|
|
2015-06-07 00:21:48 +12:00
|
|
|
app.setStyle("Fusion")
|
2015-06-23 07:34:11 +12:00
|
|
|
|
2016-03-30 05:39:33 +13:00
|
|
|
# Load style from QSettings
|
|
|
|
settings = QSettings(app.organizationName(), app.applicationName())
|
|
|
|
if settings.contains("applicationStyle"):
|
|
|
|
style = settings.value("applicationStyle")
|
|
|
|
app.setStyle(style)
|
|
|
|
|
2016-02-06 00:25:25 +13:00
|
|
|
# Translation process
|
2018-12-08 05:18:35 +13:00
|
|
|
appTranslator = QTranslator(app)
|
2016-03-30 05:39:33 +13:00
|
|
|
# By default: locale
|
2018-12-07 20:32:22 +13:00
|
|
|
|
|
|
|
def tryLoadTranslation(translation, source):
|
2019-09-05 10:50:46 +12:00
|
|
|
"""Tries to load and activate a given translation for use."""
|
2019-09-05 08:56:01 +12:00
|
|
|
if appTranslator.load(translation, appPath("i18n")):
|
2018-12-07 20:32:22 +13:00
|
|
|
app.installTranslator(appTranslator)
|
2019-09-05 08:56:01 +12:00
|
|
|
print("Loaded translation: {}".format(translation))
|
2019-09-05 10:50:46 +12:00
|
|
|
# Note: QTranslator.load() does some fancy heuristics where it simplifies
|
|
|
|
# the given locale until it is 'close enough' if the given filename does
|
|
|
|
# not work out. For example, if given 'i18n/manuskript_en_US.qm', it tries:
|
|
|
|
# * i18n/manuskript_en_US.qm.qm
|
|
|
|
# * i18n/manuskript_en_US.qm
|
|
|
|
# * i18n/manuskript_en_US
|
|
|
|
# * i18n/manuskript_en.qm
|
|
|
|
# * i18n/manuskript_en
|
|
|
|
# * i18n/manuskript.qm
|
|
|
|
# * i18n/manuskript
|
|
|
|
# We have no way to determining what it eventually went with, so mind your
|
|
|
|
# filenames when you observe strange behaviour with the loaded translations.
|
2018-12-07 20:32:22 +13:00
|
|
|
return True
|
|
|
|
else:
|
2019-09-05 08:56:01 +12:00
|
|
|
print("No translation found or loaded. ({})".format(translation))
|
2018-12-07 20:32:22 +13:00
|
|
|
return False
|
2016-03-30 05:39:33 +13:00
|
|
|
|
2019-09-05 10:50:46 +12:00
|
|
|
def activateTranslation(translation, source):
|
|
|
|
"""Loads the most suitable translation based on the available information."""
|
|
|
|
using_builtin_translation = True
|
|
|
|
|
|
|
|
if (translation != ""): # empty string == 'no translation, use builtin'
|
|
|
|
if isinstance(translation, str):
|
|
|
|
if tryLoadTranslation(translation, source):
|
|
|
|
using_builtin_translation = False
|
|
|
|
else: # A list of language codes to try. Once something works, we're done.
|
|
|
|
# This logic is loosely based on the working of QTranslator.load(QLocale, ...);
|
|
|
|
# it allows us to more accurately detect the language used for the user interface.
|
|
|
|
for language_code in translation:
|
|
|
|
lc = language_code.replace('-', '_')
|
|
|
|
if lc.lower() == 'en_US'.lower():
|
|
|
|
break
|
|
|
|
if tryLoadTranslation("manuskript_{}.qm".format(lc), source):
|
|
|
|
using_builtin_translation = False
|
|
|
|
break
|
|
|
|
|
|
|
|
if using_builtin_translation:
|
|
|
|
print("Using the builtin translation.")
|
|
|
|
|
2019-09-05 08:56:01 +12:00
|
|
|
# Load application translation
|
2018-12-07 20:32:22 +13:00
|
|
|
translation = ""
|
2019-09-05 08:56:01 +12:00
|
|
|
source = "default"
|
2016-03-30 05:39:33 +13:00
|
|
|
if settings.contains("applicationTranslation"):
|
2019-09-05 08:56:01 +12:00
|
|
|
# Use the language configured by the user.
|
2018-12-07 20:32:22 +13:00
|
|
|
translation = settings.value("applicationTranslation")
|
2019-09-05 08:56:01 +12:00
|
|
|
source = "user setting"
|
|
|
|
else:
|
|
|
|
# Auto-detect based on system locale.
|
2019-09-05 10:50:46 +12:00
|
|
|
translation = QLocale().uiLanguages()
|
|
|
|
source = "available ui languages"
|
2019-09-05 08:56:01 +12:00
|
|
|
|
|
|
|
print("Preferred translation: {} (based on {})".format(("builtin" if translation == "" else translation), source))
|
2019-09-05 10:50:46 +12:00
|
|
|
activateTranslation(translation, source)
|
2016-02-06 00:25:25 +13:00
|
|
|
|
2019-09-06 10:29:18 +12:00
|
|
|
def respectSystemDarkThemeSetting():
|
|
|
|
"""Adjusts the Qt theme to match the OS 'dark theme' setting configured by the user."""
|
|
|
|
if platform.system() is not 'Windows':
|
|
|
|
return
|
|
|
|
|
|
|
|
# Basic Windows 10 Dark Theme support.
|
|
|
|
# Source: https://forum.qt.io/topic/101391/windows-10-dark-theme/4
|
|
|
|
themeSettings = QSettings("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings.NativeFormat)
|
|
|
|
if themeSettings.value("AppsUseLightTheme") == 0:
|
|
|
|
darkPalette = QPalette()
|
|
|
|
darkColor = QColor(45,45,45)
|
|
|
|
disabledColor = QColor(127,127,127)
|
|
|
|
darkPalette.setColor(QPalette.Window, darkColor)
|
|
|
|
darkPalette.setColor(QPalette.WindowText, Qt.GlobalColor.white)
|
|
|
|
darkPalette.setColor(QPalette.Base, QColor(18,18,18))
|
|
|
|
darkPalette.setColor(QPalette.AlternateBase, darkColor)
|
|
|
|
darkPalette.setColor(QPalette.ToolTipBase, Qt.GlobalColor.white)
|
|
|
|
darkPalette.setColor(QPalette.ToolTipText, Qt.GlobalColor.white)
|
|
|
|
darkPalette.setColor(QPalette.Text, Qt.GlobalColor.white)
|
|
|
|
darkPalette.setColor(QPalette.Disabled, QPalette.Text, disabledColor)
|
|
|
|
darkPalette.setColor(QPalette.Button, darkColor)
|
|
|
|
darkPalette.setColor(QPalette.ButtonText, Qt.GlobalColor.white)
|
|
|
|
darkPalette.setColor(QPalette.Disabled, QPalette.ButtonText, disabledColor)
|
|
|
|
darkPalette.setColor(QPalette.BrightText, Qt.GlobalColor.red)
|
|
|
|
darkPalette.setColor(QPalette.Link, QColor(42, 130, 218))
|
|
|
|
|
|
|
|
darkPalette.setColor(QPalette.Highlight, QColor(42, 130, 218))
|
|
|
|
darkPalette.setColor(QPalette.HighlightedText, Qt.GlobalColor.black)
|
|
|
|
darkPalette.setColor(QPalette.Disabled, QPalette.HighlightedText, disabledColor)
|
|
|
|
|
|
|
|
# Fixes ugly (not to mention hard to read) disabled menu items.
|
|
|
|
# Source: https://bugreports.qt.io/browse/QTBUG-10322?focusedCommentId=371060#comment-371060
|
|
|
|
darkPalette.setColor(QPalette.Disabled, QPalette.Light, Qt.GlobalColor.transparent)
|
|
|
|
|
|
|
|
app.setPalette(darkPalette)
|
|
|
|
|
|
|
|
# This broke the Settings Dialog at one point... and then it stopped breaking it.
|
|
|
|
# TODO: Why'd it break? Check if tooltips look OK... and if not, make them look OK.
|
|
|
|
#app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }")
|
|
|
|
|
|
|
|
respectSystemDarkThemeSetting()
|
|
|
|
|
2015-09-29 22:17:03 +13:00
|
|
|
QIcon.setThemeSearchPaths(QIcon.themeSearchPaths() + [appPath("icons")])
|
|
|
|
QIcon.setThemeName("NumixMsk")
|
2015-06-23 07:34:11 +12:00
|
|
|
|
2017-11-30 02:34:25 +13:00
|
|
|
# Font siue
|
|
|
|
if settings.contains("appFontSize"):
|
|
|
|
f = qApp.font()
|
|
|
|
f.setPointSize(settings.value("appFontSize", type=int))
|
|
|
|
app.setFont(f)
|
|
|
|
|
2017-11-20 03:29:38 +13:00
|
|
|
# Main window
|
2017-11-19 12:20:49 +13:00
|
|
|
from manuskript.mainWindow import MainWindow
|
2015-06-23 07:34:11 +12:00
|
|
|
|
2017-11-20 03:29:38 +13:00
|
|
|
MW = MainWindow()
|
2017-10-19 23:13:20 +13:00
|
|
|
# We store the system default cursor flash time to be able to restore it
|
|
|
|
# later if necessary
|
2017-11-20 03:29:38 +13:00
|
|
|
MW._defaultCursorFlashTime = qApp.cursorFlashTime()
|
|
|
|
|
2017-12-01 05:47:23 +13:00
|
|
|
# Command line project
|
|
|
|
if len(sys.argv) > 1 and sys.argv[1][-4:] == ".msk":
|
|
|
|
if os.path.exists(sys.argv[1]):
|
|
|
|
path = os.path.abspath(sys.argv[1])
|
|
|
|
MW._autoLoadProject = path
|
|
|
|
|
2017-11-20 03:29:38 +13:00
|
|
|
return app, MW
|
|
|
|
|
2019-02-24 13:51:18 +13:00
|
|
|
def launch(app, MW = None):
|
2019-09-03 17:43:55 +12:00
|
|
|
warnAboutBuggyLibraries(app)
|
|
|
|
|
2017-11-20 03:29:38 +13:00
|
|
|
if MW is None:
|
|
|
|
from manuskript.functions import mainWindow
|
|
|
|
MW = mainWindow()
|
|
|
|
|
|
|
|
MW.show()
|
2015-06-23 07:34:11 +12:00
|
|
|
|
2019-02-24 13:51:18 +13:00
|
|
|
# Support for IPython Jupyter QT Console as a debugging aid.
|
|
|
|
# Last argument must be --console to enable it
|
|
|
|
# Code reference :
|
|
|
|
# https://github.com/ipython/ipykernel/blob/master/examples/embedding/ipkernel_qtapp.py
|
|
|
|
# https://github.com/ipython/ipykernel/blob/master/examples/embedding/internal_ipkernel.py
|
|
|
|
if len(sys.argv) > 1 and sys.argv[-1] == "--console":
|
|
|
|
try:
|
|
|
|
from IPython.lib.kernel import connect_qtconsole
|
|
|
|
from ipykernel.kernelapp import IPKernelApp
|
|
|
|
# Only to ensure matplotlib QT mainloop integration is available
|
|
|
|
import matplotlib
|
|
|
|
|
|
|
|
# Create IPython kernel within our application
|
|
|
|
kernel = IPKernelApp.instance()
|
|
|
|
|
|
|
|
# Initialize it and use matplotlib for main event loop integration with QT
|
|
|
|
kernel.initialize(['python', '--matplotlib=qt'])
|
|
|
|
|
|
|
|
# Create the console in a new process and connect
|
|
|
|
console = connect_qtconsole(kernel.abs_connection_file, profile=kernel.profile)
|
|
|
|
|
|
|
|
# Export MW and app variable to the console's namespace
|
|
|
|
kernel.shell.user_ns['MW'] = MW
|
|
|
|
kernel.shell.user_ns['app'] = app
|
|
|
|
kernel.shell.user_ns['kernel'] = kernel
|
|
|
|
kernel.shell.user_ns['console'] = console
|
|
|
|
|
|
|
|
# When we close manuskript, make sure we close the console process and stop the
|
|
|
|
# IPython kernel's mainloop, otherwise the app will never finish.
|
|
|
|
def console_cleanup():
|
|
|
|
app.quit()
|
|
|
|
console.kill()
|
|
|
|
kernel.io_loop.stop()
|
|
|
|
app.lastWindowClosed.connect(console_cleanup)
|
|
|
|
|
|
|
|
# Very important, IPython-specific step: this gets GUI event loop
|
|
|
|
# integration going, and it replaces calling app.exec_()
|
|
|
|
kernel.start()
|
|
|
|
except Exception as e:
|
|
|
|
print("Console mode requested but error initializing IPython : %s" % str(e))
|
|
|
|
print("To make use of the Interactive IPython QT Console, make sure you install : ")
|
|
|
|
print("$ pip3 install ipython qtconsole matplotlib")
|
|
|
|
qApp.exec_()
|
|
|
|
else:
|
|
|
|
qApp.exec_()
|
2015-06-22 04:35:42 +12:00
|
|
|
qApp.deleteLater()
|
2015-06-23 07:34:11 +12:00
|
|
|
|
2017-11-20 03:29:38 +13:00
|
|
|
def run():
|
|
|
|
"""
|
|
|
|
Run separates prepare and launch for two reasons:
|
|
|
|
1. I've read somewhere it helps with potential segfault (see comment below)
|
|
|
|
2. So that prepare can be used in tests, without running the whole thing
|
|
|
|
"""
|
|
|
|
# Need to return and keep `app` otherwise it gets deleted.
|
|
|
|
app, MW = prepare()
|
2018-01-07 06:48:40 +13:00
|
|
|
# Separating launch to avoid segfault, so it seem.
|
2017-11-20 03:29:38 +13:00
|
|
|
# Cf. http://stackoverflow.com/questions/12433491/is-this-pyqt-4-python-bug-or-wrongly-behaving-code
|
2019-02-24 13:51:18 +13:00
|
|
|
launch(app, MW)
|
2015-06-08 08:06:57 +12:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2015-06-23 07:07:38 +12:00
|
|
|
run()
|