Connect the splash screen and add messages to track progress
This commit is contained in:
+11
-1
@@ -35,6 +35,7 @@ from PyQt6.QtWidgets import QApplication, QErrorMessage
|
|||||||
from novelwriter.config import Config
|
from novelwriter.config import Config
|
||||||
from novelwriter.error import exceptionHandler
|
from novelwriter.error import exceptionHandler
|
||||||
from novelwriter.shared import SharedData
|
from novelwriter.shared import SharedData
|
||||||
|
from novelwriter.splash import NSplashScreen
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from novelwriter.guimain import GuiMain
|
from novelwriter.guimain import GuiMain
|
||||||
@@ -267,14 +268,23 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
|
|||||||
# Connect the exception handler before making the main GUI
|
# Connect the exception handler before making the main GUI
|
||||||
sys.excepthook = exceptionHandler
|
sys.excepthook = exceptionHandler
|
||||||
|
|
||||||
|
splash = NSplashScreen()
|
||||||
|
splash.show()
|
||||||
|
|
||||||
|
splash.showStatus("")
|
||||||
|
splash.showStatus("Starting novelWriter ...")
|
||||||
|
|
||||||
# Run Config steps that require the QApplication
|
# Run Config steps that require the QApplication
|
||||||
CONFIG.loadConfig()
|
CONFIG.loadConfig(splash)
|
||||||
CONFIG.initLocalisation(app)
|
CONFIG.initLocalisation(app)
|
||||||
SHARED.initTheme(GuiTheme())
|
SHARED.initTheme(GuiTheme())
|
||||||
|
|
||||||
# Launch main GUI
|
# Launch main GUI
|
||||||
nwGUI = GuiMain()
|
nwGUI = GuiMain()
|
||||||
nwGUI.showNormal()
|
nwGUI.showNormal()
|
||||||
|
splash.finish(nwGUI)
|
||||||
|
|
||||||
|
CONFIG.finishStartup()
|
||||||
nwGUI.postLaunchTasks(cmdOpen)
|
nwGUI.postLaunchTasks(cmdOpen)
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
+37
-4
@@ -37,20 +37,21 @@ from PyQt6.QtCore import (
|
|||||||
PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo,
|
PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo,
|
||||||
QLocale, QStandardPaths, QSysInfo, QTranslator
|
QLocale, QStandardPaths, QSysInfo, QTranslator
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QFont, QFontDatabase
|
from PyQt6.QtGui import QFont, QFontDatabase, QFontMetrics
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
|
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
|
||||||
formatTimeStamp
|
formatTimeStamp
|
||||||
)
|
)
|
||||||
from novelwriter.constants import nwFiles, nwUnicode
|
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwUnicode
|
||||||
from novelwriter.error import formatException, logException
|
from novelwriter.error import formatException, logException
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from novelwriter.core.projectdata import NWProjectData
|
from novelwriter.core.projectdata import NWProjectData
|
||||||
|
from novelwriter.splash import NSplashScreen
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ class Config:
|
|||||||
"_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate",
|
"_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate",
|
||||||
"_dShortDateTime", "_dataPath", "_errData", "_hasError", "_homePath", "_manuals",
|
"_dShortDateTime", "_dataPath", "_errData", "_hasError", "_homePath", "_manuals",
|
||||||
"_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_recentProjects",
|
"_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_recentProjects",
|
||||||
"allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName",
|
"_splash", "allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName",
|
||||||
"askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", "autoScroll",
|
"askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", "autoScroll",
|
||||||
"autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", "dialogStyle",
|
"autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", "dialogStyle",
|
||||||
"doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "doReplaceDots",
|
"doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "doReplaceDots",
|
||||||
@@ -94,6 +95,8 @@ class Config:
|
|||||||
# Initialisation
|
# Initialisation
|
||||||
# ==============
|
# ==============
|
||||||
|
|
||||||
|
self._splash = None
|
||||||
|
|
||||||
# Set Application Variables
|
# Set Application Variables
|
||||||
self.appName = "novelWriter"
|
self.appName = "novelWriter"
|
||||||
self.appHandle = "novelwriter"
|
self.appHandle = "novelwriter"
|
||||||
@@ -497,6 +500,12 @@ class Config:
|
|||||||
|
|
||||||
return sorted(langList.items(), key=lambda x: x[0])
|
return sorted(langList.items(), key=lambda x: x[0])
|
||||||
|
|
||||||
|
def splashMessage(self, message: str) -> None:
|
||||||
|
"""Send a message to the splash screen."""
|
||||||
|
if self._splash:
|
||||||
|
self._splash.showStatus(message)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Config Actions
|
# Config Actions
|
||||||
##
|
##
|
||||||
@@ -543,6 +552,8 @@ class Config:
|
|||||||
|
|
||||||
def initLocalisation(self, nwApp: QApplication) -> None:
|
def initLocalisation(self, nwApp: QApplication) -> None:
|
||||||
"""Initialise the localisation of the GUI."""
|
"""Initialise the localisation of the GUI."""
|
||||||
|
self.splashMessage("Loading localisation ...")
|
||||||
|
|
||||||
self._qLocale = QLocale(self.guiLocale)
|
self._qLocale = QLocale(self.guiLocale)
|
||||||
QLocale.setDefault(self._qLocale)
|
QLocale.setDefault(self._qLocale)
|
||||||
self._qtTrans = {}
|
self._qtTrans = {}
|
||||||
@@ -568,8 +579,11 @@ class Config:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def loadConfig(self) -> bool:
|
def loadConfig(self, splash: NSplashScreen | None = None) -> bool:
|
||||||
"""Load preferences from file and replace default settings."""
|
"""Load preferences from file and replace default settings."""
|
||||||
|
self._splash = splash
|
||||||
|
self.splashMessage("Loading user configuration ...")
|
||||||
|
|
||||||
logger.debug("Loading config file")
|
logger.debug("Loading config file")
|
||||||
|
|
||||||
conf = NWConfigParser()
|
conf = NWConfigParser()
|
||||||
@@ -690,6 +704,9 @@ class Config:
|
|||||||
# Check Values
|
# Check Values
|
||||||
# ============
|
# ============
|
||||||
|
|
||||||
|
self._prepareFont(self.guiFont, "GUI")
|
||||||
|
self._prepareFont(self.textFont, "document")
|
||||||
|
|
||||||
# If we're using straight quotes, disable auto-replace
|
# If we're using straight quotes, disable auto-replace
|
||||||
if self.fmtSQuoteOpen == self.fmtSQuoteClose == "'" and self.doReplaceSQuote:
|
if self.fmtSQuoteOpen == self.fmtSQuoteClose == "'" and self.doReplaceSQuote:
|
||||||
logger.info("Using straight single quotes, so disabling auto-replace")
|
logger.info("Using straight single quotes, so disabling auto-replace")
|
||||||
@@ -820,6 +837,11 @@ class Config:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def finishStartup(self) -> None:
|
||||||
|
"""Call after startup is complete."""
|
||||||
|
self._splash = None
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
@@ -842,6 +864,17 @@ class Config:
|
|||||||
logger.debug("Checking package 'pyenchant': OK")
|
logger.debug("Checking package 'pyenchant': OK")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _prepareFont(self, font: QFont, kind: str) -> None:
|
||||||
|
"""Check Unicode availability in font. This also initialises any
|
||||||
|
alternative character used for missing glyphs. See #2315.
|
||||||
|
"""
|
||||||
|
self.splashMessage(f"Initialising {kind} font: {font.family()}")
|
||||||
|
metrics = QFontMetrics(font)
|
||||||
|
for char in nwHtmlUnicode.U_TO_H.keys():
|
||||||
|
if not metrics.inFont(char): # type: ignore
|
||||||
|
logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class RecentProjects:
|
class RecentProjects:
|
||||||
|
|
||||||
|
|||||||
+25
-10
@@ -130,14 +130,6 @@ class GuiTheme:
|
|||||||
self._availSyntax: dict[str, Path] = {}
|
self._availSyntax: dict[str, Path] = {}
|
||||||
self._styleSheets: dict[str, str] = {}
|
self._styleSheets: dict[str, str] = {}
|
||||||
|
|
||||||
_listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
|
|
||||||
_listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
|
|
||||||
_listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
|
|
||||||
_listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
|
|
||||||
|
|
||||||
self.loadTheme()
|
|
||||||
self.loadSyntax()
|
|
||||||
|
|
||||||
# Icon Functions
|
# Icon Functions
|
||||||
self.getIcon = self.iconCache.getIcon
|
self.getIcon = self.iconCache.getIcon
|
||||||
self.getPixmap = self.iconCache.getPixmap
|
self.getPixmap = self.iconCache.getPixmap
|
||||||
@@ -186,6 +178,15 @@ class GuiTheme:
|
|||||||
logger.debug("Text 'N' Height: %d", self.textNHeight)
|
logger.debug("Text 'N' Height: %d", self.textNHeight)
|
||||||
logger.debug("Text 'N' Width: %d", self.textNWidth)
|
logger.debug("Text 'N' Width: %d", self.textNWidth)
|
||||||
|
|
||||||
|
# Process Themes
|
||||||
|
_listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
|
||||||
|
_listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
|
||||||
|
_listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
|
||||||
|
_listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
|
||||||
|
|
||||||
|
self.loadTheme()
|
||||||
|
self.loadSyntax()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -218,6 +219,7 @@ class GuiTheme:
|
|||||||
logger.error("Could not load GUI theme")
|
logger.error("Could not load GUI theme")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
CONFIG.splashMessage("Loading GUI theme ...")
|
||||||
logger.info("Loading GUI theme '%s'", theme)
|
logger.info("Loading GUI theme '%s'", theme)
|
||||||
parser = NWConfigParser()
|
parser = NWConfigParser()
|
||||||
try:
|
try:
|
||||||
@@ -357,6 +359,8 @@ class GuiTheme:
|
|||||||
QApplication.setPalette(self._guiPalette)
|
QApplication.setPalette(self._guiPalette)
|
||||||
self._buildStyleSheets(self._guiPalette)
|
self._buildStyleSheets(self._guiPalette)
|
||||||
|
|
||||||
|
CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def loadSyntax(self) -> bool:
|
def loadSyntax(self) -> bool:
|
||||||
@@ -371,6 +375,7 @@ class GuiTheme:
|
|||||||
logger.error("Could not load syntax theme")
|
logger.error("Could not load syntax theme")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
CONFIG.splashMessage("Loading syntax theme ...")
|
||||||
logger.info("Loading syntax theme '%s'", theme)
|
logger.info("Loading syntax theme '%s'", theme)
|
||||||
parser = NWConfigParser()
|
parser = NWConfigParser()
|
||||||
try:
|
try:
|
||||||
@@ -418,6 +423,8 @@ class GuiTheme:
|
|||||||
syntax.mod = self._parseColor(parser, sec, "modifier")
|
syntax.mod = self._parseColor(parser, sec, "modifier")
|
||||||
syntax.mark = self._parseColor(parser, sec, "texthighlight")
|
syntax.mark = self._parseColor(parser, sec, "texthighlight")
|
||||||
|
|
||||||
|
CONFIG.splashMessage(f"Loaded syntax theme: {meta.name}")
|
||||||
|
|
||||||
self.syntaxMeta = meta
|
self.syntaxMeta = meta
|
||||||
self.syntaxTheme = syntax
|
self.syntaxTheme = syntax
|
||||||
|
|
||||||
@@ -633,6 +640,7 @@ class GuiIcons:
|
|||||||
logger.error("Could not load icon theme")
|
logger.error("Could not load icon theme")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
CONFIG.splashMessage("Loading icon theme ...")
|
||||||
logger.info("Loading icon theme '%s'", theme)
|
logger.info("Loading icon theme '%s'", theme)
|
||||||
try:
|
try:
|
||||||
meta = ThemeMeta()
|
meta = ThemeMeta()
|
||||||
@@ -656,6 +664,9 @@ class GuiIcons:
|
|||||||
logException()
|
logException()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
CONFIG.splashMessage(f"Loaded icon theme: {meta.name}")
|
||||||
|
CONFIG.splashMessage("Generating additional icons ...")
|
||||||
|
|
||||||
# Set colour overrides for project item icons
|
# Set colour overrides for project item icons
|
||||||
if (override := CONFIG.iconColTree) != "theme":
|
if (override := CONFIG.iconColTree) != "theme":
|
||||||
color = self._svgColors.get(override, b"#000000")
|
color = self._svgColors.get(override, b"#000000")
|
||||||
@@ -668,6 +679,10 @@ class GuiIcons:
|
|||||||
self._svgColors["scene"] = color
|
self._svgColors["scene"] = color
|
||||||
self._svgColors["note"] = color
|
self._svgColors["note"] = color
|
||||||
|
|
||||||
|
# Populate generated icons cache
|
||||||
|
self.getHeaderDecoration(0)
|
||||||
|
self.getHeaderDecorationNarrow(0)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setIconColor(self, key: str, color: QColor) -> None:
|
def setIconColor(self, key: str, color: QColor) -> None:
|
||||||
@@ -822,8 +837,8 @@ class GuiIcons:
|
|||||||
##
|
##
|
||||||
|
|
||||||
def _loadIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
|
def _loadIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
|
||||||
"""Load an icon from the assets themes folder. Is guaranteed to
|
"""Load an icon from the assets themes folder. This function is
|
||||||
return a QIcon.
|
guaranteed to return a QIcon.
|
||||||
"""
|
"""
|
||||||
# If we just want the app icons, return right away
|
# If we just want the app icons, return right away
|
||||||
if name == "novelwriter":
|
if name == "novelwriter":
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ class GuiMain(QMainWindow):
|
|||||||
logger.debug("Ready: GUI")
|
logger.debug("Ready: GUI")
|
||||||
logger.info("novelWriter is ready ...")
|
logger.info("novelWriter is ready ...")
|
||||||
self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ..."))
|
self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ..."))
|
||||||
|
CONFIG.splashMessage("novelWriter is ready ...")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -70,5 +70,5 @@ class NSplashScreen(QSplashScreen):
|
|||||||
self.showMessage(message)
|
self.showMessage(message)
|
||||||
if message:
|
if message:
|
||||||
logger.info("[Splash] %s", message)
|
logger.info("[Splash] %s", message)
|
||||||
sleep(0.05)
|
sleep(0.025)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from novelwriter import (
|
|||||||
BLUE, CONFIG, END, FILE, LINE, LVLC, LVLP, TEXT, TIME, WHITE, _createApp,
|
BLUE, CONFIG, END, FILE, LINE, LVLC, LVLP, TEXT, TIME, WHITE, _createApp,
|
||||||
logger, main
|
logger, main
|
||||||
)
|
)
|
||||||
|
from novelwriter.splash import NSplashScreen
|
||||||
|
|
||||||
from tests.tools import clearLogHandlers
|
from tests.tools import clearLogHandlers
|
||||||
|
|
||||||
@@ -40,6 +41,8 @@ from tests.tools import clearLogHandlers
|
|||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseInit_Launch(caplog, monkeypatch, fncPath):
|
def testBaseInit_Launch(caplog, monkeypatch, fncPath):
|
||||||
"""Check launching the main GUI. This test """
|
"""Check launching the main GUI. This test """
|
||||||
|
monkeypatch.setattr(NSplashScreen, "finish", lambda *a: None)
|
||||||
|
monkeypatch.setattr("novelwriter.splash.sleep", lambda *a: None)
|
||||||
monkeypatch.setattr("novelwriter._createApp", lambda *a: Mock())
|
monkeypatch.setattr("novelwriter._createApp", lambda *a: Mock())
|
||||||
monkeypatch.setattr("novelwriter.guimain.GuiMain", Mock())
|
monkeypatch.setattr("novelwriter.guimain.GuiMain", Mock())
|
||||||
monkeypatch.setattr(sys, "exit", Mock())
|
monkeypatch.setattr(sys, "exit", Mock())
|
||||||
@@ -77,6 +80,9 @@ def testBaseInit_CreateApp(caplog, monkeypatch, fncPath):
|
|||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseInit_Options(monkeypatch, fncPath):
|
def testBaseInit_Options(monkeypatch, fncPath):
|
||||||
"""Test command line options for logging level."""
|
"""Test command line options for logging level."""
|
||||||
|
monkeypatch.setattr(NSplashScreen, "finish", lambda *a: None)
|
||||||
|
monkeypatch.setattr("novelwriter.splash.sleep", lambda *a: None)
|
||||||
|
|
||||||
gui = Mock()
|
gui = Mock()
|
||||||
app = Mock()
|
app = Mock()
|
||||||
app.exec = Mock(return_value=0)
|
app.exec = Mock(return_value=0)
|
||||||
|
|||||||
Reference in New Issue
Block a user