Updates to app launch (#2316)

This commit is contained in:
Veronica Berglyd Olsen
2025-04-25 19:00:39 +02:00
committed by GitHub
8 changed files with 171 additions and 19 deletions
+14 -1
View File
@@ -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,13 +268,25 @@ 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()
del splash
nwGUI.postLaunchTasks(cmdOpen)
sys.exit(app.exec())
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+37 -4
View File
@@ -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:
+25 -10
View File
@@ -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":
+10 -1
View File
@@ -310,11 +310,11 @@ class GuiMain(QMainWindow):
self.asProjTimer.start()
self.asDocTimer.start()
self.mainStatus.clearStatus()
self.showNormal()
logger.debug("Ready: GUI")
logger.info("novelWriter is ready ...")
self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ..."))
CONFIG.splashMessage("novelWriter is ready ...")
return
@@ -340,6 +340,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()
+74
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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.025)
return
+6
View File
@@ -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)
+5 -3
View File
@@ -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
# ========================