Connect the splash screen and add messages to track progress

This commit is contained in:
Veronica Berglyd Olsen
2025-04-25 18:41:28 +02:00
parent dc6259133f
commit 9c8030e588
6 changed files with 81 additions and 16 deletions
+11 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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":
+1
View File
@@ -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
+1 -1
View File
@@ -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
+6
View File
@@ -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)