From d319051942fe930279da3fd2dca4893658528871 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:27:43 +0200 Subject: [PATCH 1/5] Add a small delay before opening welcome dialog --- novelwriter/__init__.py | 1 + novelwriter/guimain.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index b8963e8f..45c2580d 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -274,6 +274,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Launch main GUI nwGUI = GuiMain() + nwGUI.showNormal() nwGUI.postLaunchTasks(cmdOpen) sys.exit(app.exec()) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index fac283f3..bf84e8b0 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -310,7 +310,6 @@ class GuiMain(QMainWindow): self.asProjTimer.start() self.asDocTimer.start() self.mainStatus.clearStatus() - self.showNormal() logger.debug("Ready: GUI") logger.info("novelWriter is ready ...") @@ -340,6 +339,15 @@ class GuiMain(QMainWindow): logger.info("Command line path: %s", cmdOpen) self.openProject(cmdOpen) + # Add a small delay for the window coordinates to be ready + # before showing any dialogs + QTimer.singleShot(50, self.showPostLaunchDialogs) + + return + + @pyqtSlot() + def showPostLaunchDialogs(self) -> None: + """Show post launch dialogs.""" if not SHARED.hasProject: self.showWelcomeDialog() From 40441c8451516226f1e6fe7ca929f2e1451948b6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:27:54 +0200 Subject: [PATCH 2/5] Fix bug in theme test --- tests/test_gui/test_gui_theme.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 77755fbe..c64f1172 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -170,14 +170,16 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): "\n" "[Palette]\n" "window = 0, 0, 0\n" - "windowtext = 255, 255, 255\n" + "text = 255, 255, 255\n" ) mainTheme._availThemes["test"] = mockTheme CONFIG.guiTheme = "test" assert mainTheme.loadTheme() is True - assert mainTheme.isDarkTheme is False - assert mainTheme.helpText.getRgb() == (190, 190, 190, 255) + assert mainTheme._guiPalette.window().color().getRgb() == (0, 0, 0, 255) + assert mainTheme._guiPalette.text().color().getRgb() == (255, 255, 255, 255) + assert mainTheme._guiPalette.light().color().getRgb() == (57, 57, 57, 255) + assert mainTheme.isDarkTheme is True # Load Default Light Theme # ======================== From dc6259133f70be2914a55c574206801f54fd3e80 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:40:04 +0200 Subject: [PATCH 3/5] Add a splash screen --- novelwriter/assets/images/splash.png | Bin 0 -> 28806 bytes novelwriter/splash.py | 74 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 novelwriter/assets/images/splash.png create mode 100644 novelwriter/splash.py diff --git a/novelwriter/assets/images/splash.png b/novelwriter/assets/images/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..c038953170319ad8af0b82f20f1b4899f6e69a2a GIT binary patch literal 28806 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 literal 0 HcmV?d00001 diff --git a/novelwriter/splash.py b/novelwriter/splash.py new file mode 100644 index 00000000..1a0c3b8c --- /dev/null +++ b/novelwriter/splash.py @@ -0,0 +1,74 @@ +""" +novelWriter – Splash Screen +=========================== + +File History: +Created: 2015-04-25 [2.7rc1] + +This file is a part of novelWriter +Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from __future__ import annotations + +import logging + +from pathlib import Path +from time import sleep + +from PyQt6.QtCore import QRect, Qt +from PyQt6.QtGui import QColor, QPainter, QPixmap +from PyQt6.QtWidgets import QSplashScreen + +logger = logging.getLogger(__name__) + +SPLASH_IMG = Path(__file__).parent / "assets" / "images" / "splash.png" + + +class NSplashScreen(QSplashScreen): + + __slots__ = ("_color", "_rect", "_text") + + def __init__(self) -> None: + super().__init__(pixmap=QPixmap(str(SPLASH_IMG)), flags=Qt.WindowType.WindowStaysOnTopHint) + + logger.debug("Create: NSplashScreen") + + font = self.font() + font.setPointSizeF(12.0) + self.setFont(font) + self._color = QColor(26, 52, 78) + self._rect = QRect(144, 110, 440, 30) + self._text = "" + return + + def __del__(self) -> None: # pragma: no cover + logger.debug("Delete: NSplashScreen") + return + + def drawContents(self, painter: QPainter) -> None: + """Draw the text message.""" + painter.setPen(self._color) + painter.drawText(self._rect, Qt.AlignmentFlag.AlignLeft, self._text) + return + + def showStatus(self, message: str) -> None: + """Update the status message.""" + self._text = message + self.showMessage(message) + if message: + logger.info("[Splash] %s", message) + sleep(0.05) + return 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 4/5] 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) From 1ec875445607306e0a8e344e98bd64e782182cfd Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:55:50 +0200 Subject: [PATCH 5/5] Delete the splash screen when done --- novelwriter/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index c118437b..0e0bb87b 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -285,6 +285,8 @@ def main(sysArgs: list | None = None) -> GuiMain | None: splash.finish(nwGUI) CONFIG.finishStartup() + del splash + nwGUI.postLaunchTasks(cmdOpen) sys.exit(app.exec())