From 9c8030e5886cd227c7aba6a4487c19c61785e3d3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:41:28 +0200 Subject: [PATCH] Connect the splash screen and add messages to track progress --- novelwriter/__init__.py | 12 ++++++++- novelwriter/config.py | 41 ++++++++++++++++++++++++++++--- novelwriter/gui/theme.py | 35 ++++++++++++++++++-------- novelwriter/guimain.py | 1 + novelwriter/splash.py | 2 +- tests/test_base/test_base_init.py | 6 +++++ 6 files changed, 81 insertions(+), 16 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 45c2580d..c118437b 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -35,6 +35,7 @@ from PyQt6.QtWidgets import QApplication, QErrorMessage from novelwriter.config import Config from novelwriter.error import exceptionHandler from novelwriter.shared import SharedData +from novelwriter.splash import NSplashScreen if TYPE_CHECKING: 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 sys.excepthook = exceptionHandler + splash = NSplashScreen() + splash.show() + + splash.showStatus("") + splash.showStatus("Starting novelWriter ...") + # Run Config steps that require the QApplication - CONFIG.loadConfig() + CONFIG.loadConfig(splash) CONFIG.initLocalisation(app) SHARED.initTheme(GuiTheme()) # Launch main GUI nwGUI = GuiMain() nwGUI.showNormal() + splash.finish(nwGUI) + + CONFIG.finishStartup() nwGUI.postLaunchTasks(cmdOpen) sys.exit(app.exec()) diff --git a/novelwriter/config.py b/novelwriter/config.py index c6dbb47b..78a4b843 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -37,20 +37,21 @@ from PyQt6.QtCore import ( PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo, QLocale, QStandardPaths, QSysInfo, QTranslator ) -from PyQt6.QtGui import QFont, QFontDatabase +from PyQt6.QtGui import QFont, QFontDatabase, QFontMetrics from PyQt6.QtWidgets import QApplication from novelwriter.common import ( NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, formatTimeStamp ) -from novelwriter.constants import nwFiles, nwUnicode +from novelwriter.constants import nwFiles, nwHtmlUnicode, nwUnicode from novelwriter.error import formatException, logException if TYPE_CHECKING: from datetime import datetime from novelwriter.core.projectdata import NWProjectData + from novelwriter.splash import NSplashScreen logger = logging.getLogger(__name__) @@ -66,7 +67,7 @@ class Config: "_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate", "_dShortDateTime", "_dataPath", "_errData", "_hasError", "_homePath", "_manuals", "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_recentProjects", - "allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName", + "_splash", "allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName", "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", "autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", "dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "doReplaceDots", @@ -94,6 +95,8 @@ class Config: # Initialisation # ============== + self._splash = None + # Set Application Variables self.appName = "novelWriter" self.appHandle = "novelwriter" @@ -497,6 +500,12 @@ class Config: 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 ## @@ -543,6 +552,8 @@ class Config: def initLocalisation(self, nwApp: QApplication) -> None: """Initialise the localisation of the GUI.""" + self.splashMessage("Loading localisation ...") + self._qLocale = QLocale(self.guiLocale) QLocale.setDefault(self._qLocale) self._qtTrans = {} @@ -568,8 +579,11 @@ class Config: return - def loadConfig(self) -> bool: + def loadConfig(self, splash: NSplashScreen | None = None) -> bool: """Load preferences from file and replace default settings.""" + self._splash = splash + self.splashMessage("Loading user configuration ...") + logger.debug("Loading config file") conf = NWConfigParser() @@ -690,6 +704,9 @@ class Config: # Check Values # ============ + self._prepareFont(self.guiFont, "GUI") + self._prepareFont(self.textFont, "document") + # If we're using straight quotes, disable auto-replace if self.fmtSQuoteOpen == self.fmtSQuoteClose == "'" and self.doReplaceSQuote: logger.info("Using straight single quotes, so disabling auto-replace") @@ -820,6 +837,11 @@ class Config: return True + def finishStartup(self) -> None: + """Call after startup is complete.""" + self._splash = None + return + ## # Internal Functions ## @@ -842,6 +864,17 @@ class Config: logger.debug("Checking package 'pyenchant': OK") 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: diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 207bc6fc..1eb2e487 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -130,14 +130,6 @@ class GuiTheme: self._availSyntax: dict[str, Path] = {} 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 self.getIcon = self.iconCache.getIcon self.getPixmap = self.iconCache.getPixmap @@ -186,6 +178,15 @@ class GuiTheme: logger.debug("Text 'N' Height: %d", self.textNHeight) 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 ## @@ -218,6 +219,7 @@ class GuiTheme: logger.error("Could not load GUI theme") return False + CONFIG.splashMessage("Loading GUI theme ...") logger.info("Loading GUI theme '%s'", theme) parser = NWConfigParser() try: @@ -357,6 +359,8 @@ class GuiTheme: QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) + CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}") + return True def loadSyntax(self) -> bool: @@ -371,6 +375,7 @@ class GuiTheme: logger.error("Could not load syntax theme") return False + CONFIG.splashMessage("Loading syntax theme ...") logger.info("Loading syntax theme '%s'", theme) parser = NWConfigParser() try: @@ -418,6 +423,8 @@ class GuiTheme: syntax.mod = self._parseColor(parser, sec, "modifier") syntax.mark = self._parseColor(parser, sec, "texthighlight") + CONFIG.splashMessage(f"Loaded syntax theme: {meta.name}") + self.syntaxMeta = meta self.syntaxTheme = syntax @@ -633,6 +640,7 @@ class GuiIcons: logger.error("Could not load icon theme") return False + CONFIG.splashMessage("Loading icon theme ...") logger.info("Loading icon theme '%s'", theme) try: meta = ThemeMeta() @@ -656,6 +664,9 @@ class GuiIcons: logException() return False + CONFIG.splashMessage(f"Loaded icon theme: {meta.name}") + CONFIG.splashMessage("Generating additional icons ...") + # Set colour overrides for project item icons if (override := CONFIG.iconColTree) != "theme": color = self._svgColors.get(override, b"#000000") @@ -668,6 +679,10 @@ class GuiIcons: self._svgColors["scene"] = color self._svgColors["note"] = color + # Populate generated icons cache + self.getHeaderDecoration(0) + self.getHeaderDecorationNarrow(0) + return True 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: - """Load an icon from the assets themes folder. Is guaranteed to - return a QIcon. + """Load an icon from the assets themes folder. This function is + guaranteed to return a QIcon. """ # If we just want the app icons, return right away if name == "novelwriter": diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index bf84e8b0..7a03076f 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -314,6 +314,7 @@ class GuiMain(QMainWindow): logger.debug("Ready: GUI") logger.info("novelWriter is ready ...") self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ...")) + CONFIG.splashMessage("novelWriter is ready ...") return diff --git a/novelwriter/splash.py b/novelwriter/splash.py index 1a0c3b8c..f1792a98 100644 --- a/novelwriter/splash.py +++ b/novelwriter/splash.py @@ -70,5 +70,5 @@ class NSplashScreen(QSplashScreen): self.showMessage(message) if message: logger.info("[Splash] %s", message) - sleep(0.05) + sleep(0.025) return diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index 39ba566f..cc1fbeb8 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -33,6 +33,7 @@ from novelwriter import ( BLUE, CONFIG, END, FILE, LINE, LVLC, LVLP, TEXT, TIME, WHITE, _createApp, logger, main ) +from novelwriter.splash import NSplashScreen from tests.tools import clearLogHandlers @@ -40,6 +41,8 @@ from tests.tools import clearLogHandlers @pytest.mark.base def testBaseInit_Launch(caplog, monkeypatch, fncPath): """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.guimain.GuiMain", Mock()) monkeypatch.setattr(sys, "exit", Mock()) @@ -77,6 +80,9 @@ def testBaseInit_CreateApp(caplog, monkeypatch, fncPath): @pytest.mark.base def testBaseInit_Options(monkeypatch, fncPath): """Test command line options for logging level.""" + monkeypatch.setattr(NSplashScreen, "finish", lambda *a: None) + monkeypatch.setattr("novelwriter.splash.sleep", lambda *a: None) + gui = Mock() app = Mock() app.exec = Mock(return_value=0)