From 1fafa0dae7c8154efcb41154bfe0772f81913799 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:28:06 +0200 Subject: [PATCH 01/20] Remove syntax theme parsing and add light and dark theme user settings --- novelwriter/common.py | 11 ++ novelwriter/config.py | 52 ++++---- novelwriter/dialogs/preferences.py | 52 ++++---- novelwriter/enum.py | 7 ++ novelwriter/gui/theme.py | 188 ++++++++++++----------------- novelwriter/guimain.py | 1 - 6 files changed, 148 insertions(+), 163 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index a4e37f62..6d1e8519 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -31,6 +31,7 @@ import xml.etree.ElementTree as ET from configparser import ConfigParser from datetime import datetime +from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, TypeGuard, TypeVar from urllib.parse import urljoin @@ -676,6 +677,9 @@ def openExternalPath(path: Path) -> bool: # Classes ## +_T_Enum = TypeVar("_T_Enum", bound=Enum) + + class NWConfigParser(ConfigParser): """Common: Adapted Config Parser @@ -736,3 +740,10 @@ class NWConfigParser(ConfigParser): for i in range(min(len(data), len(result))): result[i] = checkInt(data[i].strip(), result[i]) return result + + def rdEnum(self, section: str, option: str, default: _T_Enum) -> _T_Enum: + """Read enum value.""" + if self.has_option(section, option): + data = self.get(section, option, fallback="") + return type(default).__members__.get(data.upper(), default) + return default diff --git a/novelwriter/config.py b/novelwriter/config.py index ac403fdb..9b8752fc 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -45,6 +45,7 @@ from novelwriter.common import ( formatTimeStamp, processDialogSymbols, simplified ) from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode +from novelwriter.enum import nwTheme from novelwriter.error import formatException, logException if TYPE_CHECKING: @@ -55,8 +56,8 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -DEF_GUI = "default" -DEF_SYNTAX = "default_light" +DEF_GUI_DARK = "default_dark" +DEF_GUI_LIGHT = "default_light" DEF_ICONS = "material_rounded_normal" DEF_TREECOL = "theme" @@ -69,22 +70,22 @@ class Config: "_manuals", "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_recentProjects", "_splash", "allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName", "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", - "autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", - "dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", + "autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "darkTheme", + "dialogLine", "dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "doReplaceDots", "doReplaceSQuote", "emphLabels", "fmtApostrophe", "fmtDQuoteClose", "fmtDQuoteOpen", "fmtPadAfter", "fmtPadBefore", "fmtPadThin", "fmtSQuoteClose", - "fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "guiSyntax", "guiTheme", - "hasEnchant", "hideFocusFooter", "hideHScroll", "hideVScroll", "highlightEmph", "hostName", - "iconColDocs", "iconColTree", "iconTheme", "incNotesWCount", "isDebug", "kernelVer", - "lastNotes", "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", "narratorDialog", - "nativeFont", "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", "outlinePanePos", + "fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "hasEnchant", "hideFocusFooter", + "hideHScroll", "hideVScroll", "highlightEmph", "hostName", "iconColDocs", "iconColTree", + "iconTheme", "incNotesWCount", "isDebug", "kernelVer", "lastNotes", "lightTheme", + "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", "narratorDialog", "nativeFont", + "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", "outlinePanePos", "prefsWinSize", "scrollPastEnd", "searchCase", "searchLoop", "searchMatchCap", "searchNextFile", "searchProjCase", "searchProjRegEx", "searchProjWord", "searchRegEx", "searchWord", "showEditToolBar", "showFullPath", "showLineEndings", "showMultiSpaces", "showSessionTime", "showTabsNSpaces", "showViewerPanel", "spellLanguage", "stopWhenIdle", - "tabWidth", "textFont", "textMargin", "textWidth", "useCharCount", "userIdleTime", - "verPyQtString", "verPyQtValue", "verPyString", "verQtString", "verQtValue", - "viewComments", "viewPanePos", "viewSynopsis", "welcomeWinSize", + "tabWidth", "textFont", "textMargin", "textWidth", "themeMode", "useCharCount", + "userIdleTime", "verPyQtString", "verPyQtValue", "verPyString", "verQtString", + "verQtValue", "viewComments", "viewPanePos", "viewSynopsis", "welcomeWinSize", ) LANG_NW = 1 @@ -153,14 +154,15 @@ class Config: # General GUI Settings self.guiLocale = self._qLocale.name() - self.guiTheme = DEF_GUI # GUI theme - self.guiSyntax = DEF_SYNTAX # Syntax theme - self.guiFont = QFont() # Main GUI font - self.hideVScroll = False # Hide vertical scroll bars on main widgets - self.hideHScroll = False # Hide horizontal scroll bars on main widgets - self.lastNotes = "0x0" # The latest release notes that have been shown - self.nativeFont = True # Use native font dialog - self.useCharCount = False # Use character count as primary count + self.lightTheme = DEF_GUI_LIGHT # Light GUI theme + self.darkTheme = DEF_GUI_DARK # Dark GUI theme + self.themeMode = nwTheme.AUTO # Colour theme mode + self.guiFont = QFont() # Main GUI font + self.hideVScroll = False # Hide vertical scroll bars on main widgets + self.hideHScroll = False # Hide horizontal scroll bars on main widgets + self.lastNotes = "0x0" # The latest release notes that have been shown + self.nativeFont = True # Use native font dialog + self.useCharCount = False # Use character count as primary count # Icons self.iconTheme = DEF_ICONS # Icons theme @@ -626,8 +628,9 @@ class Config: # Main sec = "Main" self.setGuiFont(conf.rdStr(sec, "font", "")) - self.guiTheme = conf.rdStr(sec, "theme", self.guiTheme) - self.guiSyntax = conf.rdStr(sec, "syntax", self.guiSyntax) + self.lightTheme = conf.rdStr(sec, "lighttheme", self.lightTheme) + self.darkTheme = conf.rdStr(sec, "darktheme", self.darkTheme) + self.themeMode = conf.rdEnum(sec, "thememode", self.themeMode) self.iconTheme = conf.rdStr(sec, "icons", self.iconTheme) self.iconColTree = conf.rdStr(sec, "iconcoltree", self.iconColTree) self.iconColDocs = conf.rdBool(sec, "iconcoldocs", self.iconColDocs) @@ -751,8 +754,9 @@ class Config: conf["Main"] = { "font": self.guiFont.toString(), - "theme": str(self.guiTheme), - "syntax": str(self.guiSyntax), + "lighttheme": str(self.lightTheme), + "darktheme": str(self.darkTheme), + "thememode": self.themeMode.name, "icons": str(self.iconTheme), "iconcoltree": str(self.iconColTree), "iconcoldocs": str(self.iconColDocs), diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 281487b3..9587db6a 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -35,7 +35,7 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact -from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS, DEF_TREECOL from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm @@ -165,14 +165,24 @@ class GuiPreferences(NDialog): ) # Colour Theme - self.guiTheme = NComboBox(self) - self.guiTheme.setMinimumWidth(200) - for theme, name in SHARED.theme.listThemes(): - self.guiTheme.addItem(name, theme) - self.guiTheme.setCurrentData(CONFIG.guiTheme, DEF_GUI) + self.lightTheme = NComboBox(self) + self.lightTheme.setMinimumWidth(200) + self.darkTheme = NComboBox(self) + self.darkTheme.setMinimumWidth(200) + for theme, name, dark in SHARED.theme.listThemes(): + if dark: + self.darkTheme.addItem(name, theme) + else: + self.lightTheme.addItem(name, theme) + self.lightTheme.setCurrentData(CONFIG.lightTheme, DEF_GUI_LIGHT) + self.darkTheme.setCurrentData(CONFIG.darkTheme, DEF_GUI_DARK) self.mainForm.addRow( - self.tr("Colour theme"), self.guiTheme, + self.tr("Light colour theme"), self.lightTheme, + self.tr("User interface colour theme."), stretch=(3, 2) + ) + self.mainForm.addRow( + self.tr("Dark colour theme"), self.darkTheme, self.tr("User interface colour theme."), stretch=(3, 2) ) @@ -242,18 +252,6 @@ class GuiPreferences(NDialog): self.sidebar.addButton(title, section) self.mainForm.addGroupLabel(title, section) - # Document Colour Theme - self.guiSyntax = NComboBox(self) - self.guiSyntax.setMinimumWidth(200) - for syntax, name in SHARED.theme.listSyntax(): - self.guiSyntax.addItem(name, syntax) - self.guiSyntax.setCurrentData(CONFIG.guiSyntax, DEF_SYNTAX) - - self.mainForm.addRow( - self.tr("Document colour theme"), self.guiSyntax, - self.tr("Colour theme for the editor and viewer."), stretch=(3, 2) - ) - # Document Font Family self.textFont = QLineEdit(self) self.textFont.setReadOnly(True) @@ -966,18 +964,23 @@ class GuiPreferences(NDialog): # Appearance guiLocale = self.guiLocale.currentData() - guiTheme = self.guiTheme.currentData() + lightTheme = self.lightTheme.currentData() + darkTheme = self.darkTheme.currentData() iconTheme = self.iconTheme.currentData() useCharCount = self.useCharCount.isChecked() - updateTheme |= CONFIG.guiTheme != guiTheme + updateTheme |= CONFIG.lightTheme != lightTheme + updateTheme |= CONFIG.darkTheme != darkTheme updateTheme |= CONFIG.iconTheme != iconTheme needsRestart |= CONFIG.guiLocale != guiLocale needsRestart |= CONFIG.guiFont != self._guiFont refreshTree |= CONFIG.useCharCount != useCharCount + updateSyntax |= CONFIG.lightTheme != lightTheme + updateSyntax |= CONFIG.darkTheme != darkTheme CONFIG.guiLocale = guiLocale - CONFIG.guiTheme = guiTheme + CONFIG.lightTheme = lightTheme + CONFIG.darkTheme = darkTheme CONFIG.iconTheme = iconTheme CONFIG.hideVScroll = self.hideVScroll.isChecked() CONFIG.hideHScroll = self.hideHScroll.isChecked() @@ -986,11 +989,6 @@ class GuiPreferences(NDialog): CONFIG.setGuiFont(self._guiFont) # Document Style - guiSyntax = self.guiSyntax.currentData() - - updateSyntax |= CONFIG.guiSyntax != guiSyntax - - CONFIG.guiSyntax = guiSyntax CONFIG.showFullPath = self.showFullPath.isChecked() CONFIG.incNotesWCount = self.incNotesWCount.isChecked() CONFIG.setTextFont(self._textFont) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index a837d824..18899295 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -156,6 +156,13 @@ class nwFocus(Enum): OUTLINE = 3 +class nwTheme(Enum): + + AUTO = 0 + LIGHT = 1 + DARK = 2 + + class nwOutline(Enum): TITLE = 0 diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 1eb2e487..1118f7d5 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -31,16 +31,16 @@ from typing import TYPE_CHECKING, Final from PyQt6.QtCore import QSize, Qt from PyQt6.QtGui import ( - QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPainter, QPainterPath, - QPalette, QPixmap + QColor, QFont, QFontDatabase, QFontMetrics, QGuiApplication, QIcon, + QPainter, QPainterPath, QPalette, QPixmap ) from PyQt6.QtWidgets import QApplication from novelwriter import CONFIG from novelwriter.common import NWConfigParser, minmax -from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS from novelwriter.constants import nwLabels -from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme from novelwriter.error import logException from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent @@ -49,6 +49,8 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +T_ThemeEntry = tuple[str, str, bool] + STYLES_FLAT_TABS = "flatTabWidget" STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" @@ -57,6 +59,7 @@ STYLES_BIG_TOOLBUTTON = "bigToolButton" class ThemeMeta: name: str = "" + mode: str = "" description: str = "" author: str = "" credit: str = "" @@ -96,13 +99,13 @@ class GuiTheme: """ __slots__ = ( - "_availSyntax", "_availThemes", "_guiPalette", "_styleSheets", "_syntaxList", "_themeList", - "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", - "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", - "getHeaderDecorationNarrow", "getIcon", "getIconColor", "getItemIcon", "getPixmap", - "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", - "helpText", "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", - "textNWidth", "themeMeta", + "_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", + "_styleSheets", "_syntaxList", "_themeList", "baseButtonHeight", "baseIconHeight", + "baseIconSize", "buttonIconSize", "errorText", "fadedText", "fontPixelSize", + "fontPointSize", "getDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow", + "getIcon", "getIconColor", "getItemIcon", "getPixmap", "getToggleIcon", "guiFont", + "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", "iconCache", + "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth", "themeMeta", ) def __init__(self) -> None: @@ -124,8 +127,7 @@ class GuiTheme: # Load Themes self._guiPalette = QPalette() - self._themeList: list[tuple[str, str]] = [] - self._syntaxList: list[tuple[str, str]] = [] + self._themeList: list[T_ThemeEntry] = [] self._availThemes: dict[str, Path] = {} self._availSyntax: dict[str, Path] = {} self._styleSheets: dict[str, str] = {} @@ -179,13 +181,10 @@ class GuiTheme: 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 @@ -207,13 +206,35 @@ class GuiTheme: # Theme Methods ## + def isDesktopDarkMode(self) -> bool: + """Check if the desktop is in dark mode.""" + if CONFIG.verQtValue >= 0x060500 and (hint := QGuiApplication.styleHints()): + return hint.colorScheme() == Qt.ColorScheme.Dark + + palette = QPalette() + text = palette.color(QPalette.ColorRole.WindowText) + window = palette.color(QPalette.ColorRole.Window) + return text.lightnessF() > window.lightnessF() + def loadTheme(self) -> bool: """Load the currently specified GUI theme.""" - theme = CONFIG.guiTheme + match CONFIG.themeMode: + case nwTheme.LIGHT: + darkMode = False + case nwTheme.DARK: + darkMode = True + case _: + darkMode = self.isDesktopDarkMode() + + theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme if theme not in self._availThemes: logger.error("Could not find GUI theme '%s'", theme) - theme = DEF_GUI - CONFIG.guiTheme = theme + if darkMode: + theme = DEF_GUI_DARK + CONFIG.darkTheme = DEF_GUI_DARK + else: + theme = DEF_GUI_LIGHT + CONFIG.lightTheme = DEF_GUI_LIGHT if not (file := self._availThemes.get(theme)): logger.error("Could not load GUI theme") @@ -238,6 +259,7 @@ class GuiTheme: meta = ThemeMeta() if parser.has_section(sec): meta.name = parser.rdStr(sec, "name", "") + meta.mode = parser.rdStr(sec, "mode", "light") meta.description = parser.rdStr(sec, "description", "N/A") meta.author = parser.rdStr(sec, "author", "N/A") meta.credit = parser.rdStr(sec, "credit", "N/A") @@ -296,6 +318,31 @@ class GuiTheme: self.fadedText = self._parseColor(parser, sec, "fadedtext") self.errorText = self._parseColor(parser, sec, "errortext") + # Syntax + sec = "Syntax" + self.syntaxTheme = SyntaxColors() + if parser.has_section(sec): + self.syntaxTheme.back = self._parseColor(parser, sec, "background") + self.syntaxTheme.text = self._parseColor(parser, sec, "text") + self.syntaxTheme.link = self._parseColor(parser, sec, "link") + self.syntaxTheme.head = self._parseColor(parser, sec, "headertext") + self.syntaxTheme.headH = self._parseColor(parser, sec, "headertag") + self.syntaxTheme.emph = self._parseColor(parser, sec, "emphasis") + self.syntaxTheme.dialN = self._parseColor(parser, sec, "dialog") + self.syntaxTheme.dialA = self._parseColor(parser, sec, "altdialog") + self.syntaxTheme.hidden = self._parseColor(parser, sec, "hidden") + self.syntaxTheme.note = self._parseColor(parser, sec, "note") + self.syntaxTheme.code = self._parseColor(parser, sec, "shortcode") + self.syntaxTheme.key = self._parseColor(parser, sec, "keyword") + self.syntaxTheme.tag = self._parseColor(parser, sec, "tag") + self.syntaxTheme.val = self._parseColor(parser, sec, "value") + self.syntaxTheme.opt = self._parseColor(parser, sec, "optional") + self.syntaxTheme.spell = self._parseColor(parser, sec, "spellcheckline") + self.syntaxTheme.error = self._parseColor(parser, sec, "errorline") + self.syntaxTheme.repTag = self._parseColor(parser, sec, "replacetag") + self.syntaxTheme.mod = self._parseColor(parser, sec, "modifier") + self.syntaxTheme.mark = self._parseColor(parser, sec, "texthighlight") + # Update Dependant Colours # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp text = self._guiPalette.text().color() @@ -363,105 +410,22 @@ class GuiTheme: return True - def loadSyntax(self) -> bool: - """Load the currently specified syntax highlighter theme.""" - theme = CONFIG.guiSyntax - if theme not in self._availSyntax: - logger.error("Could not find syntax theme '%s'", theme) - theme = DEF_SYNTAX - CONFIG.guiSyntax = theme - - if not (file := self._availSyntax.get(theme)): - logger.error("Could not load syntax theme") - return False - - CONFIG.splashMessage("Loading syntax theme ...") - logger.info("Loading syntax theme '%s'", theme) - parser = NWConfigParser() - try: - with open(file, mode="r", encoding="utf-8") as fo: - parser.read_file(fo) - except Exception: - logger.error("Could not read file: %s", file) - logException() - return False - - # Main - sec = "Main" - meta = ThemeMeta() - if parser.has_section(sec): - meta.name = parser.rdStr(sec, "name", "") - meta.description = parser.rdStr(sec, "description", "N/A") - meta.author = parser.rdStr(sec, "author", "N/A") - meta.credit = parser.rdStr(sec, "credit", "N/A") - meta.url = parser.rdStr(sec, "url", "") - meta.license = parser.rdStr(sec, "license", "N/A") - meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") - - # Syntax - sec = "Syntax" - syntax = SyntaxColors() - if parser.has_section(sec): - syntax.back = self._parseColor(parser, sec, "background") - syntax.text = self._parseColor(parser, sec, "text") - syntax.link = self._parseColor(parser, sec, "link") - syntax.head = self._parseColor(parser, sec, "headertext") - syntax.headH = self._parseColor(parser, sec, "headertag") - syntax.emph = self._parseColor(parser, sec, "emphasis") - syntax.dialN = self._parseColor(parser, sec, "dialog") - syntax.dialA = self._parseColor(parser, sec, "altdialog") - syntax.hidden = self._parseColor(parser, sec, "hidden") - syntax.note = self._parseColor(parser, sec, "note") - syntax.code = self._parseColor(parser, sec, "shortcode") - syntax.key = self._parseColor(parser, sec, "keyword") - syntax.tag = self._parseColor(parser, sec, "tag") - syntax.val = self._parseColor(parser, sec, "value") - syntax.opt = self._parseColor(parser, sec, "optional") - syntax.spell = self._parseColor(parser, sec, "spellcheckline") - syntax.error = self._parseColor(parser, sec, "errorline") - syntax.repTag = self._parseColor(parser, sec, "replacetag") - 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 - - return True - - def listThemes(self) -> list[tuple[str, str]]: + def listThemes(self) -> list[T_ThemeEntry]: """Scan the GUI themes folder and list all themes.""" if self._themeList: return self._themeList - themes = [] + themes: list[T_ThemeEntry] = [] parser = NWConfigParser() for key, path in self._availThemes.items(): logger.debug("Checking theme config '%s'", key) - if name := _loadInternalName(parser, path): - themes.append((key, name)) + if meta := _loadInternalName(parser, path): + themes.append((key, meta[0], meta[1])) self._themeList = sorted(themes, key=_sortTheme) return self._themeList - def listSyntax(self) -> list[tuple[str, str]]: - """Scan the syntax themes folder and list all themes.""" - if self._syntaxList: - return self._syntaxList - - themes = [] - parser = NWConfigParser() - for key, path in self._availSyntax.items(): - logger.debug("Checking theme syntax '%s'", key) - if name := _loadInternalName(parser, path): - themes.append((key, name)) - - self._syntaxList = sorted(themes, key=_sortTheme) - - return self._syntaxList - def getStyleSheet(self, name: str) -> str: """Load a standard style sheet.""" return self._styleSheets.get(name, "") @@ -889,22 +853,24 @@ def _listConf(target: dict, path: Path, extension: str) -> None: return -def _sortTheme(data: tuple[str, str]) -> str: +def _sortTheme(data: tuple) -> str: """Key function for theme sorting.""" - key, name = data + key, name = data[:2] return f"*{name}" if key.startswith("default_") else name -def _loadInternalName(parser: NWConfigParser, path: str | Path) -> str: +def _loadInternalName(parser: NWConfigParser, path: str | Path) -> tuple[str, bool]: """Open a conf file and read the 'name' setting.""" try: with open(path, mode="r", encoding="utf-8") as inFile: parser.read_file(inFile) - return parser.rdStr("Main", "name", "") + name = parser.rdStr("Main", "name", "") + dark = parser.rdStr("Main", "mode", "light").lower() == "dark" + return name, dark except Exception: logger.error("Could not read file: %s", path) logException() - return "" + return "", False def _loadIconName(path: Path) -> str: diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8a895bfe..601540f0 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1069,7 +1069,6 @@ class GuiMain(QMainWindow): SHARED.project.tree.refreshAllItems() if syntax: - SHARED.theme.loadSyntax() self.docEditor.updateSyntaxColors() self.docEditor.initEditor() From 857327e9ef7a2c168075d7395ef21d31f56f47d4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:06:28 +0200 Subject: [PATCH 02/20] Update theme colour processing and fix current tests --- novelwriter/assets/themes/default_dark.conf | 1 + novelwriter/assets/themes/default_light.conf | 1 + novelwriter/gui/statusbar.py | 6 +- novelwriter/gui/theme.py | 278 +++++++++++-------- tests/reference/baseConfig_novelwriter.conf | 7 +- tests/test_base/test_base_config.py | 8 +- tests/test_dialogs/test_dlg_preferences.py | 34 +-- tests/test_gui/test_gui_guimain.py | 9 +- tests/test_gui/test_gui_theme.py | 150 ++++++---- 9 files changed, 285 insertions(+), 209 deletions(-) diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 76916604..6ccf6ede 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -1,5 +1,6 @@ [Main] name = Default Dark Theme +mode = dark description = The novelWriter standard dark theme author = Veronica Berglyd Olsen credit = Veronica Berglyd Olsen diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf index 29b4b98a..f236f404 100644 --- a/novelwriter/assets/themes/default_light.conf +++ b/novelwriter/assets/themes/default_light.conf @@ -1,5 +1,6 @@ [Main] name = Default Light Theme +mode = light description = The novelWriter standard light theme author = Veronica Berglyd Olsen credit = Veronica Berglyd Olsen diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 3239fd4a..315633da 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -143,9 +143,9 @@ class GuiMainStatus(QStatusBar): self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx)) self.timeIcon.setPixmap(self.timePixmap) - colNone = SHARED.theme.getIconColor("default").darker(150) - colSaved = SHARED.theme.getIconColor("green").darker(150) - colUnsaved = SHARED.theme.getIconColor("red").darker(150) + colNone = SHARED.theme.getBaseColor("default").darker(150) + colSaved = SHARED.theme.getBaseColor("green").darker(150) + colUnsaved = SHARED.theme.getBaseColor("red").darker(150) self.docIcon.setColors(colNone, colSaved, colUnsaved) self.projIcon.setColors(colNone, colSaved, colUnsaved) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 1118f7d5..437531a3 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -26,6 +26,7 @@ from __future__ import annotations import logging +from configparser import ConfigParser from math import ceil from typing import TYPE_CHECKING, Final @@ -37,7 +38,7 @@ from PyQt6.QtGui import ( from PyQt6.QtWidgets import QApplication from novelwriter import CONFIG -from novelwriter.common import NWConfigParser, minmax +from novelwriter.common import checkInt, minmax from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS from novelwriter.constants import nwLabels from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme @@ -99,13 +100,14 @@ class GuiTheme: """ __slots__ = ( - "_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", - "_styleSheets", "_syntaxList", "_themeList", "baseButtonHeight", "baseIconHeight", - "baseIconSize", "buttonIconSize", "errorText", "fadedText", "fontPixelSize", - "fontPointSize", "getDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow", - "getIcon", "getIconColor", "getItemIcon", "getPixmap", "getToggleIcon", "guiFont", - "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", "iconCache", - "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth", "themeMeta", + "_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", "_qColors", + "_styleSheets", "_svgColors", "_syntaxList", "_themeList", "baseButtonHeight", + "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText", + "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", + "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon", + "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", + "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth", + "themeMeta", ) def __init__(self) -> None: @@ -131,12 +133,13 @@ class GuiTheme: self._availThemes: dict[str, Path] = {} self._availSyntax: dict[str, Path] = {} self._styleSheets: dict[str, str] = {} + self._svgColors: dict[str, bytes] = {} + self._qColors: dict[str, QColor] = {} # Icon Functions self.getIcon = self.iconCache.getIcon self.getPixmap = self.iconCache.getPixmap self.getItemIcon = self.iconCache.getItemIcon - self.getIconColor = self.iconCache.getIconColor self.getToggleIcon = self.iconCache.getToggleIcon self.getDecoration = self.iconCache.getDecoration self.getHeaderDecoration = self.iconCache.getHeaderDecoration @@ -202,6 +205,14 @@ class GuiTheme: qMetrics = QFontMetrics(self.guiFont) return ceil(qMetrics.boundingRect(text).width()) + def getBaseColor(self, name: str) -> QColor: + """Return a base color.""" + return QColor(self._qColors.get(name) or QtBlack) + + def getRawBaseColor(self, name: str) -> bytes: + """Return a base color.""" + return self._svgColors.get(name, self._svgColors.get("default", b"#000000")) + ## # Theme Methods ## @@ -216,6 +227,34 @@ class GuiTheme: window = palette.color(QPalette.ColorRole.Window) return text.lightnessF() > window.lightnessF() + def parseColor(self, value: str, default: QColor = QtBlack) -> QColor: + """Parse a string as a colour value.""" + if value in self._qColors: + # Named colour + return self._qColors[value] + elif value.startswith("#"): + if len(value) >= 9: + # Convert from #RRGGBBAA to #AARRGGBB + return QColor.fromString(f"#{value[7:9]}{value[1:7]}") + else: + # Assume #RRGGBB + return QColor.fromString(value[:7]) + elif "," in value: + data = value.split(",") + entries = len(data) + if entries == 2: + # Assume name, alpha + color = self._qColors.get(data[0].strip(), default) + color.setAlpha(checkInt(data[1], 255)) + return color + else: + # Assume red, green, blue, alpha + result = [0, 0, 0, 255] + for i in range(min(entries, 4)): + result[i] = checkInt(data[i].strip(), result[i]) + return QColor(*result) + return default + def loadTheme(self) -> bool: """Load the currently specified GUI theme.""" match CONFIG.themeMode: @@ -242,7 +281,7 @@ class GuiTheme: CONFIG.splashMessage("Loading GUI theme ...") logger.info("Loading GUI theme '%s'", theme) - parser = NWConfigParser() + parser = ConfigParser() try: with open(file, mode="r", encoding="utf-8") as fo: parser.read_file(fo) @@ -258,40 +297,40 @@ class GuiTheme: sec = "Main" meta = ThemeMeta() if parser.has_section(sec): - meta.name = parser.rdStr(sec, "name", "") - meta.mode = parser.rdStr(sec, "mode", "light") - meta.description = parser.rdStr(sec, "description", "N/A") - meta.author = parser.rdStr(sec, "author", "N/A") - meta.credit = parser.rdStr(sec, "credit", "N/A") - meta.url = parser.rdStr(sec, "url", "") - meta.license = parser.rdStr(sec, "license", "N/A") - meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") + meta.name = parser.get(sec, "name", fallback="") + meta.mode = parser.get(sec, "mode", fallback="light") + meta.description = parser.get(sec, "description", fallback="N/A") + meta.author = parser.get(sec, "author", fallback="N/A") + meta.credit = parser.get(sec, "credit", fallback="N/A") + meta.url = parser.get(sec, "url", fallback="") + meta.license = parser.get(sec, "license", fallback="N/A") + meta.licenseUrl = parser.get(sec, "licenseurl", fallback="") self.themeMeta = meta # Icons - sec = "Icons" + sec = "Base" if parser.has_section(sec): - self.iconCache.setIconColor("default", self._parseColor(parser, sec, "default")) - self.iconCache.setIconColor("faded", self._parseColor(parser, sec, "faded")) - self.iconCache.setIconColor("red", self._parseColor(parser, sec, "red")) - self.iconCache.setIconColor("orange", self._parseColor(parser, sec, "orange")) - self.iconCache.setIconColor("yellow", self._parseColor(parser, sec, "yellow")) - self.iconCache.setIconColor("green", self._parseColor(parser, sec, "green")) - self.iconCache.setIconColor("aqua", self._parseColor(parser, sec, "aqua")) - self.iconCache.setIconColor("blue", self._parseColor(parser, sec, "blue")) - self.iconCache.setIconColor("purple", self._parseColor(parser, sec, "purple")) + self._setBaseColor("default", self._readColor(parser, sec, "default")) + self._setBaseColor("faded", self._readColor(parser, sec, "faded")) + self._setBaseColor("red", self._readColor(parser, sec, "red")) + self._setBaseColor("orange", self._readColor(parser, sec, "orange")) + self._setBaseColor("yellow", self._readColor(parser, sec, "yellow")) + self._setBaseColor("green", self._readColor(parser, sec, "green")) + self._setBaseColor("aqua", self._readColor(parser, sec, "aqua")) + self._setBaseColor("blue", self._readColor(parser, sec, "blue")) + self._setBaseColor("purple", self._readColor(parser, sec, "purple")) # Project sec = "Project" if parser.has_section(sec): - self.iconCache.setIconColor("root", self._parseColor(parser, sec, "root")) - self.iconCache.setIconColor("folder", self._parseColor(parser, sec, "folder")) - self.iconCache.setIconColor("file", self._parseColor(parser, sec, "file")) - self.iconCache.setIconColor("title", self._parseColor(parser, sec, "title")) - self.iconCache.setIconColor("chapter", self._parseColor(parser, sec, "chapter")) - self.iconCache.setIconColor("scene", self._parseColor(parser, sec, "scene")) - self.iconCache.setIconColor("note", self._parseColor(parser, sec, "note")) + self._setBaseColor("root", self._readColor(parser, sec, "root")) + self._setBaseColor("folder", self._readColor(parser, sec, "folder")) + self._setBaseColor("file", self._readColor(parser, sec, "file")) + self._setBaseColor("title", self._readColor(parser, sec, "title")) + self._setBaseColor("chapter", self._readColor(parser, sec, "chapter")) + self._setBaseColor("scene", self._readColor(parser, sec, "scene")) + self._setBaseColor("note", self._readColor(parser, sec, "note")) # Palette sec = "Palette" @@ -314,34 +353,34 @@ class GuiTheme: # GUI sec = "GUI" if parser.has_section(sec): - self.helpText = self._parseColor(parser, sec, "helptext") - self.fadedText = self._parseColor(parser, sec, "fadedtext") - self.errorText = self._parseColor(parser, sec, "errortext") + self.helpText = self._readColor(parser, sec, "helptext") + self.fadedText = self._readColor(parser, sec, "fadedtext") + self.errorText = self._readColor(parser, sec, "errortext") # Syntax sec = "Syntax" self.syntaxTheme = SyntaxColors() if parser.has_section(sec): - self.syntaxTheme.back = self._parseColor(parser, sec, "background") - self.syntaxTheme.text = self._parseColor(parser, sec, "text") - self.syntaxTheme.link = self._parseColor(parser, sec, "link") - self.syntaxTheme.head = self._parseColor(parser, sec, "headertext") - self.syntaxTheme.headH = self._parseColor(parser, sec, "headertag") - self.syntaxTheme.emph = self._parseColor(parser, sec, "emphasis") - self.syntaxTheme.dialN = self._parseColor(parser, sec, "dialog") - self.syntaxTheme.dialA = self._parseColor(parser, sec, "altdialog") - self.syntaxTheme.hidden = self._parseColor(parser, sec, "hidden") - self.syntaxTheme.note = self._parseColor(parser, sec, "note") - self.syntaxTheme.code = self._parseColor(parser, sec, "shortcode") - self.syntaxTheme.key = self._parseColor(parser, sec, "keyword") - self.syntaxTheme.tag = self._parseColor(parser, sec, "tag") - self.syntaxTheme.val = self._parseColor(parser, sec, "value") - self.syntaxTheme.opt = self._parseColor(parser, sec, "optional") - self.syntaxTheme.spell = self._parseColor(parser, sec, "spellcheckline") - self.syntaxTheme.error = self._parseColor(parser, sec, "errorline") - self.syntaxTheme.repTag = self._parseColor(parser, sec, "replacetag") - self.syntaxTheme.mod = self._parseColor(parser, sec, "modifier") - self.syntaxTheme.mark = self._parseColor(parser, sec, "texthighlight") + self.syntaxTheme.back = self._readColor(parser, sec, "background") + self.syntaxTheme.text = self._readColor(parser, sec, "text") + self.syntaxTheme.link = self._readColor(parser, sec, "link") + self.syntaxTheme.head = self._readColor(parser, sec, "headertext") + self.syntaxTheme.headH = self._readColor(parser, sec, "headertag") + self.syntaxTheme.emph = self._readColor(parser, sec, "emphasis") + self.syntaxTheme.dialN = self._readColor(parser, sec, "dialog") + self.syntaxTheme.dialA = self._readColor(parser, sec, "altdialog") + self.syntaxTheme.hidden = self._readColor(parser, sec, "hidden") + self.syntaxTheme.note = self._readColor(parser, sec, "note") + self.syntaxTheme.code = self._readColor(parser, sec, "shortcode") + self.syntaxTheme.key = self._readColor(parser, sec, "keyword") + self.syntaxTheme.tag = self._readColor(parser, sec, "tag") + self.syntaxTheme.val = self._readColor(parser, sec, "value") + self.syntaxTheme.opt = self._readColor(parser, sec, "optional") + self.syntaxTheme.spell = self._readColor(parser, sec, "spellcheckline") + self.syntaxTheme.error = self._readColor(parser, sec, "errorline") + self.syntaxTheme.repTag = self._readColor(parser, sec, "replacetag") + self.syntaxTheme.mod = self._readColor(parser, sec, "modifier") + self.syntaxTheme.mark = self._readColor(parser, sec, "texthighlight") # Update Dependant Colours # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp @@ -398,6 +437,18 @@ class GuiTheme: self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight) self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey) + # Set project override colours + if (override := CONFIG.iconColTree) != "theme": + color = self._svgColors.get(override, b"#000000") + self._svgColors["root"] = color + self._svgColors["folder"] = color + if not CONFIG.iconColDocs: + self._svgColors["file"] = color + self._svgColors["title"] = color + self._svgColors["chapter"] = color + self._svgColors["scene"] = color + self._svgColors["note"] = color + # Load icons after the theme is parsed self.iconCache.loadTheme(CONFIG.iconTheme) @@ -416,7 +467,7 @@ class GuiTheme: return self._themeList themes: list[T_ThemeEntry] = [] - parser = NWConfigParser() + parser = ConfigParser() for key, path in self._availThemes.items(): logger.debug("Checking theme config '%s'", key) if meta := _loadInternalName(parser, path): @@ -434,6 +485,12 @@ class GuiTheme: # Internal Functions ## + def _setBaseColor(self, key: str, color: QColor) -> None: + """Set the colour for a named colour.""" + self._qColors[key] = QColor(color) + self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8") + return + def _resetTheme(self) -> None: """Reset GUI colours to default values.""" palette = QPalette() @@ -460,37 +517,38 @@ class GuiTheme: self._guiPalette = palette - # Reset Icons - icons = self.iconCache - icons.clear() - icons.setIconColor("default", text) - icons.setIconColor("faded", faded) - icons.setIconColor("red", red) - icons.setIconColor("orange", orange) - icons.setIconColor("yellow", yellow) - icons.setIconColor("green", green) - icons.setIconColor("aqua", aqua) - icons.setIconColor("blue", blue) - icons.setIconColor("purple", purple) - icons.setIconColor("root", blue) - icons.setIconColor("folder", yellow) - icons.setIconColor("file", text) - icons.setIconColor("title", green) - icons.setIconColor("chapter", red) - icons.setIconColor("scene", blue) - icons.setIconColor("note", yellow) + # Reset Base Colours and Icons + self.iconCache.clear() + self._svgColors = {} + self._qColors = {} + self._setBaseColor("default", text) + self._setBaseColor("faded", faded) + self._setBaseColor("red", red) + self._setBaseColor("orange", orange) + self._setBaseColor("yellow", yellow) + self._setBaseColor("green", green) + self._setBaseColor("aqua", aqua) + self._setBaseColor("blue", blue) + self._setBaseColor("purple", purple) + self._setBaseColor("root", blue) + self._setBaseColor("folder", yellow) + self._setBaseColor("file", text) + self._setBaseColor("title", green) + self._setBaseColor("chapter", red) + self._setBaseColor("scene", blue) + self._setBaseColor("note", yellow) return - def _parseColor(self, parser: NWConfigParser, section: str, name: str) -> QColor: + def _readColor(self, parser: ConfigParser, section: str, name: str) -> QColor: """Parse a colour value from a config string.""" - return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255])) + return self.parseColor(parser.get(section, name, fallback="default")) def _setPalette( - self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole + self, parser: ConfigParser, section: str, name: str, value: QPalette.ColorRole ) -> None: """Set a palette colour value from a config string.""" - self._guiPalette.setBrush(value, self._parseColor(parser, section, name)) + self._guiPalette.setBrush(value, self._readColor(parser, section, name)) return def _buildStyleSheets(self, palette: QPalette) -> None: @@ -536,9 +594,8 @@ class GuiIcons: """ __slots__ = ( - "_availThemes", "_headerDec", "_headerDecNarrow", "_noIcon", - "_qColors", "_qIcons", "_svgColors", "_svgData", "_themeList", - "mainTheme", "themeMeta", + "_availThemes", "_headerDec", "_headerDecNarrow", "_meta", "_noIcon", + "_qIcons", "_svgData", "_theme", "_themeList", ) TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = { @@ -552,13 +609,11 @@ class GuiIcons: def __init__(self, mainTheme: GuiTheme) -> None: - self.mainTheme = mainTheme - self.themeMeta = ThemeMeta() + self._theme = mainTheme + self._meta = ThemeMeta() # Storage self._svgData: dict[str, bytes] = {} - self._svgColors: dict[str, bytes] = {} - self._qColors: dict[str, QColor] = {} self._qIcons: dict[str, QIcon] = {} self._headerDec: list[QPixmap] = [] self._headerDecNarrow: list[QPixmap] = [] @@ -578,12 +633,10 @@ class GuiIcons: def clear(self) -> None: """Clear the icon cache.""" self._svgData = {} - self._svgColors = {} - self._qColors = {} self._qIcons = {} self._headerDec = [] self._headerDecNarrow = [] - self.themeMeta = ThemeMeta() + self._meta = ThemeMeta() return ## @@ -622,7 +675,7 @@ class GuiIcons: meta.author = value elif key == "meta:license": meta.license = value - self.themeMeta = meta + self._meta = meta except Exception: logger.error("Could not read file: %s", file) logException() @@ -631,38 +684,16 @@ class GuiIcons: 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") - self._svgColors["root"] = color - self._svgColors["folder"] = color - if not CONFIG.iconColDocs: - self._svgColors["file"] = color - self._svgColors["title"] = color - self._svgColors["chapter"] = color - 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: - """Set an icon colour for a named colour.""" - self._qColors[key] = QColor(color) - self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8") - return - ## # Access Functions ## - def getIconColor(self, name: str) -> QColor: - """Return an icon color.""" - return QColor(self._qColors.get(name) or QtBlack) - def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon: """Return an icon from the icon buffer, or load it.""" variant = f"{name}-{color}" if color else name @@ -733,7 +764,7 @@ class GuiIcons: map or the icon map. This function always returns a QPixmap. """ if name in self.IMAGE_MAP: - idx = int(self.mainTheme.isDarkTheme) + idx = int(self._theme.isDarkTheme) imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx] else: logger.error("Decoration with name '%s' does not exist", name) @@ -757,7 +788,7 @@ class GuiIcons: def getHeaderDecoration(self, hLevel: int) -> QPixmap: """Get the decoration for a specific heading level.""" if not self._headerDec: - iPx = self.mainTheme.baseIconHeight + iPx = self._theme.baseIconHeight self._headerDec = [ self._generateDecoration("file", iPx, 0), self._generateDecoration("title", iPx, 0), @@ -770,7 +801,7 @@ class GuiIcons: def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap: """Get the narrow decoration for a specific heading level.""" if not self._headerDecNarrow: - iPx = self.mainTheme.baseIconHeight + iPx = self._theme.baseIconHeight self._headerDecNarrow = [ self._generateDecoration("file", iPx, 0), self._generateDecoration("title", iPx, 0), @@ -811,7 +842,7 @@ class GuiIcons: return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg")) if svg := self._svgData.get(name, b""): - if fill := self._svgColors.get(color or "default"): + if fill := self._theme.getRawBaseColor(color or "default"): svg = svg.replace(b"#000000", fill) pixmap = QPixmap(w, h) pixmap.fill(QtTransparent) @@ -833,7 +864,7 @@ class GuiIcons: painter = QPainter(pixmap) painter.setRenderHint(QtPaintAntiAlias) - if fill := self._svgColors.get(color or "default"): + if fill := self._theme.getRawBaseColor(color or "default"): painter.fillPath(path, QColor(fill.decode(encoding="utf-8"))) painter.end() @@ -859,13 +890,14 @@ def _sortTheme(data: tuple) -> str: return f"*{name}" if key.startswith("default_") else name -def _loadInternalName(parser: NWConfigParser, path: str | Path) -> tuple[str, bool]: +def _loadInternalName(parser: ConfigParser, path: str | Path) -> tuple[str, bool]: """Open a conf file and read the 'name' setting.""" try: + parser.clear() with open(path, mode="r", encoding="utf-8") as inFile: parser.read_file(inFile) - name = parser.rdStr("Main", "name", "") - dark = parser.rdStr("Main", "mode", "light").lower() == "dark" + name = parser.get("Main", "name", fallback="") + dark = parser.get("Main", "mode", fallback="light").lower() == "dark" return name, dark except Exception: logger.error("Could not read file: %s", path) diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 78441636..8943a9b2 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -1,10 +1,11 @@ [Meta] -timestamp = 2025-05-11 13:28:11 +timestamp = 2025-06-02 16:21:38 [Main] font = -theme = default -syntax = default_light +lighttheme = default_light +darktheme = default_dark +thememode = AUTO icons = material_rounded_normal iconcoltree = theme iconcoldocs = False diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 81b03831..98a1063d 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -131,15 +131,15 @@ def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths): assert tstConf.errorText().startswith("Could not load config file") # Change a few settings, save, reset, and reload - tstConf.guiTheme = "foo" - tstConf.guiSyntax = "bar" + tstConf.lightTheme = "foo" + tstConf.darkTheme = "bar" assert tstConf.saveConfig() is True newConf = Config() newConf.initConfig(confPath=fncPath, dataPath=fncPath) newConf.loadConfig() - assert newConf.guiTheme == "foo" - assert newConf.guiSyntax == "bar" + assert newConf.lightTheme == "foo" + assert newConf.darkTheme == "bar" # Test Correcting Quote Settings tstConf.fmtDQuoteOpen = '"' diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index 5572f430..ccb77133 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -27,7 +27,7 @@ from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent from PyQt6.QtWidgets import QFileDialog, QFontDialog from novelwriter import CONFIG, SHARED -from novelwriter.config import DEF_GUI +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT from novelwriter.constants import nwUnicode from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.quotes import GuiQuoteSelect @@ -55,15 +55,11 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): assert "en_GB" in languages # Check GUI Themes - themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())] - assert len(themes) >= 5 - assert DEF_GUI in themes + themes = [prefs.lightTheme.itemData(i) for i in range(prefs.lightTheme.count())] + assert DEF_GUI_LIGHT in themes - # Check GUI Syntax - syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())] - assert len(syntax) >= 10 - assert "default_dark" in syntax - assert "default_light" in syntax + themes = [prefs.darkTheme.itemData(i) for i in range(prefs.darkTheme.count())] + assert DEF_GUI_DARK in themes # Check Spell Checking spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())] @@ -124,7 +120,7 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): button = prefs.buttonBox.button(QtDialogSave) assert button is not None button.click() - assert signal.args == [False, False, False, False] + assert len(signal.args) == 4 # Check Close Button prefs.show() @@ -151,6 +147,12 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): (fncPath / "nw_en_US.qm").touch() (fncPath / "project_en_US.json").touch() CONFIG._nwLangPath = fncPath + SHARED.theme._themeList = [ + ("theme1", "Theme 1", False), + ("theme2", "Theme 2", False), + ("theme3", "Theme 3", True), + ("theme4", "Theme 4", True), + ] prefs = GuiPreferences(nwGUI) with qtbot.waitExposed(prefs): @@ -158,7 +160,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): # Appearance prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US")) - prefs.guiTheme.setCurrentIndex(prefs.guiTheme.findData("default_dark")) + prefs.lightTheme.setCurrentIndex(prefs.lightTheme.findData("theme1")) + prefs.darkTheme.setCurrentIndex(prefs.darkTheme.findData("theme3")) with monkeypatch.context() as mp: mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True)) prefs.nativeFont.setChecked(True) # Use OS font dialog @@ -169,14 +172,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): prefs.useCharCount.setChecked(True) assert CONFIG.guiLocale != "en_US" - assert CONFIG.guiTheme != "default_dark" + assert CONFIG.lightTheme == "default_light" + assert CONFIG.darkTheme == "default_dark" assert CONFIG.guiFont.family() != "" assert CONFIG.hideVScroll is False assert CONFIG.hideHScroll is False assert CONFIG.useCharCount is False # Document Style - prefs.guiSyntax.setCurrentIndex(prefs.guiSyntax.findData("default_dark")) with monkeypatch.context() as mp: mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True)) prefs.nativeFont.setChecked(False) # Use Qt font dialog @@ -185,7 +188,6 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): prefs.showFullPath.setChecked(False) prefs.incNotesWCount.setChecked(False) - assert CONFIG.guiSyntax != "default_dark" assert CONFIG.textFont.family() != "" assert CONFIG.showFullPath is True assert CONFIG.incNotesWCount is True @@ -344,14 +346,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): # Appearance assert CONFIG.guiLocale == "en_US" - assert CONFIG.guiTheme == "default_dark" + assert CONFIG.lightTheme == "theme1" + assert CONFIG.darkTheme == "theme3" assert CONFIG.guiFont == QFont() assert CONFIG.hideVScroll is True assert CONFIG.hideHScroll is True assert CONFIG.useCharCount is True # Document Style - assert CONFIG.guiSyntax == "default_dark" assert CONFIG.textFont == QFont() assert CONFIG.showFullPath is False assert CONFIG.incNotesWCount is False diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 6fda9b6a..91207d09 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -34,7 +34,7 @@ from PyQt6.QtWidgets import QInputDialog, QMessageBox from novelwriter import CONFIG, SHARED from novelwriter.constants import nwFiles from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwView +from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwTheme, nwView from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.outline import GuiOutlineView @@ -180,10 +180,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): def testGuiMain_UpdateTheme(qtbot, nwGUI): """Test updating the theme in the GUI.""" mainTheme = SHARED.theme - CONFIG.guiTheme = "default_dark" - CONFIG.guiSyntax = "default_dark" + CONFIG.themeMode = nwTheme.DARK + CONFIG.darkTheme = "default_dark" + CONFIG.lightTheme = "default_light" mainTheme.loadTheme() - mainTheme.loadSyntax() + nwGUI._processConfigChanges(False, True, False, False) nwGUI._processConfigChanges(True, True, True, True) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index c64f1172..ff4308f2 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -22,6 +22,7 @@ from __future__ import annotations import sys +from configparser import ConfigParser from pathlib import Path import pytest @@ -29,8 +30,7 @@ import pytest from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap from novelwriter import CONFIG, SHARED -from novelwriter.common import NWConfigParser -from novelwriter.config import DEF_GUI +from novelwriter.config import DEF_GUI_LIGHT from novelwriter.constants import nwLabels from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.gui.theme import _listConf @@ -39,17 +39,51 @@ from tests.mocked import causeOSError from tests.tools import writeFile +@pytest.mark.gui +def testGuiTheme_ParseColor(qtbot, nwGUI): + """Test the colour parsing.""" + theme = SHARED.theme + + # Pre-Populate + theme._qColors["red"] = QColor(255, 0, 0) + theme._qColors["green"] = QColor(0, 255, 0) + theme._qColors["blue"] = QColor(0, 0, 255) + + # By Name + assert theme.parseColor("red").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("green").getRgb() == (0, 255, 0, 255) + assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255) + assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255) + + # CSS Format + assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored + assert theme.parseColor("#ff00007f15").getRgb() == (255, 0, 0, 127) # Too long -> truncated + + # Name + Alpha + assert theme.parseColor("red, 255").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("red, 127").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("red, 512").getRgb() == (255, 0, 0, 255) # Value truncated + + # Values + assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated + + @pytest.mark.gui def testGuiTheme_Main(qtbot, nwGUI, tstPaths): """Test the theme class init.""" - mainTheme = SHARED.theme + theme = SHARED.theme # Methods # ======= - mSize = mainTheme.getTextWidth("m") + mSize = theme.getTextWidth("m") assert mSize > 0 - assert mainTheme.getTextWidth("m", mainTheme.guiFont) == mSize + assert theme.getTextWidth("m", theme.guiFont) == mSize # Scan for Themes # =============== @@ -70,7 +104,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): # Parse Colours # ============= - parser = NWConfigParser() + parser = ConfigParser() parser["Palette"] = { "colour1": "100, 150, 200", # Valid "colour2": "100, 150, 200, 250", # With alpha @@ -81,38 +115,39 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): } # Test the parser for several valid and invalid values - assert mainTheme._parseColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255) - assert mainTheme._parseColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250) - assert mainTheme._parseColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250) - assert mainTheme._parseColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255) - assert mainTheme._parseColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0) - assert mainTheme._parseColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255) + assert theme._readColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255) + assert theme._readColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250) + assert theme._readColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250) + assert theme._readColor(parser, "Palette", "colour4").getRgb() == (0, 0, 0, 250) + assert theme._readColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0) + assert theme._readColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255) # The palette should load with the parsed values - mainTheme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255) - mainTheme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - mainTheme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - mainTheme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255) - mainTheme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0) - mainTheme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255) + theme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255) + theme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) + theme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) + theme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 250) + theme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0) + theme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255) # Non-existing value should return default colour - mainTheme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255) + theme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255) # qtbot.stop() @pytest.mark.gui +@pytest.mark.skip def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): """Test the theme part of the class.""" - mainTheme = SHARED.theme + theme = SHARED.theme # List Themes # =========== @@ -120,45 +155,46 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): # Block the reading of the files with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - assert mainTheme.listThemes() == [] + theme._themeList = [] + assert theme.listThemes() == [] # Load the theme info, default themes first - themesList = mainTheme.listThemes() + themesList = theme.listThemes() assert themesList[0] == ("default_dark", "Default Dark Theme") assert themesList[1] == ("default_light", "Default Light Theme") assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night") assert themesList[3] == ("dracula", "Dracula") # A second call should returned the cached list - assert mainTheme.listThemes() == mainTheme._themeList + assert theme.listThemes() == theme._themeList # Check handling of broken theme settings CONFIG.guiTheme = "not_a_theme" - availThemes = mainTheme._availThemes - mainTheme._availThemes = {} - assert mainTheme.loadTheme() is False - mainTheme._availThemes = availThemes + availThemes = theme._availThemes + theme._availThemes = {} + assert theme.loadTheme() is False + theme._availThemes = availThemes # Check handling of unreadable file - CONFIG.guiTheme = DEF_GUI + CONFIG.guiTheme = DEF_GUI_LIGHT with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - assert mainTheme.loadTheme() is False + assert theme.loadTheme() is False # Load Default Theme # ================== if sys.platform != "win32": # Set a mock colour for the window background - mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0) + theme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0) # Load the default theme - CONFIG.guiTheme = DEF_GUI - assert mainTheme.loadTheme() is True + CONFIG.guiTheme = DEF_GUI_LIGHT + assert theme.loadTheme() is True # This should load a standard palette wCol = QPalette().color(QPalette.ColorRole.Window).getRgb() - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol # Mock Dark Theme # =============== @@ -172,32 +208,32 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): "window = 0, 0, 0\n" "text = 255, 255, 255\n" ) - mainTheme._availThemes["test"] = mockTheme + theme._availThemes["test"] = mockTheme CONFIG.guiTheme = "test" - assert mainTheme.loadTheme() is True - 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 + assert theme.loadTheme() is True + assert theme._guiPalette.window().color().getRgb() == (0, 0, 0, 255) + assert theme._guiPalette.text().color().getRgb() == (255, 255, 255, 255) + assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255) + assert theme.isDarkTheme is True # Load Default Light Theme # ======================== CONFIG.guiTheme = "default_light" - assert mainTheme.loadTheme() is True + assert theme.loadTheme() is True # Check a few values - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.Window ).getRgb() == (239, 239, 239, 255) - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.WindowText ).getRgb() == (0, 0, 0, 255) - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.Base ).getRgb() == (255, 255, 255, 255) - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.AlternateBase ).getRgb() == (224, 224, 224, 255) @@ -205,21 +241,22 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): # ======================= CONFIG.guiTheme = "default_dark" - assert mainTheme.loadTheme() is True + assert theme.loadTheme() is True # Check a few values - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255) - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255) - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255) - assert mainTheme._guiPalette.color( + assert theme._guiPalette.color( QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255) # qtbot.stop() +@pytest.mark.skip @pytest.mark.gui def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): """Test the syntax part of the class.""" @@ -283,6 +320,7 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): # qtbot.stop() +@pytest.mark.skip @pytest.mark.gui def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths): """Test the icon cache class.""" From 04526385d1ae7757aeeec52c3ca5cc059995c571 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:43:27 +0200 Subject: [PATCH 03/20] Update existing matched GUI and syntax themes --- .../assets/syntax/cyberpunk_night.conf | 28 ----- novelwriter/assets/syntax/dracula.conf | 44 ------- novelwriter/assets/syntax/snazzy.conf | 42 ------- novelwriter/assets/syntax/solarized_dark.conf | 29 ----- .../assets/syntax/solarized_light.conf | 29 ----- .../assets/themes/cyberpunk_night.conf | 91 +++++++++------ novelwriter/assets/themes/dracula.conf | 94 +++++++++------ novelwriter/assets/themes/snazzy.conf | 104 +++++++++++------ novelwriter/assets/themes/solarized_dark.conf | 110 ++++++++++++------ .../assets/themes/solarized_light.conf | 110 ++++++++++++------ 10 files changed, 338 insertions(+), 343 deletions(-) delete mode 100644 novelwriter/assets/syntax/cyberpunk_night.conf delete mode 100644 novelwriter/assets/syntax/dracula.conf delete mode 100644 novelwriter/assets/syntax/snazzy.conf delete mode 100644 novelwriter/assets/syntax/solarized_dark.conf delete mode 100644 novelwriter/assets/syntax/solarized_light.conf diff --git a/novelwriter/assets/syntax/cyberpunk_night.conf b/novelwriter/assets/syntax/cyberpunk_night.conf deleted file mode 100644 index 57a14536..00000000 --- a/novelwriter/assets/syntax/cyberpunk_night.conf +++ /dev/null @@ -1,28 +0,0 @@ -[Main] -name = Cyberpunk Night -author = Anders Lemvigh -url = https://github.com/alemvigh -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -[Syntax] -background = 0, 0, 0 -text = 150, 150, 150 -link = 77, 77, 255 -headertext = 255, 255, 255 -headertag = 50, 0, 180 -emphasis = 0, 255, 255 -dialog = 0, 255, 0 -altdialog = 0, 140, 255 -note = 150, 150, 150 -hidden = 77, 77, 100 -shortcode = 255, 255, 0 -keyword = 255, 100, 255 -tag = 255, 150, 10 -value = 255, 150, 10 -spellcheckline = 242, 72, 23 -errorline = 186, 218, 4 -replacetag = 0, 0, 180 -modifier = 144, 142, 176 -optional = 180, 180, 180 -texthighlight = 255, 255, 132, 96 diff --git a/novelwriter/assets/syntax/dracula.conf b/novelwriter/assets/syntax/dracula.conf deleted file mode 100644 index fad01ea8..00000000 --- a/novelwriter/assets/syntax/dracula.conf +++ /dev/null @@ -1,44 +0,0 @@ -[Main] -name = Dracula -author = Veronica Berglyd Olsen (adaptation) -credit = Zeno Rocha -url = https://draculatheme.com -license = MIT -licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE - -## -# Colours: -# Background = 282a36 : 40, 42, 54 -# Foreground = f8f8f2 : 248, 248, 242 -# Comment = 6272a4 : 98, 114, 164 -# Cyan = 8be9fd : 139, 233, 253 -# Green = 50fa7b : 80, 250, 123 -# Orange = ffb86c : 255, 184, 108 -# Pink = ff79c6 : 255, 121, 198 -# Purple = bd93f9 : 189, 147, 249 -# Red = ff5555 : 255, 85, 85 -# Yellow = f1fa8c : 241, 250, 140 -## - -[Syntax] -# See: https://spec.draculatheme.com/ -background = 40, 42, 54 -text = 248, 248, 242 -link = 255, 121, 198 -headertext = 189, 147, 249 -headertag = 189, 147, 249, 160 -emphasis = 255, 184, 108 -dialog = 80, 250, 123 -altdialog = 241, 250, 140 -note = 255, 204, 233 -hidden = 98, 114, 164 -shortcode = 139, 233, 253 -keyword = 255, 121, 198 -tag = 255, 184, 108 -value = 255, 184, 108 -optional = 80, 250, 123 -spellcheckline = 255, 85, 85 -errorline = 80, 250, 123 -replacetag = 241, 250, 140 -modifier = 139, 233, 253 -texthighlight = 241, 250, 140, 96 diff --git a/novelwriter/assets/syntax/snazzy.conf b/novelwriter/assets/syntax/snazzy.conf deleted file mode 100644 index d948b770..00000000 --- a/novelwriter/assets/syntax/snazzy.conf +++ /dev/null @@ -1,42 +0,0 @@ -[Main] -name = Snazzy Light -author = Veronica Berglyd Olsen (adaptation) -credit = Florian Reuschel (color theme) -url = https://github.com/loilo/vscode-snazzy-light -license = MIT License -licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE - -## -# Colours: -# Background = fafbfc : 250, 251, 252 -# Foreground = 565869 : 86, 88, 105 -# Comment = 9194a2 : 145, 148, 162 -# Yellow = cf9c00 : 207, 156, 0 -# Red = ff5c57 : 255, 92, 87 -# Pink = f767bb : 247, 103, 187 -# Blue = 09a1ed : 9, 161, 237 -# Cyan = 13bbb7 : 19, 187, 183 -# Green = 2dae58 : 45, 174, 88 -## - -[Syntax] -background = 250, 251, 252 -text = 86, 88, 105 -link = 9, 161, 237 -headertext = 45, 174, 88 -headertag = 45, 174, 88, 160 -emphasis = 247, 103, 187 -dialog = 9, 161, 237 -altdialog = 207, 156, 0 -note = 120, 187, 185 -hidden = 145, 148, 162 -shortcode = 247, 103, 187 -keyword = 9, 161, 237 -tag = 45, 174, 88 -value = 207, 156, 0 -optional = 207, 156, 0 -spellcheckline = 255, 92, 87 -errorline = 45, 174, 88 -replacetag = 19, 187, 183 -modifier = 247, 103, 187 -texthighlight = 207, 156, 0, 96 diff --git a/novelwriter/assets/syntax/solarized_dark.conf b/novelwriter/assets/syntax/solarized_dark.conf deleted file mode 100644 index 0fbacdb7..00000000 --- a/novelwriter/assets/syntax/solarized_dark.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Solarized Dark -author = nullbasis -credit = Ethan Schoonover -url = https://ethanschoonover.com/solarized/ -license = MIT -licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE - -[Syntax] -background = 7, 54, 66 -text = 253, 246, 227 -link = 38, 139, 210 -headertext = 147, 161, 161 -headertag = 42, 161, 152 -emphasis = 38, 139, 210 -dialog = 42, 161, 152 -altdialog = 42, 161, 152 -note = 101, 161, 156 -hidden = 147, 161, 161 -shortcode = 147, 161, 161 -keyword = 133, 153, 0 -tag = 203, 75, 22 -value = 203, 75, 22 -optional = 147, 161, 161 -spellcheckline = 203, 75, 22 -errorline = 220, 50, 47 -replacetag = 133, 153, 0 -modifier = 181, 137, 0 -texthighlight = 181, 137, 0, 96 diff --git a/novelwriter/assets/syntax/solarized_light.conf b/novelwriter/assets/syntax/solarized_light.conf deleted file mode 100644 index 525a59e5..00000000 --- a/novelwriter/assets/syntax/solarized_light.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Solarized Light -author = nullbasis -credit = Ethan Schoonover -url = https://ethanschoonover.com/solarized/ -license = MIT -licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE - -[Syntax] -background = 253, 246, 227 -text = 0, 43, 54 -link = 38, 139, 210 -headertext = 88, 110, 117 -headertag = 42, 161, 152 -emphasis = 38, 139, 210 -dialog = 42, 161, 152 -altdialog = 42, 161, 152 -note = 27, 102, 96 -hidden = 88, 110, 117 -shortcode = 88, 110, 117 -keyword = 133, 153, 0 -tag = 203, 75, 22 -value = 203, 75, 22 -optional = 88, 110, 117 -spellcheckline = 203, 75, 22 -errorline = 220, 50, 47 -replacetag = 133, 153, 0 -modifier = 181, 137, 0 -texthighlight = 181, 137, 0, 96 diff --git a/novelwriter/assets/themes/cyberpunk_night.conf b/novelwriter/assets/themes/cyberpunk_night.conf index 125cab45..1ee10ca8 100644 --- a/novelwriter/assets/themes/cyberpunk_night.conf +++ b/novelwriter/assets/themes/cyberpunk_night.conf @@ -1,48 +1,71 @@ [Main] name = Cyberpunk Night +mode = dark description = A taste of the future 80s author = Anders Lemvigh url = https://github.com/alemvigh license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ -[Icons] -default = 136, 136, 136 -faded = 97, 97, 97 -red = 242, 72, 23 -orange = 255, 150, 10 -yellow = 255, 255, 0 -green = 0, 255, 0 -aqua = 0, 255, 255 -blue = 77, 77, 255 -purple = 50, 0, 180 +[Base] +default = #888888 +faded = #616161 +red = #f24817 +orange = #ff960a +yellow = #ffff00 +green = #00ff00 +aqua = #00ffff +blue = #4d4dff +purple = #320064 [Project] -root = 77, 77, 255 -folder = 255, 255, 0 -file = 136, 136, 136 -title = 0, 255, 0 -chapter = 242, 72, 23 -scene = 77, 77, 255 -note = 255, 255, 0 +root = #4d4dff +folder = #ffff00 +file = #888888 +title = #00ff00 +chapter = #f24817 +scene = #4d4dff +note = #ffff00 [Palette] -window = 0, 0, 0 -windowtext = 150, 150, 150 -base = 0, 0, 0 -alternatebase = 40, 40, 40 -text = 150, 150, 150 -tooltipbase = 40, 20, 70 -tooltiptext = 255, 255, 255 -button = 5, 0, 10 -buttontext = 150, 150, 150 -brighttext = 255, 255, 255 -highlight = 50, 30, 80 -highlightedtext = 255, 255, 255 -link = 77, 77, 255 -linkvisited = 50, 0, 80 +window = #000000 +windowtext = #969696 +base = #000000 +alternatebase = #282828 +text = #969696 +tooltipbase = #281446 +tooltiptext = #ffffff +button = #05000a +buttontext = #969696 +brighttext = #ffffff +highlight = #321e50 +highlightedtext = #ffffff +link = #4d4dff +linkvisited = #320050 [GUI] -helptext = 97, 97, 97 -fadedtext = 97, 97, 97 -errortext = 242, 72, 23 +helptext = #616161 +fadedtext = #616161 +errortext = #f24817 + +[Syntax] +background = #000000 +text = #969696 +link = #4d4dff +headertext = #ffffff +headertag = #320064 +emphasis = #00ffff +dialog = #00ff00 +altdialog = #008cff +note = #969696 +hidden = #4d4d64 +shortcode = #ffff00 +keyword = #ff64ff +tag = #ff960a +value = #ff960a +spellcheckline = #f24817 +errorline = #bada04 +replacetag = #0000b4 +modifier = #908eb0 +optional = #b4b4b4 +texthighlight = #ffff8460 diff --git a/novelwriter/assets/themes/dracula.conf b/novelwriter/assets/themes/dracula.conf index 5805b84b..49f30501 100644 --- a/novelwriter/assets/themes/dracula.conf +++ b/novelwriter/assets/themes/dracula.conf @@ -1,5 +1,6 @@ [Main] name = Dracula +mode = dark description = A dark theme with bright colours author = Veronica Berglyd Olsen (adaptation) credit = Zeno Rocha @@ -9,9 +10,10 @@ licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE ## # Colours: +# See: https://spec.draculatheme.com/ # Background = 282a36 : 40, 42, 54 # Foreground = f8f8f2 : 248, 248, 242 -# Current Line = 44475A : 68, 71, 90 +# Current Line = 44475a : 68, 71, 90 # Comment = 6272a4 : 98, 114, 164 # Cyan = 8be9fd : 139, 233, 253 # Green = 50fa7b : 80, 250, 123 @@ -22,43 +24,65 @@ licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE # Yellow = f1fa8c : 241, 250, 140 ## -[Icons] -default = 230, 230, 224 -faded = 98, 114, 164 -red = 255, 85, 85 -orange = 255, 184, 108 -yellow = 241, 250, 140 -green = 80, 250, 123 -aqua = 139, 233, 253 -blue = 147, 207, 249 -purple = 189, 147, 249 +[Base] +default = #e6e6e0 +faded = #6272a4 +red = #ff5555 +orange = #ffb86c +yellow = #f1fa8c +green = #50fa7b +aqua = #8be9fd +blue = #93cff9 +purple = #bd93f9 [Project] -root = 189, 147, 249 -folder = 241, 250, 140 -file = 230, 230, 224 -title = 80, 250, 123 -chapter = 255, 85, 85 -scene = 139, 233, 253 -note = 241, 250, 140 +root = #bd93f9 +folder = #f1fa8c +file = #e6e6e0 +title = #50fa7b +chapter = #ff5555 +scene = #8be9fd +note = #f1fa8c [Palette] -window = 68, 71, 90 -windowtext = 248, 248, 242 -base = 40, 42, 54 -alternatebase = 51, 54, 69 -text = 248, 248, 242 -tooltipbase = 241, 250, 140 -tooltiptext = 40, 42, 54 -button = 80, 83, 105 -buttontext = 248, 248, 242 -brighttext = 68, 71, 90 -highlight = 166, 129, 218 -highlightedtext = 248, 248, 242 -link = 139, 233, 253 -linkvisited = 139, 233, 253 +window = #44475a +windowtext = #f8f8f2 +base = #282a36 +alternatebase = #333645 +text = #f8f8f2 +tooltipbase = #f1fa8c +tooltiptext = #282a36 +button = #505369 +buttontext = #f8f8f2 +brighttext = #44475a +highlight = #a681da +highlightedtext = #f8f8f2 +link = #8be9fd +linkvisited = #8be9fd [GUI] -helptext = 204, 172, 249 -fadedtext = 98, 114, 164 -errortext = 255, 85, 85 +helptext = #ccacf9 +fadedtext = #6272a4 +errortext = #ff5555 + +[Syntax] +background = #282a36 +text = #f8f8f2 +link = #ff79c6 +headertext = #bd93f9 +headertag = #bd93f9a0 +emphasis = #ffb86c +dialog = #50fa7b +altdialog = #f1fa8c +note = #ffcce9 +hidden = #6272a4 +shortcode = #8be9fd +keyword = #ff79c6 +tag = #ffb86c +value = #ffb86c +optional = #50fa7b +spellcheckline = #ff5555 +errorline = #50fa7b +replacetag = #f1fa8c +modifier = #8be9fd +texthighlight = #f1fa8c60 diff --git a/novelwriter/assets/themes/snazzy.conf b/novelwriter/assets/themes/snazzy.conf index 39ec3c87..19a662c0 100644 --- a/novelwriter/assets/themes/snazzy.conf +++ b/novelwriter/assets/themes/snazzy.conf @@ -1,48 +1,84 @@ [Main] name = Snazzy Light +mode = light author = Veronica Berglyd Olsen (adaptation) credit = Florian Reuschel (color theme) url = https://github.com/loilo/vscode-snazzy-light license = MIT License licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE -[Icons] -default = 86, 88, 105 -faded = 84, 85, 84 -red = 255, 92, 87 -orange = 245, 185, 0 -yellow = 207, 156, 0 -green = 45, 174, 88 -aqua = 19, 187, 183 -blue = 9, 161, 237 -purple = 247, 103, 187 +## +# Colours: +# Background = fafbfc : 250, 251, 252 +# Foreground = 565869 : 86, 88, 105 +# Comment = 9194a2 : 145, 148, 162 +# Yellow = cf9c00 : 207, 156, 0 +# Red = ff5c57 : 255, 92, 87 +# Pink = f767bb : 247, 103, 187 +# Blue = 09a1ed : 9, 161, 237 +# Cyan = 13bbb7 : 19, 187, 183 +# Green = 2dae58 : 45, 174, 88 +## + +[Base] +default = #565869 +faded = #545554 +red = #ff5c57 +orange = #f5b900 +yellow = #cf9c00 +green = #2dae58 +aqua = #13bbb7 +blue = #09a1ed +purple = #f767bb [Project] -root = 9, 161, 237 -folder = 207, 156, 0 -file = 84, 85, 84 -title = 45, 174, 88 -chapter = 255, 92, 87 -scene = 9, 161, 237 -note = 207, 156, 0 +root = #09a1ed +folder = #cf9c00 +file = #545554 +title = #2dae58 +chapter = #ff5c57 +scene = #09a1ed +note = #cf9c00 [Palette] -window = 243, 244, 245 -windowtext = 86, 88, 105 -base = 250, 251, 252 -alternatebase = 234, 234, 235 -text = 86, 88, 105 -tooltipbase = 245, 233, 194 -tooltiptext = 86, 88, 105 -button = 250, 251, 252 -buttontext = 86, 88, 105 -brighttext = 255, 255, 255 -highlight = 9, 161, 237 -highlightedtext = 255, 255, 255 -link = 9, 161, 237 -linkvisited = 9, 161, 237 +window = #f3f4f5 +windowtext = #565869 +base = #fafbfc +alternatebase = #eaeaeb +text = #565869 +tooltipbase = #f5e9c2 +tooltiptext = #565869 +button = #fafbfc +buttontext = #565869 +brighttext = #ffffff +highlight = #09a1ed +highlightedtext = #ffffff +link = #09a1ed +linkvisited = #09a1ed [GUI] -helptext = 9, 161, 237 -fadedtext = 84, 85, 84 -errortext = 255, 92, 87 +helptext = #09a1ed +fadedtext = #545554 +errortext = #ff5c57 + +[Syntax] +background = #fafbfc +text = #565869 +link = #09a1ed +headertext = #2dae58 +headertag = #2dae58a0 +emphasis = #f767bb +dialog = #09a1ed +altdialog = #cf9c00 +note = #78bbb9 +hidden = #9194a2 +shortcode = #f767bb +keyword = #09a1ed +tag = #2dae58 +value = #cf9c00 +optional = #cf9c00 +spellcheckline = #ff5c57 +errorline = #2dae58 +replacetag = #13bbb7 +modifier = #f767bb +texthighlight = #cf9c0060 diff --git a/novelwriter/assets/themes/solarized_dark.conf b/novelwriter/assets/themes/solarized_dark.conf index 0f401eb9..354b2be9 100644 --- a/novelwriter/assets/themes/solarized_dark.conf +++ b/novelwriter/assets/themes/solarized_dark.conf @@ -1,48 +1,90 @@ [Main] name = Solarized Dark +mode = dark author = nullbasis credit = Ethan Schoonover url = https://ethanschoonover.com/solarized/ license = MIT licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE -[Icons] -default = 230, 223, 206 -faded = 166, 161, 149 -red = 220, 50, 47 -orange = 203, 75, 22 -yellow = 181, 137, 0 -green = 133, 153, 0 -aqua = 42, 161, 152 -blue = 38, 139, 210 -purple = 211, 54, 130 +## +# base03: #002b36 +# base02: #073642 +# base01: #586e75 +# base00: #657b83 +# base0: #839496 +# base1: #93a1a1 +# base2: #eee8d5 +# base3: #fdf6e3 +# yellow: #b58900 +# orange: #cb4b16 +# red: #dc322f +# magenta: #d33682 +# violet: #6c71c4 +# blue: #268bd2 +# cyan: #2aa198 +# green: #859900 +## + +[Base] +default = #e6dfce +faded = #a6a195 +red = #dc322f +orange = #cb4b16 +yellow = #b58900 +green = #859900 +aqua = #2aa198 +blue = #268bd2 +purple = #6c71c4 [Project] -root = 42, 161, 152 -folder = 42, 161, 152 -file = 230, 223, 206 -title = 133, 153, 0 -chapter = 220, 50, 47 -scene = 38, 139, 210 -note = 181, 137, 0 +root = #2aa198 +folder = #2aa198 +file = #e6dfce +title = #859900 +chapter = #dc322f +scene = #268bd2 +note = #b58900 [Palette] -window = 0, 43, 54 -windowtext = 253, 246, 227 -base = 7, 54, 66 -alternatebase = 88, 110, 117 -text = 253, 246, 227 -tooltipbase = 133, 153, 0 -tooltiptext = 0, 43, 54 -button = 7, 54, 66 -buttontext = 253, 246, 227 -brighttext = 7, 54, 66 -highlight = 42, 161, 152 -highlightedtext = 0, 43, 54 -link = 38, 139, 210 -linkvisited = 38, 139, 210 +window = #002b36 +windowtext = #fdf6e3 +base = #073642 +alternatebase = #586e75 +text = #fdf6e3 +tooltipbase = #859900 +tooltiptext = #002b36 +button = #073642 +buttontext = #fdf6e3 +brighttext = #073642 +highlight = #2aa198 +highlightedtext = #002b36 +link = #268bd2 +linkvisited = #268bd2 [GUI] -helptext = 101, 123, 131 -fadedtext = 101, 123, 131 -errortext = 220, 50, 47 +helptext = #657b83 +fadedtext = #657b83 +errortext = #dc322f + +[Syntax] +background = #073642 +text = #fdf6e3 +link = #268bd2 +headertext = #93a1a1 +headertag = #2aa198 +emphasis = #268bd2 +dialog = #2aa198 +altdialog = #2aa198 +note = #65a19c +hidden = #93a1a1 +shortcode = #93a1a1 +keyword = #859900 +tag = #cb4b16 +value = #cb4b16 +optional = #93a1a1 +spellcheckline = #cb4b16 +errorline = #dc322f +replacetag = #859900 +modifier = #b58900 +texthighlight = #b5890060 diff --git a/novelwriter/assets/themes/solarized_light.conf b/novelwriter/assets/themes/solarized_light.conf index 06f4cb20..7d7427f5 100644 --- a/novelwriter/assets/themes/solarized_light.conf +++ b/novelwriter/assets/themes/solarized_light.conf @@ -1,48 +1,90 @@ [Main] name = Solarized Light +mode = light author = nullbasis credit = Ethan Schoonover url = https://ethanschoonover.com/solarized/ license = MIT licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE -[Icons] -default = 59, 68, 71 -faded = 78, 91, 95 -red = 220, 50, 47 -orange = 203, 75, 22 -yellow = 181, 137, 0 -green = 133, 153, 0 -aqua = 42, 161, 152 -blue = 38, 139, 210 -purple = 211, 54, 130 +## +# base03: #002b36 +# base02: #073642 +# base01: #586e75 +# base00: #657b83 +# base0: #839496 +# base1: #93a1a1 +# base2: #eee8d5 +# base3: #fdf6e3 +# yellow: #b58900 +# orange: #cb4b16 +# red: #dc322f +# magenta: #d33682 +# violet: #6c71c4 +# blue: #268bd2 +# cyan: #2aa198 +# green: #859900 +## + +[Base] +default = #3b4447 +faded = #4e5b5f +red = #dc322f +orange = #cb4b16 +yellow = #b58900 +green = #859900 +aqua = #2aa198 +blue = #268bd2 +purple = #6c71c4 [Project] -root = 42, 161, 152 -folder = 42, 161, 152 -file = 59, 68, 71 -title = 133, 153, 0 -chapter = 220, 50, 47 -scene = 38, 139, 210 -note = 181, 137, 0 +root = #2aa198 +folder = #2aa198 +file = #3b4447 +title = #859900 +chapter = #dc322f +scene = #268bd2 +note = #b58900 [Palette] -window = 238, 232, 213 -windowtext = 0, 43, 54 -base = 253, 246, 227 -alternatebase = 147, 161, 161 -text = 0, 43, 54 -tooltipbase = 133, 153, 0 -tooltiptext = 0, 43, 54 -button = 238, 232, 213 -buttontext = 0, 43, 54 -brighttext = 253, 246, 227 -highlight = 42, 161, 152 -highlightedtext = 253, 246, 227 -link = 38, 139, 210 -linkvisited = 38, 139, 210 +window = #eee8d5 +windowtext = #002b36 +base = #fdf6e3 +alternatebase = #93a1a1 +text = #002b36 +tooltipbase = #859900 +tooltiptext = #002b36 +button = #eee8d5 +buttontext = #002b36 +brighttext = #fdf6e3 +highlight = #2aa198 +highlightedtext = #fdf6e3 +link = #268bd2 +linkvisited = #268bd2 [GUI] -helptext = 78, 91, 95 -fadedtext = 78, 91, 95 -errortext = 220, 50, 47 +helptext = #4e5b5f +fadedtext = #4e5b5f +errortext = #dc322f + +[Syntax] +background = #fdf6e3 +text = #002b36 +link = #268bd2 +headertext = #586e75 +headertag = #2aa198 +emphasis = #268bd2 +dialog = #2aa198 +altdialog = #2aa198 +note = #1b6660 +hidden = #586e75 +shortcode = #586e75 +keyword = #859900 +tag = #cb4b16 +value = #cb4b16 +optional = #586e75 +spellcheckline = #cb4b16 +errorline = #dc322f +replacetag = #859900 +modifier = #b58900 +texthighlight = #b5890060 From 072d0f0acdd5f6f583f9b96d37556186e91fb94a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:40:58 +0200 Subject: [PATCH 04/20] Merge and update the default themes --- novelwriter/assets/syntax/default_dark.conf | 42 --------- novelwriter/assets/syntax/default_light.conf | 42 --------- novelwriter/assets/themes/default_dark.conf | 90 ++++++++++++-------- novelwriter/assets/themes/default_light.conf | 90 ++++++++++++-------- novelwriter/gui/theme.py | 2 +- 5 files changed, 113 insertions(+), 153 deletions(-) delete mode 100644 novelwriter/assets/syntax/default_dark.conf delete mode 100644 novelwriter/assets/syntax/default_light.conf diff --git a/novelwriter/assets/syntax/default_dark.conf b/novelwriter/assets/syntax/default_dark.conf deleted file mode 100644 index 8e467189..00000000 --- a/novelwriter/assets/syntax/default_dark.conf +++ /dev/null @@ -1,42 +0,0 @@ -[Main] -name = Default Dark -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -## -# Colours: -# Background = 2a2a2a : 42, 42, 42 -# Foreground = cccccc : 204, 204, 204 -# Comment = 969696 : 150, 150, 150 -# Red = e64250 : 230, 66, 80 -# Orange = f99157 : 249, 145, 87 -# Yellow = ffff84 : 255, 255, 132 -# Green = 99cc99 : 153, 204, 153 -# Blue = 288ed7 : 102, 153, 204 -# Purple = 8c56d7 : 140, 86, 215 -## - -[Syntax] -background = 42, 42, 42 -text = 204, 204, 204 -link = 102, 153, 204 -headertext = 153, 204, 153 -headertag = 153, 204, 153, 160 -emphasis = 249, 145, 87 -dialog = 102, 153, 204 -altdialog = 102, 153, 204 -note = 255, 255, 216 -hidden = 150, 150, 150 -shortcode = 153, 204, 153 -keyword = 230, 66, 80 -tag = 153, 204, 153 -value = 102, 153, 204 -optional = 153, 204, 153 -spellcheckline = 230, 66, 80 -errorline = 153, 204, 153 -replacetag = 153, 204, 153 -modifier = 153, 204, 153 -texthighlight = 255, 255, 132, 96 diff --git a/novelwriter/assets/syntax/default_light.conf b/novelwriter/assets/syntax/default_light.conf deleted file mode 100644 index 2e5ae404..00000000 --- a/novelwriter/assets/syntax/default_light.conf +++ /dev/null @@ -1,42 +0,0 @@ -[Main] -name = Default Light -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -## -# Colours: -# Background = ffffff : 255, 255, 255 -# Foreground = 000000 : 0, 0, 0 -# Comment = 646464 : 100, 100, 100 -# Red = c83232 : 200, 50, 50 -# Orange = ae6e1e : 174, 110, 30 -# Yellow = 969600 : 150, 150, 0 -# Green = 006400 : 0, 100, 0 -# Blue = 0000c8 : 0, 0, 200 -# Purple = 6400ae : 100, 0, 174 -## - -[Syntax] -background = 255, 255, 255 -text = 0, 0, 0 -link = 0, 0, 200 -headertext = 0, 100, 0 -headertag = 0, 100, 0, 160 -emphasis = 174, 110, 30 -dialog = 0, 0, 200 -altdialog = 0, 0, 200 -note = 100, 100, 0 -hidden = 100, 100, 100 -shortcode = 0, 100, 0 -keyword = 200, 50, 50 -tag = 0, 100, 0 -value = 0, 0, 200 -optional = 0, 100, 0 -spellcheckline = 200, 50, 50 -errorline = 0, 100, 0 -replacetag = 0, 100, 0 -modifier = 0, 100, 0 -texthighlight = 150, 150, 0, 96 diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 6ccf6ede..a8e6a3de 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -8,43 +8,65 @@ url = https://github.com/vkbo/novelWriter license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ -[Icons] -default = 184, 184, 184 -faded = 148, 148, 148 -red = 242, 119, 122 -orange = 249, 145, 57 -yellow = 255, 204, 102 -green = 153, 204, 153 -aqua = 102, 204, 204 -blue = 102, 153, 204 -purple = 204, 153, 204 +[Base] +default = #cccccc +faded = #949494 +red = #f2777a +orange = #f99139 +yellow = #ffcc66 +green = #99cc99 +aqua = #66cccc +blue = #6699cc +purple = #cc99cc [Project] -root = 102, 153, 204 -folder = 255, 204, 102 -file = 184, 184, 184 -title = 153, 204, 153 -chapter = 242, 119, 122 -scene = 102, 153, 204 -note = 255, 204, 102 +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow [Palette] -window = 54, 54, 54 -windowtext = 204, 204, 204 -base = 62, 62, 62 -alternatebase = 78, 78, 78 -text = 204, 204, 204 -tooltipbase = 255, 255, 192 -tooltiptext = 21, 21, 13 -button = 62, 62, 62 -buttontext = 204, 204, 204 -brighttext = 62, 62, 62 -highlight = 44, 152, 247 -highlightedtext = 255, 255, 255 -link = 102, 153, 204 -linkvisited = 102, 153, 204 +window = #363636 +windowtext = default +base = #3e3e3e +alternatebase = #4e4e4e +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #3e3e3e +buttontext = default +brighttext = #3e3e3e +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue [GUI] -helptext = 164, 164, 164 -fadedtext = 148, 148, 148 -errortext = 255, 164, 164 +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = #363636 +text = default +link = blue +headertext = green +headertag = green, 160 +emphasis = orange +dialog = blue +altdialog = blue +note = yellow +hidden = faded +shortcode = green +keyword = red +tag = green +value = blue +optional = green +spellcheckline = red +errorline = green +replacetag = green +modifier = green +texthighlight = yellow, 72 diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf index f236f404..c4496d13 100644 --- a/novelwriter/assets/themes/default_light.conf +++ b/novelwriter/assets/themes/default_light.conf @@ -8,43 +8,65 @@ url = https://github.com/vkbo/novelWriter license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ -[Icons] -default = 72, 72, 72 -faded = 108, 108, 108 -red = 240, 40, 41 -orange = 245, 135, 31 -yellow = 234, 183, 0 -green = 113, 140, 0 -aqua = 62, 153, 159 -blue = 66, 113, 174 -purple = 137, 89, 168 +[Base] +default = #303030 +faded = #6c6c6c +red = #a62a2d +orange = #b36829 +yellow = #a68542 +green = #296629 +aqua = #269999 +blue = #3a70a6 +purple = #b35ab3 [Project] -root = 66, 113, 174 -folder = 234, 183, 0 -file = 72, 72, 72 -title = 113, 140, 0 -chapter = 240, 40, 41 -scene = 66, 113, 174 -note = 234, 183, 0 +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow [Palette] -window = 239, 239, 239 -windowtext = 0, 0, 0 -base = 255, 255, 255 -alternatebase = 224, 224, 224 -text = 0, 0, 0 -tooltipbase = 255, 255, 220 -tooltiptext = 0, 0, 0 -button = 239, 239, 239 -buttontext = 0, 0, 0 -brighttext = 255, 255, 255 -highlight = 48, 135, 198 -highlightedtext = 255, 255, 255 -link = 66, 113, 174 -linkvisited = 66, 113, 174 +window = #efefef +windowtext = #000000 +base = #ffffff +alternatebase = #e0e0e0 +text = #000000 +tooltipbase = #ffffdc +tooltiptext = #000000 +button = #efefef +buttontext = #000000 +brighttext = #ffffff +highlight = #3087c6 +highlightedtext = #ffffff +link = blue +linkvisited = blue [GUI] -helptext = 92, 92, 92 -fadedtext = 108, 108, 108 -errortext = 255, 92, 92 +helptext = #5c5c5c +fadedtext = #6c6c6c +errortext = red + +[Syntax] +background = #ffffff +text = #000000 +link = blue +headertext = green +headertag = green, 160 +emphasis = orange +dialog = blue +altdialog = blue +note = yellow +hidden = faded +shortcode = green +keyword = red +tag = green +value = blue +optional = green +spellcheckline = red +errorline = green +replacetag = green +modifier = green +texthighlight = #c8c80060 diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 437531a3..4a53f453 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -244,7 +244,7 @@ class GuiTheme: entries = len(data) if entries == 2: # Assume name, alpha - color = self._qColors.get(data[0].strip(), default) + color = QColor(self._qColors.get(data[0].strip(), default)) color.setAlpha(checkInt(data[1], 255)) return color else: From 3258b8806f74ff89cd421bbea3be0f67350cdac4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:15:33 +0200 Subject: [PATCH 05/20] Merge and update the tomorrow themes --- novelwriter/assets/syntax/tomorrow.conf | 49 ---------- novelwriter/assets/syntax/tomorrow_night.conf | 49 ---------- .../assets/syntax/tomorrow_night_blue.conf | 49 ---------- .../assets/syntax/tomorrow_night_bright.conf | 49 ---------- .../syntax/tomorrow_night_eighties.conf | 49 ---------- novelwriter/assets/themes/default_dark.conf | 40 ++++---- novelwriter/assets/themes/tomorrow.conf | 91 +++++++++++++++++++ novelwriter/assets/themes/tomorrow_night.conf | 90 ++++++++++++++++++ .../assets/themes/tomorrow_night_blue.conf | 91 +++++++++++++++++++ .../assets/themes/tomorrow_night_bright.conf | 91 +++++++++++++++++++ .../themes/tomorrow_night_eighties.conf | 91 +++++++++++++++++++ 11 files changed, 474 insertions(+), 265 deletions(-) delete mode 100644 novelwriter/assets/syntax/tomorrow.conf delete mode 100644 novelwriter/assets/syntax/tomorrow_night.conf delete mode 100644 novelwriter/assets/syntax/tomorrow_night_blue.conf delete mode 100644 novelwriter/assets/syntax/tomorrow_night_bright.conf delete mode 100644 novelwriter/assets/syntax/tomorrow_night_eighties.conf create mode 100644 novelwriter/assets/themes/tomorrow.conf create mode 100644 novelwriter/assets/themes/tomorrow_night.conf create mode 100644 novelwriter/assets/themes/tomorrow_night_blue.conf create mode 100644 novelwriter/assets/themes/tomorrow_night_bright.conf create mode 100644 novelwriter/assets/themes/tomorrow_night_eighties.conf diff --git a/novelwriter/assets/syntax/tomorrow.conf b/novelwriter/assets/syntax/tomorrow.conf deleted file mode 100644 index d3d2b8e4..00000000 --- a/novelwriter/assets/syntax/tomorrow.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = ffffff : 255, 255, 255 -# Current Line = efefef : 239, 239, 239 -# Selection = d6d6d6 : 214, 214, 214 -# Foreground = 4d4d4c : 77, 77, 76 -# Comment = 8e908c : 142, 144, 140 -# Red = c82829 : 240, 40, 41 -# Orange = f5871f : 245, 135, 31 -# Yellow = eab700 : 234, 183, 0 -# Green = 718c00 : 113, 140, 0 -# Aqua = 3e999f : 62, 153, 159 -# Blue = 4271ae : 66, 113, 174 -# Purple = 8959a8 : 137, 89, 168 -## - -[Main] -name = Tomorrow -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 255, 255, 255 -text = 77, 77, 76 -link = 66, 113, 174 -headertext = 66, 113, 174 -headertag = 66, 113, 174, 160 -emphasis = 245, 135, 31 -dialog = 113, 140, 0 -altdialog = 234, 183, 0 -note = 115, 90, 0 -hidden = 142, 144, 140 -shortcode = 66, 113, 174 -keyword = 240, 40, 41 -tag = 137, 89, 168 -value = 234, 183, 0 -optional = 66, 113, 174 -spellcheckline = 240, 40, 41 -errorline = 113, 140, 0 -replacetag = 62, 153, 159 -modifier = 245, 135, 31 -texthighlight = 234, 183, 0, 96 diff --git a/novelwriter/assets/syntax/tomorrow_night.conf b/novelwriter/assets/syntax/tomorrow_night.conf deleted file mode 100644 index 15257945..00000000 --- a/novelwriter/assets/syntax/tomorrow_night.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 1d1f21 : 29, 31, 33 -# Current Line = 282a2e : 40, 42, 46 -# Selection = 373b41 : 55, 59. 65 -# Foreground = c5c8c6 : 197, 200, 198 -# Comment = 969896 : 150, 152, 150 -# Red = cc6666 : 204, 102, 102 -# Orange = de935f : 222, 147, 95 -# Yellow = f0c674 : 240, 198, 116 -# Green = b5bd68 : 181, 189, 104 -# Aqua = 8abeb7 : 138, 190, 183 -# Blue = 81a2be : 129, 162, 190 -# Purple = b294bb : 178, 148, 187 -## - -[Main] -name = Tomorrow Night -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 29, 31, 33 -text = 197, 200, 198 -link = 197, 200, 198, 160 -headertext = 129, 162, 190 -headertag = 94, 119, 139 -emphasis = 222, 147, 95 -dialog = 181, 189, 104 -altdialog = 240, 198, 116 -note = 240, 219, 178 -hidden = 150, 152, 150 -shortcode = 129, 162, 190 -keyword = 204, 102, 102 -tag = 178, 148, 187 -value = 240, 198, 116 -optional = 129, 162, 190 -spellcheckline = 204, 102, 102 -errorline = 181, 189, 104 -replacetag = 138, 190, 183 -modifier = 222, 147, 95 -texthighlight = 240, 198, 116, 96 diff --git a/novelwriter/assets/syntax/tomorrow_night_blue.conf b/novelwriter/assets/syntax/tomorrow_night_blue.conf deleted file mode 100644 index 89067e4f..00000000 --- a/novelwriter/assets/syntax/tomorrow_night_blue.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night Blue -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 002451 : 0, 36, 81 -# Current Line = 00346e : 0, 52, 110 -# Selection = 003f8e : 0, 63, 142 -# Foreground = ffffff : 255, 255, 255 -# Comment = 7285b7 : 114, 133, 183 -# Red = ff9da4 : 255, 157, 164 -# Orange = ffc58f : 255, 197, 143 -# Yellow = ffeead : 255, 238, 173 -# Green = d1f1a9 : 209, 241, 169 -# Aqua = 99ffff : 153, 255, 255 -# Blue = bbdaff : 187, 218, 255 -# Purple = ebbbff : 235, 187, 255 -## - -[Main] -name = Tomorrow Night Blue -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 0, 36, 81 -text = 255, 255, 255 -link = 187, 218, 255 -headertext = 187, 218, 255 -headertag = 187, 218, 255, 160 -emphasis = 255, 197, 143 -dialog = 209, 241, 169 -altdialog = 255, 238, 173 -note = 255, 247, 214 -hidden = 114, 133, 183 -shortcode = 187, 218, 255 -keyword = 255, 157, 164 -tag = 235, 187, 255 -value = 255, 238, 173 -optional = 187, 218, 255 -spellcheckline = 255, 157, 164 -errorline = 209, 241, 169 -replacetag = 153, 255, 255 -modifier = 255, 197, 143 -texthighlight = 255, 238, 173, 96 diff --git a/novelwriter/assets/syntax/tomorrow_night_bright.conf b/novelwriter/assets/syntax/tomorrow_night_bright.conf deleted file mode 100644 index 0f1fd055..00000000 --- a/novelwriter/assets/syntax/tomorrow_night_bright.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night Bright -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 000000 : 0, 0, 0 -# Current Line = 2a2a2a : 42, 42, 42 -# Selection = 424242 : 66, 66, 66 -# Foreground = eaeaea : 234, 234, 234 -# Comment = 969896 : 150, 152, 150 -# Red = d54e53 : 213, 78, 83 -# Orange = e78c45 : 231, 140, 69 -# Yellow = e7c547 : 231, 197, 71 -# Green = b9ca4a : 185, 202, 74 -# Aqua = 70c0b1 : 112, 192, 177 -# Blue = 7aa6da : 122, 166, 218 -# Purple = c397d8 : 195, 151, 216 -## - -[Main] -name = Tomorrow Night Bright -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 0, 0, 0 -text = 234, 234, 234 -link = 122, 166, 218 -headertext = 122, 166, 218 -headertag = 122, 166, 218, 160 -emphasis = 231, 140, 69 -dialog = 185, 202, 74 -altdialog = 231, 197, 71 -note = 231, 214, 150 -hidden = 150, 152, 150 -shortcode = 122, 166, 218 -keyword = 213, 78, 83 -tag = 195, 151, 216 -value = 231, 197, 71 -optional = 122, 166, 218 -spellcheckline = 213, 78, 83 -errorline = 185, 202, 74 -replacetag = 112, 192, 177 -modifier = 231, 140, 69 -texthighlight = 231, 197, 71, 128 diff --git a/novelwriter/assets/syntax/tomorrow_night_eighties.conf b/novelwriter/assets/syntax/tomorrow_night_eighties.conf deleted file mode 100644 index a638a871..00000000 --- a/novelwriter/assets/syntax/tomorrow_night_eighties.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night Eighties -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 2d2d2d : 45, 45, 45 -# Current Line = 393939 : 57, 57, 57 -# Selection = 515151 : 81, 81, 81 -# Foreground = cccccc : 204, 204, 204 -# Comment = 999999 : 153, 153, 153 -# Red = f2777a : 242, 119, 122 -# Orange = f99157 : 249, 145, 57 -# Yellow = ffcc66 : 255, 204, 102 -# Green = 99cc99 : 153, 204, 153 -# Aqua = 66cccc : 102, 204, 204 -# Blue = 6699cc : 102, 153, 204 -# Purple = cc99cc : 204, 153, 204 -## - -[Main] -name = Tomorrow Night Eighties -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 45, 45, 45 -text = 204, 204, 204 -link = 102, 153, 204 -headertext = 102, 153, 204 -headertag = 102, 153, 204, 160 -emphasis = 249, 145, 57 -dialog = 153, 204, 153 -altdialog = 255, 204, 102 -note = 255, 230, 179 -hidden = 153, 153, 153 -shortcode = 102, 153, 204 -keyword = 242, 119, 122 -tag = 204, 153, 204 -value = 255, 204, 102 -optional = 102, 153, 204 -spellcheckline = 242, 119, 122 -errorline = 153, 204, 153 -replacetag = 102, 204, 204 -modifier = 249, 145, 57 -texthighlight = 255, 204, 102, 96 diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index a8e6a3de..7b0c3922 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -50,23 +50,23 @@ fadedtext = #949494 errortext = red [Syntax] -background = #363636 -text = default -link = blue -headertext = green -headertag = green, 160 -emphasis = orange -dialog = blue -altdialog = blue -note = yellow -hidden = faded -shortcode = green -keyword = red -tag = green -value = blue -optional = green -spellcheckline = red -errorline = green -replacetag = green -modifier = green -texthighlight = yellow, 72 +background = #363636 +text = default +link = blue +headertext = green +headertag = green, 160 +emphasis = orange +dialog = blue +altdialog = blue +note = yellow +hidden = faded +shortcode = green +keyword = red +tag = green +value = blue +optional = green +spellcheckline = red +errorline = green +replacetag = green +modifier = green +texthighlight = yellow, 72 diff --git a/novelwriter/assets/themes/tomorrow.conf b/novelwriter/assets/themes/tomorrow.conf new file mode 100644 index 00000000..ab49754b --- /dev/null +++ b/novelwriter/assets/themes/tomorrow.conf @@ -0,0 +1,91 @@ +[Main] +name = Tomorrow +mode = light +author = Veronica Berglyd Olsen (adaptation) +credit = Chris Kempson (color theme) +url = https://github.com/chriskempson/tomorrow-theme +license = MIT License +licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md + +## +# Theme: Tomorrow +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #ffffff +# Current Line = #efefef +# Selection = #d6d6d6 +# Foreground = #4d4d4c +# Comment = #8e908c +# Red = #c82829 +# Orange = #f5871f +# Yellow = #eab700 +# Green = #718c00 +# Aqua = #3e999f +# Blue = #4271ae +# Purple = #8959a8 +## + +[Base] +default = #484848 +faded = #6c6c6c +red = #c82829 +orange = #f5871f +yellow = #eab700 +green = #718c00 +aqua = #3e999f +blue = #4271ae +purple = #8959a8 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #efefef +windowtext = #000000 +base = #ffffff +alternatebase = #d6d6d6 +text = #000000 +tooltipbase = #ffffdc +tooltiptext = #000000 +button = #ffffff +buttontext = #000000 +brighttext = #ffffff +highlight = blue +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #5c5c5c +fadedtext = faded +errortext = red + +[Syntax] +background = #ffffff +text = #4d4d4c +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #b38c00 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = orange +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tomorrow_night.conf b/novelwriter/assets/themes/tomorrow_night.conf new file mode 100644 index 00000000..16290bf0 --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night.conf @@ -0,0 +1,90 @@ +[Main] +name = Tomorrow Night +mode = dark +author = Veronica Berglyd Olsen (adaptation) +credit = Chris Kempson (color theme) +url = https://github.com/chriskempson/tomorrow-theme +license = MIT License +licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md +## +# Theme: Tomorrow Night +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #1d1f21 +# Current Line = #282a2e +# Selection = #373b41 +# Foreground = #c5c8c6 +# Comment = #969896 +# Red = #cc6666 +# Orange = #de935f +# Yellow = #f0c674 +# Green = #b5bd68 +# Aqua = #8abeb7 +# Blue = #81a2be +# Purple = #b294bb +## + +[Base] +default = #c5c8c6 +faded = #969896 +red = #cc6666 +orange = #de935f +yellow = #f0c674 +green = #b5bd68 +aqua = #8abeb7 +blue = #81a2be +purple = #b294bb + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #282a2e +windowtext = default +base = #1d1f21 +alternatebase = #373b41 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #1d1f21 +buttontext = default +brighttext = #1d1f21 +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = #1d1f21 +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #f0dbb2 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = orange +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tomorrow_night_blue.conf b/novelwriter/assets/themes/tomorrow_night_blue.conf new file mode 100644 index 00000000..93b6592e --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night_blue.conf @@ -0,0 +1,91 @@ +[Main] +name = Tomorrow Night Blue +mode = dark +author = Veronica Berglyd Olsen (adaptation) +credit = Chris Kempson (color theme) +url = https://github.com/chriskempson/tomorrow-theme +license = MIT License +licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md + +## +# Theme: Tomorrow Night Blue +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #002451 +# Current Line = #00346e +# Selection = #003f8e +# Foreground = #ffffff +# Comment = #7285b7 +# Red = #ff9da4 +# Orange = #ffc58f +# Yellow = #ffeead +# Green = #d1f1a9 +# Aqua = #99ffff +# Blue = #bbdaff +# Purple = #ebbbff +## + +[Base] +default = #ffffff +faded = #7285b7 +red = #ff9da4 +orange = #ffc58f +yellow = #ffeead +green = #d1f1a9 +aqua = #99ffff +blue = #bbdaff +purple = #ebbbff + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #00346e +windowtext = default +base = #002451 +alternatebase = #003f8e +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #002451 +buttontext = default +brighttext = #002451 +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = #002451 +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #fff4cc +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = orange +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tomorrow_night_bright.conf b/novelwriter/assets/themes/tomorrow_night_bright.conf new file mode 100644 index 00000000..de76e694 --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night_bright.conf @@ -0,0 +1,91 @@ +[Main] +name = Tomorrow Night Bright +mode = dark +author = Veronica Berglyd Olsen (adaptation) +credit = Chris Kempson (color theme) +url = https://github.com/chriskempson/tomorrow-theme +license = MIT License +licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md + +## +# Theme: Tomorrow Night Bright +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #000000 +# Current Line = #2a2a2a +# Selection = #424242 +# Foreground = #eaeaea +# Comment = #969896 +# Red = #d54e53 +# Orange = #e78c45 +# Yellow = #e7c547 +# Green = #b9ca4a +# Aqua = #70c0b1 +# Blue = #7aa6da +# Purple = #c397d8 +## + +[Base] +default = #eaeaea +faded = #969896 +red = #d54e53 +orange = #e78c45 +yellow = #e7c547 +green = #b9ca4a +aqua = #70c0b1 +blue = #7aa6da +purple = #c397d8 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #2a2a2a +windowtext = default +base = #000000 +alternatebase = #424242 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #000000 +buttontext = default +brighttext = #000000 +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = #000000 +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #e7d696 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = orange +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tomorrow_night_eighties.conf b/novelwriter/assets/themes/tomorrow_night_eighties.conf new file mode 100644 index 00000000..e3e8bbec --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night_eighties.conf @@ -0,0 +1,91 @@ +[Main] +name = Tomorrow Night Eighties +mode = dark +author = Veronica Berglyd Olsen (adaptation) +credit = Chris Kempson (color theme) +url = https://github.com/chriskempson/tomorrow-theme +license = MIT License +licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md + +## +# Theme: Tomorrow Night Eighties +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #2d2d2d +# Current Line = #393939 +# Selection = #515151 +# Foreground = #cccccc +# Comment = #999999 +# Red = #f2777a +# Orange = #f99157 +# Yellow = #ffcc66 +# Green = #99cc99 +# Aqua = #66cccc +# Blue = #6699cc +# Purple = #cc99cc +## + +[Base] +default = #cccccc +faded = #999999 +red = #f2777a +orange = #f99157 +yellow = #ffcc66 +green = #99cc99 +aqua = #66cccc +blue = #6699cc +purple = #cc99cc + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #393939 +windowtext = default +base = #2d2d2d +alternatebase = #515151 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #2d2d2d +buttontext = default +brighttext = #2d2d2d +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = #2d2d2d +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #ffe6b3 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = orange +texthighlight = yellow, 96 From eed5cd187738ce43a30a6e0b2b73b711b68deb41 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:02:00 +0200 Subject: [PATCH 06/20] Merge and update the owl and tango themes --- novelwriter/assets/syntax/light_owl.conf | 49 ------------ novelwriter/assets/syntax/night_owl.conf | 49 ------------ novelwriter/assets/syntax/tango.conf | 39 ---------- novelwriter/assets/themes/light_owl.conf | 91 ++++++++++++++++++++++ novelwriter/assets/themes/night_owl.conf | 91 ++++++++++++++++++++++ novelwriter/assets/themes/tango_dark.conf | 81 +++++++++++++++++++ novelwriter/assets/themes/tango_light.conf | 81 +++++++++++++++++++ 7 files changed, 344 insertions(+), 137 deletions(-) delete mode 100644 novelwriter/assets/syntax/light_owl.conf delete mode 100644 novelwriter/assets/syntax/night_owl.conf delete mode 100644 novelwriter/assets/syntax/tango.conf create mode 100644 novelwriter/assets/themes/light_owl.conf create mode 100644 novelwriter/assets/themes/night_owl.conf create mode 100644 novelwriter/assets/themes/tango_dark.conf create mode 100644 novelwriter/assets/themes/tango_light.conf diff --git a/novelwriter/assets/syntax/light_owl.conf b/novelwriter/assets/syntax/light_owl.conf deleted file mode 100644 index ce65e704..00000000 --- a/novelwriter/assets/syntax/light_owl.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Light Owl -# Source: https://github.com/sdras/night-owl-vscode-theme -# Credit: Sarah Drasner -## -# Colours: -# Background = fbfbfb : 251, 251, 251 -# Current Line = e0e0e0 : 224, 224, 224 -# Selection = e0e0e0 : 224, 224, 224 -# Foreground = 403f52 : 64, 63, 82 -# Comment = 989fb1 : 152, 159, 177 -# Red = de3d3a : 222, 61, 58 -# Orange = e0af05 : 224, 175, 5 -# Yellow = daaa01 : 218, 170, 1 -# Green = 08916a : 8, 145, 106 -# Aqua = 2aa298 : 42, 162, 152 -# Blue = 288ed7 : 40, 142, 215 -# Purple = 964ac1 : 150, 74, 193 -## - -[Main] -name = Light Owl -author = Veronica Berglyd Olsen (adaptation) -credit = Sarah Drasner (color theme) -url = https://github.com/sdras/night-owl-vscode-theme -license = MIT License -licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE - -[Syntax] -background = 251, 251, 251 -text = 64, 63, 82 -link = 40, 142, 215 -headertext = 40, 142, 215 -headertag = 40, 142, 215, 160 -emphasis = 224, 175, 5 -dialog = 8, 145, 106 -altdialog = 218, 170, 1 -note = 128, 118, 75 -hidden = 152, 159, 177 -shortcode = 40, 142, 215 -keyword = 222, 61, 58 -tag = 150, 74, 193 -value = 224, 175, 5 -optional = 40, 142, 215 -spellcheckline = 222, 61, 58 -errorline = 8, 145, 106 -replacetag = 42, 162, 152 -modifier = 8, 145, 106 -texthighlight = 218, 170, 1, 96 diff --git a/novelwriter/assets/syntax/night_owl.conf b/novelwriter/assets/syntax/night_owl.conf deleted file mode 100644 index 337cdfb8..00000000 --- a/novelwriter/assets/syntax/night_owl.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Night Owl -# Source: https://github.com/sdras/night-owl-vscode-theme -# Credit: Sarah Drasner -## -# Colours: -# Background = 011627 : 1, 22, 39 -# Current Line = 32374d : 50, 55, 77 -# Selection = 32374d : 50, 55, 77 -# Foreground = d6deeb : 214, 222, 235 -# Comment = 637777 : 99, 119, 119 -# Red = f78c6c : 247, 140, 108 -# Orange = ecc48d : 236, 196, 141 -# Yellow = ffeb95 : 255, 235, 149 -# Green = addb67 : 173, 219, 103 -# Aqua = 7fdbca : 127, 219, 202 -# Blue = 82aaff : 130, 170, 255 -# Purple = c792ea : 199, 146, 234 -## - -[Main] -name = Night Owl -author = Veronica Berglyd Olsen (adaptation) -credit = Sarah Drasner (color theme) -url = https://github.com/sdras/night-owl-vscode-theme -license = MIT License -licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE - -[Syntax] -background = 1, 22, 39 -text = 214, 222, 235 -link = 130, 170, 255 -headertext = 130, 170, 255 -headertag = 130, 170, 255, 160 -emphasis = 236, 196, 141 -dialog = 173, 219, 103 -altdialog = 255, 235, 149 -note = 255, 249, 202 -hidden = 99, 119, 119 -shortcode = 130, 170, 255 -keyword = 247, 140, 108 -tag = 199, 146, 234 -value = 255, 235, 149 -optional = 130, 170, 255 -spellcheckline = 247, 140, 108 -errorline = 173, 219, 103 -replacetag = 127, 219, 202 -modifier = 173, 219, 103 -texthighlight = 255, 235, 149, 96 diff --git a/novelwriter/assets/syntax/tango.conf b/novelwriter/assets/syntax/tango.conf deleted file mode 100644 index 7c5e9daa..00000000 --- a/novelwriter/assets/syntax/tango.conf +++ /dev/null @@ -1,39 +0,0 @@ -[Main] -name = Tango -author = Veronica Berglyd Olsen (adaptation) - -## -# Colours: -# Background = 30302f : 48, 48, 47 -# Foreground = eeeeec : 238, 238, 236 -# Comment = 8f8f8d : 143, 143, 141 -# Red = ef2929 : 239, 41, 41 -# Orange = c4a000 : 196, 160, 0 -# Yellow = fce94f : 252, 233, 79 -# Green = 8ae234 : 138, 226, 52 -# Cyan = 34e2e2 : 52, 226, 226 -# Blue = 3465a4 : 114, 159, 207 -# Purple = ad7fa8 : 173, 127, 168 -## - -[Syntax] -background = 48, 48, 47 -text = 238, 238, 236 -link = 114, 159, 207 -headertext = 114, 159, 207 -headertag = 114, 159, 207, 160 -emphasis = 196, 160, 0 -dialog = 138, 226, 52 -altdialog = 252, 233, 79 -note = 252, 242, 164 -hidden = 143, 143, 141 -shortcode = 114, 159, 207 -keyword = 239, 41, 41 -tag = 173, 127, 168 -value = 239, 41, 41 -optional = 114, 159, 207 -spellcheckline = 239, 41, 41 -errorline = 138, 226, 52 -replacetag = 52, 226, 226 -modifier = 114, 159, 207 -texthighlight = 252, 233, 79, 96 diff --git a/novelwriter/assets/themes/light_owl.conf b/novelwriter/assets/themes/light_owl.conf new file mode 100644 index 00000000..d91cf0ba --- /dev/null +++ b/novelwriter/assets/themes/light_owl.conf @@ -0,0 +1,91 @@ +[Main] +name = Light Owl +mode = light +author = Veronica Berglyd Olsen (adaptation) +credit = Sarah Drasner (color theme) +url = https://github.com/sdras/night-owl-vscode-theme +license = MIT License +licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE + +## +# Theme: Light Owl +# Source: https://github.com/sdras/night-owl-vscode-theme +# Credit: Sarah Drasner +## +# Colours: +# Background = #fbfbfb +# Current Line = #e0e0e0 +# Selection = #e0e0e0 +# Foreground = #403f52 +# Comment = #989fb1 +# Red = #de3d3a +# Orange = #e0af05 +# Yellow = #daaa01 +# Green = #08916a +# Aqua = #2aa298 +# Blue = #288ed7 +# Purple = #964ac1 +## + +[Base] +default = #403f52 +faded = #989fb1 +red = #de3d3a +orange = #e0af05 +yellow = #daaa01 +green = #08916a +aqua = #2aa298 +blue = #288ed7 +purple = #964ac1 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #e0e0e0 +windowtext = default +base = #fbfbfb +alternatebase = #e0e0e0 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #e0e0e0 +buttontext = default +brighttext = #e0e0e0 +highlight = #3087c6 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = #fbfbfb +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #80764b +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = orange +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = green +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/night_owl.conf b/novelwriter/assets/themes/night_owl.conf new file mode 100644 index 00000000..a5f0a02b --- /dev/null +++ b/novelwriter/assets/themes/night_owl.conf @@ -0,0 +1,91 @@ +[Main] +name = Night Owl +mode = dark +author = Veronica Berglyd Olsen (adaptation) +credit = Sarah Drasner (color theme) +url = https://github.com/sdras/night-owl-vscode-theme +license = MIT License +licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE + +## +# Theme: Night Owl +# Source: https://github.com/sdras/night-owl-vscode-theme +# Credit: Sarah Drasner +## +# Colours: +# Background = #011627 +# Current Line = #32374d +# Selection = #32374d +# Foreground = #d6deeb +# Comment = #637777 +# Red = #f78c6c +# Orange = #ecc48d +# Yellow = #ffeb95 +# Green = #addb67 +# Aqua = #7fdbca +# Blue = #82aaff +# Purple = #c792ea +## + +[Base] +default = #d6deeb +faded = #637777 +red = #f78c6c +orange = #ecc48d +yellow = #addb67 +green = #addb67 +aqua = #7fdbca +blue = #82aaff +purple = #c792ea + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #32374d +windowtext = default +base = #011627 +alternatebase = #32374d +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #32374d +buttontext = default +brighttext = #32374d +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = #011627 +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #fff9ca +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = orange +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = green +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tango_dark.conf b/novelwriter/assets/themes/tango_dark.conf new file mode 100644 index 00000000..873e7f11 --- /dev/null +++ b/novelwriter/assets/themes/tango_dark.conf @@ -0,0 +1,81 @@ +[Main] +name = Tango Dark +mode = dark +author = Veronica Berglyd Olsen (adaptation) + +## +# Colours: +# Background = 30302f : 48, 48, 47 +# Foreground = eeeeec : 238, 238, 236 +# Comment = 8f8f8d : 143, 143, 141 +# Red = ef2929 : 239, 41, 41 +# Orange = c4a000 : 196, 160, 0 +# Yellow = fce94f : 252, 233, 79 +# Green = 8ae234 : 138, 226, 52 +# Cyan = 34e2e2 : 52, 226, 226 +# Blue = 3465a4 : 114, 159, 207 +# Purple = ad7fa8 : 173, 127, 168 +## + +[Base] +default = #eeeeec +faded = #babdb6 +red = #ef2929 +orange = #fcaf3e +yellow = #fce94f +green = #8ae234 +aqua = #34e2e2 +blue = #729fcf +purple = #ad7fa8 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #555753 +windowtext = default +base = #2e3436 +alternatebase = #888a85 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #555753 +buttontext = default +brighttext = #555753 +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #e9b96e +fadedtext = faded +errortext = red + +[Syntax] +background = #2e3436 +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #fcf2a4 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = red +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tango_light.conf b/novelwriter/assets/themes/tango_light.conf new file mode 100644 index 00000000..6ba5566a --- /dev/null +++ b/novelwriter/assets/themes/tango_light.conf @@ -0,0 +1,81 @@ +[Main] +name = Tango Light +mode = light +author = Veronica Berglyd Olsen (adaptation) + +## +# Colours: +# Background = #eeeeec +# Foreground = #2e3436 +# Comment = 8f8f8d : 143, 143, 141 +# Red = #a40000 +# Orange = c4a000 : 196, 160, 0 +# Yellow = fce94f : 252, 233, 79 +# Green = 8ae234 : 138, 226, 52 +# Cyan = 34e2e2 : 52, 226, 226 +# Blue = 3465a4 : 114, 159, 207 +# Purple = ad7fa8 : 173, 127, 168 +## + +[Base] +default = #2e3436 +faded = #888a85 +red = #a40000 +orange = #ce5c00 +yellow = #c4a000 +green = #4e9a06 +aqua = #069a9a +blue = #204a87 +purple = #5c3566 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #d3d7cf +windowtext = default +base = #eeeeec +alternatebase = #e0e0e0 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #d3d7cf +buttontext = default +brighttext = #d3d7cf +highlight = #3087c6 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #8f5902 +fadedtext = faded +errortext = red + +[Syntax] +background = #eeeeec +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #a78800 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = red +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow, 96 From d0f11a25956b94717c42f1eae67e7b6e1b7eaea9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:05:52 +0200 Subject: [PATCH 07/20] Fix a few inconsistencies --- novelwriter/assets/themes/default.conf | 3 -- novelwriter/assets/themes/default_light.conf | 44 +++++++++---------- novelwriter/assets/themes/light_owl.conf | 40 ++++++++--------- novelwriter/assets/themes/night_owl.conf | 40 ++++++++--------- novelwriter/assets/themes/tango_dark.conf | 40 ++++++++--------- novelwriter/assets/themes/tango_light.conf | 40 ++++++++--------- novelwriter/assets/themes/tomorrow.conf | 8 ++-- novelwriter/assets/themes/tomorrow_night.conf | 1 + 8 files changed, 107 insertions(+), 109 deletions(-) delete mode 100644 novelwriter/assets/themes/default.conf diff --git a/novelwriter/assets/themes/default.conf b/novelwriter/assets/themes/default.conf deleted file mode 100644 index 9cd9cd5b..00000000 --- a/novelwriter/assets/themes/default.conf +++ /dev/null @@ -1,3 +0,0 @@ -[Main] -name = Qt Default Theme -description = Qt standard colours diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf index c4496d13..0c309a3c 100644 --- a/novelwriter/assets/themes/default_light.conf +++ b/novelwriter/assets/themes/default_light.conf @@ -34,8 +34,8 @@ windowtext = #000000 base = #ffffff alternatebase = #e0e0e0 text = #000000 -tooltipbase = #ffffdc -tooltiptext = #000000 +tooltipbase = #ffffc0 +tooltiptext = #15150d button = #efefef buttontext = #000000 brighttext = #ffffff @@ -50,23 +50,23 @@ fadedtext = #6c6c6c errortext = red [Syntax] -background = #ffffff -text = #000000 -link = blue -headertext = green -headertag = green, 160 -emphasis = orange -dialog = blue -altdialog = blue -note = yellow -hidden = faded -shortcode = green -keyword = red -tag = green -value = blue -optional = green -spellcheckline = red -errorline = green -replacetag = green -modifier = green -texthighlight = #c8c80060 +background = #ffffff +text = #000000 +link = blue +headertext = green +headertag = green, 160 +emphasis = orange +dialog = blue +altdialog = blue +note = yellow +hidden = faded +shortcode = green +keyword = red +tag = green +value = blue +optional = green +spellcheckline = red +errorline = green +replacetag = green +modifier = green +texthighlight = #c8c80060 diff --git a/novelwriter/assets/themes/light_owl.conf b/novelwriter/assets/themes/light_owl.conf index d91cf0ba..f72b8d70 100644 --- a/novelwriter/assets/themes/light_owl.conf +++ b/novelwriter/assets/themes/light_owl.conf @@ -69,23 +69,23 @@ fadedtext = faded errortext = red [Syntax] -background = #fbfbfb -text = default -link = blue -headertext = blue -headertag = blue, 160 -emphasis = orange -dialog = green -altdialog = yellow -note = #80764b -hidden = faded -shortcode = blue -keyword = red -tag = purple -value = orange -optional = blue -spellcheckline = red -errorline = green -replacetag = aqua -modifier = green -texthighlight = yellow, 96 +background = #fbfbfb +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #80764b +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = orange +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = green +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/night_owl.conf b/novelwriter/assets/themes/night_owl.conf index a5f0a02b..0d176c45 100644 --- a/novelwriter/assets/themes/night_owl.conf +++ b/novelwriter/assets/themes/night_owl.conf @@ -69,23 +69,23 @@ fadedtext = faded errortext = red [Syntax] -background = #011627 -text = default -link = blue -headertext = blue -headertag = blue, 160 -emphasis = orange -dialog = green -altdialog = yellow -note = #fff9ca -hidden = faded -shortcode = blue -keyword = red -tag = purple -value = orange -optional = blue -spellcheckline = red -errorline = green -replacetag = aqua -modifier = green -texthighlight = yellow, 96 +background = #011627 +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #fff9ca +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = orange +optional = blue +spellcheckline = red +errorline = green +replacetag = aqua +modifier = green +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tango_dark.conf b/novelwriter/assets/themes/tango_dark.conf index 873e7f11..ca0ba841 100644 --- a/novelwriter/assets/themes/tango_dark.conf +++ b/novelwriter/assets/themes/tango_dark.conf @@ -59,23 +59,23 @@ fadedtext = faded errortext = red [Syntax] -background = #2e3436 -text = default -link = blue -headertext = blue -headertag = blue, 160 -emphasis = orange -dialog = green -altdialog = yellow -note = #fcf2a4 -hidden = faded -shortcode = blue -keyword = red -tag = purple -value = red -optional = blue -spellcheckline = red -errorline = green -replacetag = cyan -modifier = blue -texthighlight = yellow, 96 +background = #2e3436 +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #fcf2a4 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = red +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tango_light.conf b/novelwriter/assets/themes/tango_light.conf index 6ba5566a..174b0571 100644 --- a/novelwriter/assets/themes/tango_light.conf +++ b/novelwriter/assets/themes/tango_light.conf @@ -59,23 +59,23 @@ fadedtext = faded errortext = red [Syntax] -background = #eeeeec -text = default -link = blue -headertext = blue -headertag = blue, 160 -emphasis = orange -dialog = green -altdialog = yellow -note = #a78800 -hidden = faded -shortcode = blue -keyword = red -tag = purple -value = red -optional = blue -spellcheckline = red -errorline = green -replacetag = cyan -modifier = blue -texthighlight = yellow, 96 +background = #eeeeec +text = default +link = blue +headertext = blue +headertag = blue, 160 +emphasis = orange +dialog = green +altdialog = yellow +note = #a78800 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = red +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow, 96 diff --git a/novelwriter/assets/themes/tomorrow.conf b/novelwriter/assets/themes/tomorrow.conf index ab49754b..2b734209 100644 --- a/novelwriter/assets/themes/tomorrow.conf +++ b/novelwriter/assets/themes/tomorrow.conf @@ -53,11 +53,11 @@ windowtext = #000000 base = #ffffff alternatebase = #d6d6d6 text = #000000 -tooltipbase = #ffffdc -tooltiptext = #000000 -button = #ffffff +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #efefef buttontext = #000000 -brighttext = #ffffff +brighttext = #efefef highlight = blue highlightedtext = #ffffff link = blue diff --git a/novelwriter/assets/themes/tomorrow_night.conf b/novelwriter/assets/themes/tomorrow_night.conf index 16290bf0..27abd454 100644 --- a/novelwriter/assets/themes/tomorrow_night.conf +++ b/novelwriter/assets/themes/tomorrow_night.conf @@ -6,6 +6,7 @@ credit = Chris Kempson (color theme) url = https://github.com/chriskempson/tomorrow-theme license = MIT License licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md + ## # Theme: Tomorrow Night # Source: https://github.com/chriskempson/tomorrow-theme From 55e601cf9d04689682ccce645217af3ef20c00d3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:18:19 +0200 Subject: [PATCH 08/20] Merge and update the grey themes --- novelwriter/assets/syntax/grey_dark.conf | 29 --------- novelwriter/assets/syntax/grey_light.conf | 29 --------- novelwriter/assets/themes/default_dark.conf | 2 +- novelwriter/assets/themes/grey_dark.conf | 71 +++++++++++++++++++++ novelwriter/assets/themes/grey_light.conf | 71 +++++++++++++++++++++ 5 files changed, 143 insertions(+), 59 deletions(-) delete mode 100644 novelwriter/assets/syntax/grey_dark.conf delete mode 100644 novelwriter/assets/syntax/grey_light.conf create mode 100644 novelwriter/assets/themes/grey_dark.conf create mode 100644 novelwriter/assets/themes/grey_light.conf diff --git a/novelwriter/assets/syntax/grey_dark.conf b/novelwriter/assets/syntax/grey_dark.conf deleted file mode 100644 index cc36478c..00000000 --- a/novelwriter/assets/syntax/grey_dark.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Grey Dark -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -[Syntax] -background = 54, 54, 54 -text = 200, 200, 200 -link = 200, 200, 200 -headertext = 225, 225, 225 -headertag = 225, 225, 225, 160 -emphasis = 200, 200, 200 -dialog = 200, 200, 200 -altdialog = 200, 200, 200 -note = 200, 200, 200 -hidden = 150, 150, 150 -shortcode = 225, 225, 225 -keyword = 225, 225, 225 -tag = 200, 200, 200 -value = 200, 200, 200 -optional = 225, 225, 225 -spellcheckline = 200, 46, 0 -errorline = 46, 200, 0 -replacetag = 225, 225, 225 -modifier = 225, 225, 225 -texthighlight = 255, 255, 255, 64 diff --git a/novelwriter/assets/syntax/grey_light.conf b/novelwriter/assets/syntax/grey_light.conf deleted file mode 100644 index 80a8a01c..00000000 --- a/novelwriter/assets/syntax/grey_light.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Grey Light -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -[Syntax] -background = 255, 255, 255 -text = 20, 20, 20 -link = 20, 20, 20 -headertext = 0, 0, 0 -headertag = 0, 0, 0, 160 -emphasis = 20, 20, 20 -dialog = 20, 20, 20 -altdialog = 20, 20, 20 -note = 20, 20, 20 -hidden = 100, 100, 100 -shortcode = 0, 0, 0 -keyword = 0, 0, 0 -tag = 20, 20, 20 -value = 20, 20, 20 -optional = 0, 0, 0 -spellcheckline = 200, 0, 0 -errorline = 0, 150, 0 -replacetag = 0, 0, 0 -modifier = 0, 0, 0 -texthighlight = 0, 0, 0, 64 diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 7b0c3922..35acdb4c 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -46,7 +46,7 @@ linkvisited = blue [GUI] helptext = #a4a4a4 -fadedtext = #949494 +fadedtext = faded errortext = red [Syntax] diff --git a/novelwriter/assets/themes/grey_dark.conf b/novelwriter/assets/themes/grey_dark.conf new file mode 100644 index 00000000..d8c4a1fd --- /dev/null +++ b/novelwriter/assets/themes/grey_dark.conf @@ -0,0 +1,71 @@ +[Main] +name = Grey Dark +mode = dark +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter +license = CC BY-SA 4.0 +licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ + +[Base] +default = #cccccc +faded = #949494 +red = #f2777a +orange = #f99139 +yellow = #ffcc66 +green = #99cc99 +aqua = #66cccc +blue = #6699cc +purple = #cc99cc + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #363636 +windowtext = default +base = #3e3e3e +alternatebase = #4e4e4e +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #3e3e3e +buttontext = default +brighttext = #3e3e3e +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #a4a4a4 +fadedtext = faded +errortext = red + +[Syntax] +background = #363636 +text = #c8c8c8 +link = #c8c8c8 +headertext = #e1e1e1 +headertag = #e1e1e1a0 +emphasis = #c8c8c8 +dialog = #c8c8c8 +altdialog = #c8c8c8 +note = #c8c8c8 +hidden = faded +shortcode = #e1e1e1 +keyword = #e1e1e1 +tag = #c8c8c8 +value = #c8c8c8 +optional = #e1e1e1 +spellcheckline = red +errorline = green +replacetag = #e1e1e1 +modifier = #e1e1e1 +texthighlight = #ffffff40 diff --git a/novelwriter/assets/themes/grey_light.conf b/novelwriter/assets/themes/grey_light.conf new file mode 100644 index 00000000..ffa8fe61 --- /dev/null +++ b/novelwriter/assets/themes/grey_light.conf @@ -0,0 +1,71 @@ +[Main] +name = Grey Light +mode = light +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter +license = CC BY-SA 4.0 +licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ + +[Base] +default = #303030 +faded = #6c6c6c +red = #a62a2d +orange = #b36829 +yellow = #a68542 +green = #296629 +aqua = #269999 +blue = #3a70a6 +purple = #b35ab3 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow + +[Palette] +window = #efefef +windowtext = default +base = #ffffff +alternatebase = #e0e0e0 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #efefef +buttontext = default +brighttext = #ffffff +highlight = #3087c6 +highlightedtext = #ffffff +link = blue +linkvisited = blue + +[GUI] +helptext = #5c5c5c +fadedtext = faded +errortext = red + +[Syntax] +background = #ffffff +text = #141414 +link = #141414 +headertext = #000000 +headertag = #000000a0 +emphasis = #141414 +dialog = #141414 +altdialog = #141414 +note = #141414 +hidden = faded +shortcode = #000000 +keyword = #000000 +tag = #141414 +value = #141414 +optional = #000000 +spellcheckline = red +errorline = green +replacetag = #000000 +modifier = #000000 +texthighlight = #00000040 From 10475f32fbee62724b3d65832700f295f23968a8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:21:35 +0200 Subject: [PATCH 09/20] Remove references to syntax themes --- CREDITS.md | 4 ++-- novelwriter/assets/text/credits_en.htm | 4 ++-- novelwriter/config.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 9ac3b34a..b3c527a5 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -62,8 +62,8 @@ Some of the assets bundled with novelWriter were adapted from the following sour * **Material Symbols** icons by Google Inc (Apache 2.0) * **Remix** icons by RemixIcon (Apache 2.0) * **Font Awesome** icons by Fonticons Inc (CC BY 4.0) -* **Tomorrow** syntax themes by Chris Kempson (MIT License) -* **Owl** syntax themes by Sarah Drasner (MIT License) +* **Tomorrow** themes by Chris Kempson (MIT License) +* **Owl** themes by Sarah Drasner (MIT License) * **Solarized** themes by Ethan Schoonover (MIT License) * **Cyberpunk Night** theme by Anders Lemvigh (CC BY-SA 4.0) * **Dracula** theme by Zeno Rocha (MIT License) diff --git a/novelwriter/assets/text/credits_en.htm b/novelwriter/assets/text/credits_en.htm index 88b3887d..33903616 100644 --- a/novelwriter/assets/text/credits_en.htm +++ b/novelwriter/assets/text/credits_en.htm @@ -75,8 +75,8 @@ more contributions are listed on the project's Members page.

  • Material Symbols icons by Google Inc (Apache 2.0)
  • Remix icons by RemixIcon (Apache 2.0)
  • Font Awesome icons by Fonticons Inc (CC BY 4.0)
  • -
  • Tomorrow syntax themes by Chris Kempson (MIT License)
  • -
  • Owl syntax themes by Sarah Drasner (MIT License)
  • +
  • Tomorrow themes by Chris Kempson (MIT License)
  • +
  • Owl themes by Sarah Drasner (MIT License)
  • Solarized themes by Ethan Schoonover (MIT License)
  • Cyberpunk Night theme by Anders Lemvigh (CC BY-SA 4.0)
  • Dracula theme by Zeno Rocha (MIT License)
  • diff --git a/novelwriter/config.py b/novelwriter/config.py index 9b8752fc..db173019 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -553,11 +553,10 @@ class Config: self._confPath.mkdir(exist_ok=True) self._dataPath.mkdir(exist_ok=True) - # Also create the syntax, themes and icons folders if possible + # Also create the themes and icons folders if possible if self._dataPath.is_dir(): (self._dataPath / "cache").mkdir(exist_ok=True) (self._dataPath / "icons").mkdir(exist_ok=True) - (self._dataPath / "syntax").mkdir(exist_ok=True) (self._dataPath / "themes").mkdir(exist_ok=True) self._recentPaths.loadCache() From 89a7c3e5cb4914441e607b0f48ddc43fdb4c8ba6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:08:01 +0200 Subject: [PATCH 10/20] Add dark/light toggle button to sidebar --- novelwriter/constants.py | 13 ++++++++- novelwriter/gui/sidebar.py | 43 ++++++++++++++++++++++++++--- novelwriter/gui/theme.py | 33 ++++++++++++----------- novelwriter/guimain.py | 55 ++++++++++++++++++++++++++------------ 4 files changed, 107 insertions(+), 37 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index c20b9c47..336c6318 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -28,7 +28,8 @@ from typing import Final from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from novelwriter.enum import ( - nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape + nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape, + nwTheme ) @@ -453,6 +454,16 @@ class nwLabels: "blue": QT_TRANSLATE_NOOP("Constant", "Blue"), "purple": QT_TRANSLATE_NOOP("Constant", "Purple"), } + THEME_MODE_ICON: Final[dict[nwTheme, str]] = { + nwTheme.AUTO: "theme_auto", + nwTheme.LIGHT: "theme_light", + nwTheme.DARK: "theme_dark", + } + THEME_MODE_LABEL: Final[dict[nwTheme, str]] = { + nwTheme.AUTO: QT_TRANSLATE_NOOP("Constant", "System Theme"), + nwTheme.LIGHT: QT_TRANSLATE_NOOP("Constant", "Light Theme"), + nwTheme.DARK: QT_TRANSLATE_NOOP("Constant", "Dark Theme"), + } class nwHeadFmt: diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index f784623f..a4dd3c9c 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -27,12 +27,13 @@ import logging from typing import TYPE_CHECKING -from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal +from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget -from novelwriter import SHARED +from novelwriter import CONFIG, SHARED from novelwriter.common import qtLambda -from novelwriter.enum import nwView +from novelwriter.constants import nwLabels, trConst +from novelwriter.enum import nwTheme, nwView from novelwriter.extensions.eventfilters import StatusTipFilter from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON @@ -77,6 +78,10 @@ class GuiSideBar(QWidget): self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View"))) self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE)) + self.tbTheme = NIconToolButton(self, iSz) + self.tbTheme.setToolTip(self.tr("Switch Colour Theme")) + self.tbTheme.clicked.connect(self._cycleColurTheme) + self.tbBuild = NIconToolButton(self, iSz) self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) @@ -109,6 +114,7 @@ class GuiSideBar(QWidget): self.outerBox.addWidget(self.tbOutline) self.outerBox.addWidget(self.tbBuild) self.outerBox.addStretch(1) + self.outerBox.addWidget(self.tbTheme) self.outerBox.addWidget(self.tbDetails) self.outerBox.addWidget(self.tbStats) self.outerBox.addWidget(self.tbSettings) @@ -131,6 +137,7 @@ class GuiSideBar(QWidget): self.tbSearch.setStyleSheet(buttonStyle) self.tbOutline.setStyleSheet(buttonStyle) self.tbBuild.setStyleSheet(buttonStyle) + self.tbTheme.setStyleSheet(buttonStyle) self.tbDetails.setStyleSheet(buttonStyle) self.tbStats.setStyleSheet(buttonStyle) self.tbSettings.setStyleSheet(buttonStyle) @@ -144,6 +151,36 @@ class GuiSideBar(QWidget): self.tbStats.setThemeIcon("sb_stats") self.tbSettings.setThemeIcon("settings") + self._setThemeModeIcon() + + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _cycleColurTheme(self) -> None: + """Go to nex colour theme.""" + match CONFIG.themeMode: + case nwTheme.AUTO: + CONFIG.themeMode = nwTheme.LIGHT + case nwTheme.LIGHT: + CONFIG.themeMode = nwTheme.DARK + case nwTheme.DARK: + CONFIG.themeMode = nwTheme.AUTO + self.mainGui.checkThemeUpdate() + self._setThemeModeIcon() + return + + ## + # Internal Functions + ## + + def _setThemeModeIcon(self) -> None: + """Set the theme button icon.""" + self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode]) + self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode])) return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 4a53f453..43a6ffe3 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -100,22 +100,22 @@ class GuiTheme: """ __slots__ = ( - "_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", "_qColors", - "_styleSheets", "_svgColors", "_syntaxList", "_themeList", "baseButtonHeight", - "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText", - "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", + "_availSyntax", "_availThemes", "_currentTheme", "_darkThemes", "_guiPalette", + "_lightThemes", "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "_themeList", + "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", + "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", - "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth", + "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth", "themeMeta", ) def __init__(self) -> None: - self.iconCache = GuiIcons(self) - - # GUI Theme + # Theme Objects self.themeMeta = ThemeMeta() + self.iconCache = GuiIcons(self) + self.syntaxTheme = SyntaxColors() self.isDarkTheme = False # Special Text Colours @@ -123,11 +123,8 @@ class GuiTheme: self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) - # Syntax Theme - self.syntaxMeta = ThemeMeta() - self.syntaxTheme = SyntaxColors() - # Load Themes + self._currentTheme = "" self._guiPalette = QPalette() self._themeList: list[T_ThemeEntry] = [] self._availThemes: dict[str, Path] = {} @@ -275,6 +272,10 @@ class GuiTheme: theme = DEF_GUI_LIGHT CONFIG.lightTheme = DEF_GUI_LIGHT + if theme == self._currentTheme: + logger.info("Theme '%s' is already loaded", theme) + return False + if not (file := self._availThemes.get(theme)): logger.error("Could not load GUI theme") return False @@ -387,7 +388,6 @@ class GuiTheme: text = self._guiPalette.text().color() window = self._guiPalette.window().color() highlight = self._guiPalette.highlight().color() - isDark = text.lightnessF() > window.lightnessF() QtColActive = QPalette.ColorGroup.Active QtColInactive = QPalette.ColorGroup.Inactive @@ -407,8 +407,8 @@ class GuiTheme: darkOff = dark.darker(150) shadowOff = ref.darker(150) - grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) - dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) + grey = QColor(120, 120, 120) if darkMode else QColor(140, 140, 140) + dimmed = QColor(130, 130, 130) if darkMode else QColor(190, 190, 190) placeholder = QColor(text) placeholder.setAlpha(128) @@ -453,10 +453,11 @@ class GuiTheme: self.iconCache.loadTheme(CONFIG.iconTheme) # Finalise - self.isDarkTheme = isDark + self.isDarkTheme = darkMode QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) + self._currentTheme = theme CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}") return True diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 601540f0..6af7b3c7 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -30,7 +30,7 @@ from datetime import datetime from pathlib import Path from time import time -from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSlot from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut from PyQt6.QtWidgets import ( QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, @@ -902,10 +902,46 @@ class GuiMain(QMainWindow): return not self.splitView.isVisible() + def checkThemeUpdate(self) -> None: + """Load theme if mode changed.""" + if SHARED.theme.loadTheme(): + self.refreshThemeColors(syntax=True) + self.docEditor.initEditor() + self.docViewer.initViewer() + return + + def refreshThemeColors(self, syntax: bool) -> None: + """Refresh the GUI theme.""" + SHARED.theme.loadTheme() + self.setPalette(QApplication.palette()) + self.docEditor.updateTheme() + self.docViewer.updateTheme() + self.docViewerPanel.updateTheme() + self.sideBar.updateTheme() + self.projView.updateTheme() + self.novelView.updateTheme() + self.projSearch.updateTheme() + self.outlineView.updateTheme() + self.itemDetails.updateTheme() + self.mainStatus.updateTheme() + SHARED.project.tree.refreshAllItems() + + if syntax: + self.docEditor.updateSyntaxColors() + + return + ## # Events ## + def changeEvent(self, event: QEvent) -> None: + """Capture application change events.""" + if int(event.type()) == 210: + # ThemeChange + self.checkThemeUpdate() + return + def closeEvent(self, event: QCloseEvent) -> None: """Capture the closing event of the GUI and call the close function to handle all the close process steps. @@ -1054,22 +1090,7 @@ class GuiMain(QMainWindow): self.novelView.refreshCurrentTree() if theme: - SHARED.theme.loadTheme() - self.setPalette(QApplication.palette()) - self.docEditor.updateTheme() - self.docViewer.updateTheme() - self.docViewerPanel.updateTheme() - self.sideBar.updateTheme() - self.projView.updateTheme() - self.novelView.updateTheme() - self.projSearch.updateTheme() - self.outlineView.updateTheme() - self.itemDetails.updateTheme() - self.mainStatus.updateTheme() - SHARED.project.tree.refreshAllItems() - - if syntax: - self.docEditor.updateSyntaxColors() + self.refreshThemeColors(syntax=syntax) self.docEditor.initEditor() self.docViewer.initViewer() From 059085a21a50d53f5b7d543bcbc14b00068033bc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:13:30 +0200 Subject: [PATCH 11/20] Add theme icons --- novelwriter/assets/icons/font_awesome.icons | 3 +++ novelwriter/assets/icons/material_filled_bold.icons | 3 +++ novelwriter/assets/icons/material_filled_normal.icons | 3 +++ novelwriter/assets/icons/material_filled_thin.icons | 3 +++ novelwriter/assets/icons/material_rounded_bold.icons | 3 +++ novelwriter/assets/icons/material_rounded_normal.icons | 3 +++ novelwriter/assets/icons/material_rounded_thin.icons | 3 +++ novelwriter/assets/icons/remix_filled.icons | 3 +++ novelwriter/assets/icons/remix_outline.icons | 3 +++ utils/icon_themes.py | 4 ++++ utils/icon_themes/font_awesome.json | 4 ++++ utils/icon_themes/material_symbols.json | 4 ++++ utils/icon_themes/remix.json | 4 ++++ 13 files changed, 43 insertions(+) diff --git a/novelwriter/assets/icons/font_awesome.icons b/novelwriter/assets/icons/font_awesome.icons index 550fd857..75204942 100644 --- a/novelwriter/assets/icons/font_awesome.icons +++ b/novelwriter/assets/icons/font_awesome.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_filled_bold.icons b/novelwriter/assets/icons/material_filled_bold.icons index bc902698..584a04bb 100644 --- a/novelwriter/assets/icons/material_filled_bold.icons +++ b/novelwriter/assets/icons/material_filled_bold.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_filled_normal.icons b/novelwriter/assets/icons/material_filled_normal.icons index ce666d14..a6d7933e 100644 --- a/novelwriter/assets/icons/material_filled_normal.icons +++ b/novelwriter/assets/icons/material_filled_normal.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_filled_thin.icons b/novelwriter/assets/icons/material_filled_thin.icons index 70c4f364..a405ea3e 100644 --- a/novelwriter/assets/icons/material_filled_thin.icons +++ b/novelwriter/assets/icons/material_filled_thin.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_rounded_bold.icons b/novelwriter/assets/icons/material_rounded_bold.icons index 08564be2..02d83821 100644 --- a/novelwriter/assets/icons/material_rounded_bold.icons +++ b/novelwriter/assets/icons/material_rounded_bold.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_rounded_normal.icons b/novelwriter/assets/icons/material_rounded_normal.icons index 3da2739e..06887aa7 100644 --- a/novelwriter/assets/icons/material_rounded_normal.icons +++ b/novelwriter/assets/icons/material_rounded_normal.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_rounded_thin.icons b/novelwriter/assets/icons/material_rounded_thin.icons index 61fcd5fa..1002fcbf 100644 --- a/novelwriter/assets/icons/material_rounded_thin.icons +++ b/novelwriter/assets/icons/material_rounded_thin.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/remix_filled.icons b/novelwriter/assets/icons/remix_filled.icons index b2305187..51e73a78 100644 --- a/novelwriter/assets/icons/remix_filled.icons +++ b/novelwriter/assets/icons/remix_filled.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/remix_outline.icons b/novelwriter/assets/icons/remix_outline.icons index 0fae7c91..02ce0243 100644 --- a/novelwriter/assets/icons/remix_outline.icons +++ b/novelwriter/assets/icons/remix_outline.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/utils/icon_themes.py b/utils/icon_themes.py index 00d75714..752b29e5 100644 --- a/utils/icon_themes.py +++ b/utils/icon_themes.py @@ -90,6 +90,10 @@ ICONS = [ "sb_search", "sb_stats", + "theme_light", + "theme_dark", + "theme_auto", + "add", "bookmarks", "browse", diff --git a/utils/icon_themes/font_awesome.json b/utils/icon_themes/font_awesome.json index 5ed09376..d98dc9e2 100644 --- a/utils/icon_themes/font_awesome.json +++ b/utils/icon_themes/font_awesome.json @@ -56,6 +56,10 @@ "sb_search": "magnifying-glass", "sb_stats": "chart-simple", + "theme_light": "sun", + "theme_dark": "moon", + "theme_auto": "circle-half-stroke", + "add": "plus", "bookmarks": "bookmark", "browse": "folder-open", diff --git a/utils/icon_themes/material_symbols.json b/utils/icon_themes/material_symbols.json index 230f5c60..61c1975a 100644 --- a/utils/icon_themes/material_symbols.json +++ b/utils/icon_themes/material_symbols.json @@ -56,6 +56,10 @@ "sb_search": "search", "sb_stats": "bar_chart", + "theme_light": "light_mode", + "theme_dark": "dark_mode", + "theme_auto": "contrast", + "add": "add", "bookmarks": "bookmarks", "browse": "folder_open", diff --git a/utils/icon_themes/remix.json b/utils/icon_themes/remix.json index 300b8bbe..0ac173ec 100644 --- a/utils/icon_themes/remix.json +++ b/utils/icon_themes/remix.json @@ -56,6 +56,10 @@ "sb_search": "file-search", "sb_stats": "bar-chart-fill", + "theme_light": "sun", + "theme_dark": "moon", + "theme_auto": "contrast", + "add": "add", "bookmarks": "bookmark", "browse": "folder-2", From 44528b61735f0a4e161262799cf9fa1698f83f6a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:20:51 +0200 Subject: [PATCH 12/20] Fix icon theme reload --- novelwriter/gui/theme.py | 4 ++-- novelwriter/guimain.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 43a6ffe3..bcdb1108 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -252,7 +252,7 @@ class GuiTheme: return QColor(*result) return default - def loadTheme(self) -> bool: + def loadTheme(self, force: bool = False) -> bool: """Load the currently specified GUI theme.""" match CONFIG.themeMode: case nwTheme.LIGHT: @@ -272,7 +272,7 @@ class GuiTheme: theme = DEF_GUI_LIGHT CONFIG.lightTheme = DEF_GUI_LIGHT - if theme == self._currentTheme: + if theme == self._currentTheme and not force: logger.info("Theme '%s' is already loaded", theme) return False diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6af7b3c7..41edcf3e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -910,9 +910,9 @@ class GuiMain(QMainWindow): self.docViewer.initViewer() return - def refreshThemeColors(self, syntax: bool) -> None: + def refreshThemeColors(self, syntax: bool = False, force: bool = False) -> None: """Refresh the GUI theme.""" - SHARED.theme.loadTheme() + SHARED.theme.loadTheme(force=force) self.setPalette(QApplication.palette()) self.docEditor.updateTheme() self.docViewer.updateTheme() @@ -1090,7 +1090,7 @@ class GuiMain(QMainWindow): self.novelView.refreshCurrentTree() if theme: - self.refreshThemeColors(syntax=syntax) + self.refreshThemeColors(syntax=syntax, force=True) self.docEditor.initEditor() self.docViewer.initViewer() From b0ea1781e7cc3b90c32d609cf7d401e0ac08fdbf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:28:28 +0200 Subject: [PATCH 13/20] Fix test --- tests/test_gui/test_gui_theme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index ff4308f2..316f029b 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -137,8 +137,9 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255) # Non-existing value should return default colour + theme._setBaseColor("default", QColor(64, 64, 64, 255)) theme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (64, 64, 64, 255) # qtbot.stop() From 82188d156bb6e7bf57e4e83a6c97f8c8bf2aa2b6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:02:38 +0200 Subject: [PATCH 14/20] Change the name and alpha format of colour themes --- novelwriter/assets/themes/default_dark.conf | 4 +- novelwriter/assets/themes/default_light.conf | 2 +- novelwriter/assets/themes/light_owl.conf | 4 +- novelwriter/assets/themes/night_owl.conf | 4 +- novelwriter/assets/themes/tango_dark.conf | 6 +- novelwriter/assets/themes/tango_light.conf | 10 +-- novelwriter/assets/themes/tomorrow.conf | 4 +- novelwriter/assets/themes/tomorrow_night.conf | 4 +- .../assets/themes/tomorrow_night_blue.conf | 4 +- .../assets/themes/tomorrow_night_bright.conf | 4 +- .../themes/tomorrow_night_eighties.conf | 4 +- novelwriter/gui/sidebar.py | 12 ++-- novelwriter/gui/theme.py | 36 +++++----- tests/test_gui/test_gui_theme.py | 72 +++++++++---------- 14 files changed, 84 insertions(+), 86 deletions(-) diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 35acdb4c..71943bd8 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -54,7 +54,7 @@ background = #363636 text = default link = blue headertext = green -headertag = green, 160 +headertag = green:160 emphasis = orange dialog = blue altdialog = blue @@ -69,4 +69,4 @@ spellcheckline = red errorline = green replacetag = green modifier = green -texthighlight = yellow, 72 +texthighlight = yellow:72 diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf index 0c309a3c..a67759dc 100644 --- a/novelwriter/assets/themes/default_light.conf +++ b/novelwriter/assets/themes/default_light.conf @@ -54,7 +54,7 @@ background = #ffffff text = #000000 link = blue headertext = green -headertag = green, 160 +headertag = green:160 emphasis = orange dialog = blue altdialog = blue diff --git a/novelwriter/assets/themes/light_owl.conf b/novelwriter/assets/themes/light_owl.conf index f72b8d70..3544b6cd 100644 --- a/novelwriter/assets/themes/light_owl.conf +++ b/novelwriter/assets/themes/light_owl.conf @@ -73,7 +73,7 @@ background = #fbfbfb text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -88,4 +88,4 @@ spellcheckline = red errorline = green replacetag = aqua modifier = green -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/night_owl.conf b/novelwriter/assets/themes/night_owl.conf index 0d176c45..36ec3432 100644 --- a/novelwriter/assets/themes/night_owl.conf +++ b/novelwriter/assets/themes/night_owl.conf @@ -73,7 +73,7 @@ background = #011627 text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -88,4 +88,4 @@ spellcheckline = red errorline = green replacetag = aqua modifier = green -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tango_dark.conf b/novelwriter/assets/themes/tango_dark.conf index ca0ba841..9c4dc59e 100644 --- a/novelwriter/assets/themes/tango_dark.conf +++ b/novelwriter/assets/themes/tango_dark.conf @@ -63,7 +63,7 @@ background = #2e3436 text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -76,6 +76,6 @@ value = red optional = blue spellcheckline = red errorline = green -replacetag = cyan +replacetag = aqua modifier = blue -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tango_light.conf b/novelwriter/assets/themes/tango_light.conf index 174b0571..3c66d1e9 100644 --- a/novelwriter/assets/themes/tango_light.conf +++ b/novelwriter/assets/themes/tango_light.conf @@ -1,6 +1,6 @@ [Main] -name = Tango Light -mode = light +name = Tango Light +mode = light author = Veronica Berglyd Olsen (adaptation) ## @@ -63,7 +63,7 @@ background = #eeeeec text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -76,6 +76,6 @@ value = red optional = blue spellcheckline = red errorline = green -replacetag = cyan +replacetag = aqua modifier = blue -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow.conf b/novelwriter/assets/themes/tomorrow.conf index 2b734209..f8b9d5df 100644 --- a/novelwriter/assets/themes/tomorrow.conf +++ b/novelwriter/assets/themes/tomorrow.conf @@ -73,7 +73,7 @@ background = #ffffff text = #4d4d4c link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -88,4 +88,4 @@ spellcheckline = red errorline = green replacetag = aqua modifier = orange -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night.conf b/novelwriter/assets/themes/tomorrow_night.conf index 27abd454..d0e91fbd 100644 --- a/novelwriter/assets/themes/tomorrow_night.conf +++ b/novelwriter/assets/themes/tomorrow_night.conf @@ -73,7 +73,7 @@ background = #1d1f21 text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -88,4 +88,4 @@ spellcheckline = red errorline = green replacetag = aqua modifier = orange -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_blue.conf b/novelwriter/assets/themes/tomorrow_night_blue.conf index 93b6592e..79fa3ea4 100644 --- a/novelwriter/assets/themes/tomorrow_night_blue.conf +++ b/novelwriter/assets/themes/tomorrow_night_blue.conf @@ -73,7 +73,7 @@ background = #002451 text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -88,4 +88,4 @@ spellcheckline = red errorline = green replacetag = aqua modifier = orange -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_bright.conf b/novelwriter/assets/themes/tomorrow_night_bright.conf index de76e694..3c2ad22d 100644 --- a/novelwriter/assets/themes/tomorrow_night_bright.conf +++ b/novelwriter/assets/themes/tomorrow_night_bright.conf @@ -73,7 +73,7 @@ background = #000000 text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -88,4 +88,4 @@ spellcheckline = red errorline = green replacetag = aqua modifier = orange -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_eighties.conf b/novelwriter/assets/themes/tomorrow_night_eighties.conf index e3e8bbec..8f969073 100644 --- a/novelwriter/assets/themes/tomorrow_night_eighties.conf +++ b/novelwriter/assets/themes/tomorrow_night_eighties.conf @@ -73,7 +73,7 @@ background = #2d2d2d text = default link = blue headertext = blue -headertag = blue, 160 +headertag = blue:160 emphasis = orange dialog = green altdialog = yellow @@ -88,4 +88,4 @@ spellcheckline = red errorline = green replacetag = aqua modifier = orange -texthighlight = yellow, 96 +texthighlight = yellow:96 diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index a4dd3c9c..e0ccb9e5 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -82,10 +82,6 @@ class GuiSideBar(QWidget): self.tbTheme.setToolTip(self.tr("Switch Colour Theme")) self.tbTheme.clicked.connect(self._cycleColurTheme) - self.tbBuild = NIconToolButton(self, iSz) - self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) - self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) - self.tbDetails = NIconToolButton(self, iSz) self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details"))) self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog) @@ -94,6 +90,10 @@ class GuiSideBar(QWidget): self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog) + self.tbBuild = NIconToolButton(self, iSz) + self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) + self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) + # Settings Menu self.tbSettings = NIconToolButton(self, iSz) self.tbSettings.setToolTip(self.tr("Settings")) @@ -114,9 +114,9 @@ class GuiSideBar(QWidget): self.outerBox.addWidget(self.tbOutline) self.outerBox.addWidget(self.tbBuild) self.outerBox.addStretch(1) - self.outerBox.addWidget(self.tbTheme) self.outerBox.addWidget(self.tbDetails) self.outerBox.addWidget(self.tbStats) + self.outerBox.addWidget(self.tbTheme) self.outerBox.addWidget(self.tbSettings) self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setSpacing(6) @@ -137,9 +137,9 @@ class GuiSideBar(QWidget): self.tbSearch.setStyleSheet(buttonStyle) self.tbOutline.setStyleSheet(buttonStyle) self.tbBuild.setStyleSheet(buttonStyle) - self.tbTheme.setStyleSheet(buttonStyle) self.tbDetails.setStyleSheet(buttonStyle) self.tbStats.setStyleSheet(buttonStyle) + self.tbTheme.setStyleSheet(buttonStyle) self.tbSettings.setStyleSheet(buttonStyle) self.tbProject.setThemeIcon("sb_project") diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index bcdb1108..236bebe7 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -229,27 +229,25 @@ class GuiTheme: if value in self._qColors: # Named colour return self._qColors[value] - elif value.startswith("#"): - if len(value) >= 9: - # Convert from #RRGGBBAA to #AARRGGBB - return QColor.fromString(f"#{value[7:9]}{value[1:7]}") - else: - # Assume #RRGGBB - return QColor.fromString(value[:7]) + elif value.startswith("#") and len(value) == 7: + # Assume #RRGGBB + return QColor.fromString(value) + elif value.startswith("#") and len(value) == 9: + # Assume #RRGGBBAA and convert to #AARRGGBB + return QColor.fromString(f"#{value[7:9]}{value[1:7]}") + elif ":" in value: + # Colour name and alpha + name, _, alpha = value.partition(":") + color = QColor(self._qColors.get(name.strip(), default)) + color.setAlpha(checkInt(alpha, 255)) + return color elif "," in value: + # Integer red, green, blue, alpha data = value.split(",") - entries = len(data) - if entries == 2: - # Assume name, alpha - color = QColor(self._qColors.get(data[0].strip(), default)) - color.setAlpha(checkInt(data[1], 255)) - return color - else: - # Assume red, green, blue, alpha - result = [0, 0, 0, 255] - for i in range(min(entries, 4)): - result[i] = checkInt(data[i].strip(), result[i]) - return QColor(*result) + result = [0, 0, 0, 255] + for i in range(min(len(data), 4)): + result[i] = checkInt(data[i].strip(), result[i]) + return QColor(*result) return default def loadTheme(self, force: bool = False) -> bool: diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 316f029b..f57a2c30 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -39,40 +39,6 @@ from tests.mocked import causeOSError from tests.tools import writeFile -@pytest.mark.gui -def testGuiTheme_ParseColor(qtbot, nwGUI): - """Test the colour parsing.""" - theme = SHARED.theme - - # Pre-Populate - theme._qColors["red"] = QColor(255, 0, 0) - theme._qColors["green"] = QColor(0, 255, 0) - theme._qColors["blue"] = QColor(0, 0, 255) - - # By Name - assert theme.parseColor("red").getRgb() == (255, 0, 0, 255) - assert theme.parseColor("green").getRgb() == (0, 255, 0, 255) - assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255) - assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255) - - # CSS Format - assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255) - assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127) - assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored - assert theme.parseColor("#ff00007f15").getRgb() == (255, 0, 0, 127) # Too long -> truncated - - # Name + Alpha - assert theme.parseColor("red, 255").getRgb() == (255, 0, 0, 255) - assert theme.parseColor("red, 127").getRgb() == (255, 0, 0, 127) - assert theme.parseColor("red, 512").getRgb() == (255, 0, 0, 255) # Value truncated - - # Values - assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255) - assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255) - assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127) - assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated - - @pytest.mark.gui def testGuiTheme_Main(qtbot, nwGUI, tstPaths): """Test the theme class init.""" @@ -118,7 +84,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): assert theme._readColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255) assert theme._readColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250) assert theme._readColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250) - assert theme._readColor(parser, "Palette", "colour4").getRgb() == (0, 0, 0, 250) + assert theme._readColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255) assert theme._readColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0) assert theme._readColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255) @@ -130,7 +96,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): theme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window) assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) theme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 250) + assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255) theme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window) assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0) theme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window) @@ -144,6 +110,40 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): # qtbot.stop() +@pytest.mark.gui +def testGuiTheme_ParseColor(qtbot, nwGUI): + """Test the colour parsing.""" + theme = SHARED.theme + + # Pre-Populate + theme._qColors["red"] = QColor(255, 0, 0) + theme._qColors["green"] = QColor(0, 255, 0) + theme._qColors["blue"] = QColor(0, 0, 255) + + # By Name + assert theme.parseColor("red").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("green").getRgb() == (0, 255, 0, 255) + assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255) + assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255) + + # CSS Format + assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored + assert theme.parseColor("#ff00007f15").getRgb() == (0, 0, 0, 255) # Too long -> ignored + + # Name + Alpha + assert theme.parseColor("red:255").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("red:127").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("red:512").getRgb() == (255, 0, 0, 255) # Value truncated + + # Values + assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated + + @pytest.mark.gui @pytest.mark.skip def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): From 60b5f660e8d899db5704251a9417add2049364f0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:30:13 +0200 Subject: [PATCH 15/20] Rename colour aqua to cyan in themes --- novelwriter/assets/themes/cyberpunk_night.conf | 2 +- novelwriter/assets/themes/default_dark.conf | 2 +- novelwriter/assets/themes/default_light.conf | 2 +- novelwriter/assets/themes/dracula.conf | 2 +- novelwriter/assets/themes/grey_dark.conf | 2 +- novelwriter/assets/themes/grey_light.conf | 2 +- novelwriter/assets/themes/light_owl.conf | 4 ++-- novelwriter/assets/themes/night_owl.conf | 4 ++-- novelwriter/assets/themes/snazzy.conf | 2 +- novelwriter/assets/themes/solarized_dark.conf | 2 +- novelwriter/assets/themes/solarized_light.conf | 2 +- novelwriter/assets/themes/tango_dark.conf | 4 ++-- novelwriter/assets/themes/tango_light.conf | 4 ++-- novelwriter/assets/themes/tomorrow.conf | 4 ++-- novelwriter/assets/themes/tomorrow_night.conf | 4 ++-- novelwriter/assets/themes/tomorrow_night_blue.conf | 4 ++-- novelwriter/assets/themes/tomorrow_night_bright.conf | 4 ++-- novelwriter/assets/themes/tomorrow_night_eighties.conf | 4 ++-- novelwriter/constants.py | 2 +- novelwriter/gui/theme.py | 6 +++--- 20 files changed, 31 insertions(+), 31 deletions(-) diff --git a/novelwriter/assets/themes/cyberpunk_night.conf b/novelwriter/assets/themes/cyberpunk_night.conf index 1ee10ca8..40b5dd53 100644 --- a/novelwriter/assets/themes/cyberpunk_night.conf +++ b/novelwriter/assets/themes/cyberpunk_night.conf @@ -14,7 +14,7 @@ red = #f24817 orange = #ff960a yellow = #ffff00 green = #00ff00 -aqua = #00ffff +cyan = #00ffff blue = #4d4dff purple = #320064 diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 71943bd8..e559de9e 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -15,7 +15,7 @@ red = #f2777a orange = #f99139 yellow = #ffcc66 green = #99cc99 -aqua = #66cccc +cyan = #66cccc blue = #6699cc purple = #cc99cc diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf index a67759dc..638d6879 100644 --- a/novelwriter/assets/themes/default_light.conf +++ b/novelwriter/assets/themes/default_light.conf @@ -15,7 +15,7 @@ red = #a62a2d orange = #b36829 yellow = #a68542 green = #296629 -aqua = #269999 +cyan = #269999 blue = #3a70a6 purple = #b35ab3 diff --git a/novelwriter/assets/themes/dracula.conf b/novelwriter/assets/themes/dracula.conf index 49f30501..04e32450 100644 --- a/novelwriter/assets/themes/dracula.conf +++ b/novelwriter/assets/themes/dracula.conf @@ -31,7 +31,7 @@ red = #ff5555 orange = #ffb86c yellow = #f1fa8c green = #50fa7b -aqua = #8be9fd +cyan = #8be9fd blue = #93cff9 purple = #bd93f9 diff --git a/novelwriter/assets/themes/grey_dark.conf b/novelwriter/assets/themes/grey_dark.conf index d8c4a1fd..21d95224 100644 --- a/novelwriter/assets/themes/grey_dark.conf +++ b/novelwriter/assets/themes/grey_dark.conf @@ -14,7 +14,7 @@ red = #f2777a orange = #f99139 yellow = #ffcc66 green = #99cc99 -aqua = #66cccc +cyan = #66cccc blue = #6699cc purple = #cc99cc diff --git a/novelwriter/assets/themes/grey_light.conf b/novelwriter/assets/themes/grey_light.conf index ffa8fe61..651ab6d0 100644 --- a/novelwriter/assets/themes/grey_light.conf +++ b/novelwriter/assets/themes/grey_light.conf @@ -14,7 +14,7 @@ red = #a62a2d orange = #b36829 yellow = #a68542 green = #296629 -aqua = #269999 +cyan = #269999 blue = #3a70a6 purple = #b35ab3 diff --git a/novelwriter/assets/themes/light_owl.conf b/novelwriter/assets/themes/light_owl.conf index 3544b6cd..b18f5fb7 100644 --- a/novelwriter/assets/themes/light_owl.conf +++ b/novelwriter/assets/themes/light_owl.conf @@ -34,7 +34,7 @@ red = #de3d3a orange = #e0af05 yellow = #daaa01 green = #08916a -aqua = #2aa298 +cyan = #2aa298 blue = #288ed7 purple = #964ac1 @@ -86,6 +86,6 @@ value = orange optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = green texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/night_owl.conf b/novelwriter/assets/themes/night_owl.conf index 36ec3432..8aae2b62 100644 --- a/novelwriter/assets/themes/night_owl.conf +++ b/novelwriter/assets/themes/night_owl.conf @@ -34,7 +34,7 @@ red = #f78c6c orange = #ecc48d yellow = #addb67 green = #addb67 -aqua = #7fdbca +cyan = #7fdbca blue = #82aaff purple = #c792ea @@ -86,6 +86,6 @@ value = orange optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = green texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/snazzy.conf b/novelwriter/assets/themes/snazzy.conf index 19a662c0..6220dcc3 100644 --- a/novelwriter/assets/themes/snazzy.conf +++ b/novelwriter/assets/themes/snazzy.conf @@ -27,7 +27,7 @@ red = #ff5c57 orange = #f5b900 yellow = #cf9c00 green = #2dae58 -aqua = #13bbb7 +cyan = #13bbb7 blue = #09a1ed purple = #f767bb diff --git a/novelwriter/assets/themes/solarized_dark.conf b/novelwriter/assets/themes/solarized_dark.conf index 354b2be9..5cf4e157 100644 --- a/novelwriter/assets/themes/solarized_dark.conf +++ b/novelwriter/assets/themes/solarized_dark.conf @@ -33,7 +33,7 @@ red = #dc322f orange = #cb4b16 yellow = #b58900 green = #859900 -aqua = #2aa198 +cyan = #2aa198 blue = #268bd2 purple = #6c71c4 diff --git a/novelwriter/assets/themes/solarized_light.conf b/novelwriter/assets/themes/solarized_light.conf index 7d7427f5..f185bd73 100644 --- a/novelwriter/assets/themes/solarized_light.conf +++ b/novelwriter/assets/themes/solarized_light.conf @@ -33,7 +33,7 @@ red = #dc322f orange = #cb4b16 yellow = #b58900 green = #859900 -aqua = #2aa198 +cyan = #2aa198 blue = #268bd2 purple = #6c71c4 diff --git a/novelwriter/assets/themes/tango_dark.conf b/novelwriter/assets/themes/tango_dark.conf index 9c4dc59e..33e758f7 100644 --- a/novelwriter/assets/themes/tango_dark.conf +++ b/novelwriter/assets/themes/tango_dark.conf @@ -24,7 +24,7 @@ red = #ef2929 orange = #fcaf3e yellow = #fce94f green = #8ae234 -aqua = #34e2e2 +cyan = #34e2e2 blue = #729fcf purple = #ad7fa8 @@ -76,6 +76,6 @@ value = red optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = blue texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tango_light.conf b/novelwriter/assets/themes/tango_light.conf index 3c66d1e9..877f71ac 100644 --- a/novelwriter/assets/themes/tango_light.conf +++ b/novelwriter/assets/themes/tango_light.conf @@ -24,7 +24,7 @@ red = #a40000 orange = #ce5c00 yellow = #c4a000 green = #4e9a06 -aqua = #069a9a +cyan = #069a9a blue = #204a87 purple = #5c3566 @@ -76,6 +76,6 @@ value = red optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = blue texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow.conf b/novelwriter/assets/themes/tomorrow.conf index f8b9d5df..974816c1 100644 --- a/novelwriter/assets/themes/tomorrow.conf +++ b/novelwriter/assets/themes/tomorrow.conf @@ -34,7 +34,7 @@ red = #c82829 orange = #f5871f yellow = #eab700 green = #718c00 -aqua = #3e999f +cyan = #3e999f blue = #4271ae purple = #8959a8 @@ -86,6 +86,6 @@ value = yellow optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = orange texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night.conf b/novelwriter/assets/themes/tomorrow_night.conf index d0e91fbd..94fecbc0 100644 --- a/novelwriter/assets/themes/tomorrow_night.conf +++ b/novelwriter/assets/themes/tomorrow_night.conf @@ -34,7 +34,7 @@ red = #cc6666 orange = #de935f yellow = #f0c674 green = #b5bd68 -aqua = #8abeb7 +cyan = #8abeb7 blue = #81a2be purple = #b294bb @@ -86,6 +86,6 @@ value = yellow optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = orange texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_blue.conf b/novelwriter/assets/themes/tomorrow_night_blue.conf index 79fa3ea4..88aac2e3 100644 --- a/novelwriter/assets/themes/tomorrow_night_blue.conf +++ b/novelwriter/assets/themes/tomorrow_night_blue.conf @@ -34,7 +34,7 @@ red = #ff9da4 orange = #ffc58f yellow = #ffeead green = #d1f1a9 -aqua = #99ffff +cyan = #99ffff blue = #bbdaff purple = #ebbbff @@ -86,6 +86,6 @@ value = yellow optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = orange texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_bright.conf b/novelwriter/assets/themes/tomorrow_night_bright.conf index 3c2ad22d..ab3c4766 100644 --- a/novelwriter/assets/themes/tomorrow_night_bright.conf +++ b/novelwriter/assets/themes/tomorrow_night_bright.conf @@ -34,7 +34,7 @@ red = #d54e53 orange = #e78c45 yellow = #e7c547 green = #b9ca4a -aqua = #70c0b1 +cyan = #70c0b1 blue = #7aa6da purple = #c397d8 @@ -86,6 +86,6 @@ value = yellow optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = orange texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_eighties.conf b/novelwriter/assets/themes/tomorrow_night_eighties.conf index 8f969073..ed67eb95 100644 --- a/novelwriter/assets/themes/tomorrow_night_eighties.conf +++ b/novelwriter/assets/themes/tomorrow_night_eighties.conf @@ -34,7 +34,7 @@ red = #f2777a orange = #f99157 yellow = #ffcc66 green = #99cc99 -aqua = #66cccc +cyan = #66cccc blue = #6699cc purple = #cc99cc @@ -86,6 +86,6 @@ value = yellow optional = blue spellcheckline = red errorline = green -replacetag = aqua +replacetag = cyan modifier = orange texthighlight = yellow:96 diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 336c6318..5b7b260d 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -450,7 +450,7 @@ class nwLabels: "orange": QT_TRANSLATE_NOOP("Constant", "Orange"), "yellow": QT_TRANSLATE_NOOP("Constant", "Yellow"), "green": QT_TRANSLATE_NOOP("Constant", "Green"), - "aqua": QT_TRANSLATE_NOOP("Constant", "Aqua"), + "cyan": QT_TRANSLATE_NOOP("Constant", "Cyan"), "blue": QT_TRANSLATE_NOOP("Constant", "Blue"), "purple": QT_TRANSLATE_NOOP("Constant", "Purple"), } diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 236bebe7..b5e2d2f2 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -316,7 +316,7 @@ class GuiTheme: self._setBaseColor("orange", self._readColor(parser, sec, "orange")) self._setBaseColor("yellow", self._readColor(parser, sec, "yellow")) self._setBaseColor("green", self._readColor(parser, sec, "green")) - self._setBaseColor("aqua", self._readColor(parser, sec, "aqua")) + self._setBaseColor("cyan", self._readColor(parser, sec, "cyan")) self._setBaseColor("blue", self._readColor(parser, sec, "blue")) self._setBaseColor("purple", self._readColor(parser, sec, "purple")) @@ -505,7 +505,7 @@ class GuiTheme: orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31) yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0) green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0) - aqua = QColor(102, 204, 204) if isDark else QColor(62, 153, 159) + cyan = QColor(102, 204, 204) if isDark else QColor(62, 153, 159) blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174) purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168) @@ -526,7 +526,7 @@ class GuiTheme: self._setBaseColor("orange", orange) self._setBaseColor("yellow", yellow) self._setBaseColor("green", green) - self._setBaseColor("aqua", aqua) + self._setBaseColor("cyan", cyan) self._setBaseColor("blue", blue) self._setBaseColor("purple", purple) self._setBaseColor("root", blue) From df7a7216268cae42938fe92e17cbcc7ac81eb11d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:30:59 +0200 Subject: [PATCH 16/20] Update documentation on colour themes --- docs/source/index.rst | 1 + docs/source/more/customise.rst | 353 ++++++++++--------- docs/source/more/dictionaries.rst | 55 +++ docs/source/user_interface/editor_viewer.rst | 2 +- 4 files changed, 242 insertions(+), 169 deletions(-) create mode 100644 docs/source/more/dictionaries.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 8e863ff8..793d1d38 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -75,6 +75,7 @@ storage for robustness. more/counting more/typography + more/dictionaries more/customise more/handling_errors more/project_format diff --git a/docs/source/more/customise.rst b/docs/source/more/customise.rst index 80aa1201..8f3d2f60 100644 --- a/docs/source/more/customise.rst +++ b/docs/source/more/customise.rst @@ -1,94 +1,35 @@ .. _docs_more_custom: -************** -Customisations -************** +************* +Custom Themes +************* -.. _Enchant: https://rrthomas.github.io/enchant/ -.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/ - -There are a few ways you can customise novelWriter yourself. Currently, you can add new GUI themes, -your own syntax themes, and install additional dictionaries. - - -.. _docs_more_custom_dict: - -Spell Check Dictionaries -======================== - -novelWriter uses Enchant_ as the spell checking tool. Depending on your operating system, it may or -may not load all installed spell check dictionaries automatically. - - -Linux and MacOS ---------------- - -On Linux and MacOS, you generally only have to install hunspell, aspell or myspell dictionaries on -your system like you do for other applications. See your distro or OS documentation for how to do -this. These dictionaries should show up as available spell check languages in novelWriter. - - -Windows -------- - -For Windows, English is included with the installation. For other languages you have to download -and add dictionaries yourself. - -**Install Tool** - -A small tool to assist with this can be found under **Tools > Add Dictionaries**. It will import -spell checking dictionaries from Free Office or Libre Office extensions. The dictionaries are then -installed in the install location for the Enchant library and should thus work for any application -that uses Enchant for spell checking. - -**Manual Install** - -If you prefer to do this manually or want to use a different source than the ones mentioned above, -You need to get compatible dictionary files for your language. You need two files files ending with -``.aff`` and ``.dic``. These files must then be copied to the following location: - -``C:\Users\\AppData\Local\enchant\hunspell`` - -This assumes your user profile is stored at ``C:\Users\``. The last one or two folders may -not exist, so you may need to create them. - -You can find the various dictionaries on the `Free Desktop`_ website. - -.. note:: - The Free Desktop link points to a repository, and what may look like file links inside the - dictionary folder are actually links to web pages. If you right-click and download those, you - get HTML files, not dictionaries! - - In order to download the actual dictionary files, right-click the "plain" label at the end of - each line and download that. +There are a few ways you can customise novelWriter yourself. Currently, you can relatively easily +add new GUI themes. You can also add new icon themes, although this is not as straightforward. .. _docs_more_custom_theme: -Syntax and GUI Themes -===================== +Colour Themes +============= -Adding your own GUI and syntax themes is relatively easy, although it requires that you manually -edit config files with colour values. The themes are defined by simple plain text config files with -meta data and colour settings. +Adding your own colour themes is relatively easy, although it requires that you manually edit +config files with colour values. The themes are defined by simple plain text config files with meta +data and colour settings. In order to make your own versions, first copy one of the existing files to your local computer and modify it as you like. -* The existing syntax themes are stored in - `novelwriter/assets/syntax `_. -* The existing GUI themes are stored in - `novelwriter/assets/themes `_. -* The existing icon themes are stored in - `novelwriter/assets/icons `_. +The existing colour themes are stored in +`novelwriter/assets/themes `_. Remember to also change the name of your theme by modifying the ``name`` setting at the top of the file, otherwise you may not be able to distinguish them in **Preferences**. For novelWriter to be able to locate the custom theme files, you must copy them to the -:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder there named -``syntax`` for syntax themes, just ``themes`` for GUI themes, and ``icons`` for icon themes. These -folders are created the first time you start novelWriter. +:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder +there named ``themes`` for colour themes. These folders are created the first time you start +novelWriter. Once the files are copied there, they should show up in **Preferences** with the label you set as ``name`` inside the file. @@ -99,71 +40,123 @@ set as ``name`` inside the file. files up to date. -Custom GUI and Icons Theme --------------------------- +The Theme File Format +--------------------- -A GUI theme ``.conf`` file consists of the following settings: +A colour theme ``.conf`` file consists of the following settings: .. code-block:: cfg + :caption: The theme file for the "Default Light Theme" [Main] - name = My Custom Theme - description = A description of my custom theme - author = Jane Doe - credit = John Doe - url = https://example.com + name = Default Light Theme + mode = light + description = The novelWriter standard light theme + author = Veronica Berglyd Olsen + credit = Veronica Berglyd Olsen + url = https://github.com/vkbo/novelWriter license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - [Icons] - default = 100, 100, 100 - faded = 100, 100, 100 - red = 255, 0, 0 - orange = 255, 128, 0 - yellow = 255, 255, 0 - green = 0, 255, 0 - aqua = 0, 255, 255 - blue = 0, 0, 255 - purple = 255, 0, 255 + [Base] + default = #303030 + faded = #6c6c6c + red = #a62a2d + orange = #b36829 + yellow = #a68542 + green = #296629 + cyan = #269999 + blue = #3a70a6 + purple = #b35ab3 [Project] - root = 0, 255, 255 - folder = 255, 255, 0 - file = 100, 100, 100 - title = 0, 255, 0 - chapter = 255, 0, 0 - scene = 0, 0, 255 - note = 255, 255, 0 + root = blue + folder = yellow + file = default + title = green + chapter = red + scene = blue + note = yellow [Palette] - window = 100, 100, 100 - windowtext = 100, 100, 100 - base = 100, 100, 100 - alternatebase = 100, 100, 100 - text = 100, 100, 100 - tooltipbase = 100, 100, 100 - tooltiptext = 100, 100, 100 - button = 100, 100, 100 - buttontext = 100, 100, 100 - brighttext = 100, 100, 100 - highlight = 100, 100, 100 - highlightedtext = 100, 100, 100 - link = 100, 100, 100 - linkvisited = 100, 100, 100 + window = #efefef + windowtext = #000000 + base = #ffffff + alternatebase = #e0e0e0 + text = #000000 + tooltipbase = #ffffc0 + tooltiptext = #15150d + button = #efefef + buttontext = #000000 + brighttext = #ffffff + highlight = #3087c6 + highlightedtext = #ffffff + link = blue + linkvisited = blue [GUI] - helptext = 0, 0, 0 - fadedtext = 128, 128, 128 - errortext = 255, 0, 0 + helptext = #5c5c5c + fadedtext = #6c6c6c + errortext = red -In the Main section you must at least define the ``name`` settings. + [Syntax] + background = #ffffff + text = #000000 + link = blue + headertext = green + headertag = green:160 + emphasis = orange + dialog = blue + altdialog = blue + note = yellow + hidden = faded + shortcode = green + keyword = red + tag = green + value = blue + optional = green + spellcheckline = red + errorline = green + replacetag = green + modifier = green + texthighlight = #c8c80060 -The Palette values correspond to the Qt enum values for ``QPalette::ColorRole``, see the -`Qt documentation `_ for more details. The -colour values are RGB numbers on the format ``r, g, b`` where each is an integer from ``0`` to -``255``. Omitted values are not loaded and will use default values. If the ``helptext`` colour is -not defined, it is computed as a colour between the ``window`` and ``windowtext`` colour. -Additional shades of some of the colours are also computed. These are mainly used for 3D effects. + +Theme Sections +-------------- + +.. _ColorRole: https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum + +The theme file is made up of different sections depending on what part of novelWriter the theme +affects. + +.. csv-table:: Theme Sections Overview + :header: "Section", "Description" + :class: "tight-table" + + "``[Main]``", "Meta data about the theme, You must at least set ``name`` and ``mode``, and ``mode`` must be either ``light`` or ``dark``." + "``[Base]``", "The base colours of the theme. These are also selectable colours in various places inside the app, like for icon colours in **Preferences**." + "``[Project]``", "The colours used for icons and markers for the different project item types." + "``[Palette]``", "The colours used for styling the user interface. The values correspond to the ColorRole_ values in the Qt library." + "``[GUI]``", "The colours used for styling additional elements of the user interface." + "``[Syntax]``", "The colours used for syntax highlighting in documents." + + +Colour Value Formats +-------------------- + +There are several ways to enter colour values: + +.. csv-table:: Colour Formats + :header: "Syntax", "Description" + :class: "tight-table" + + "``#RRGGBB``", "A CSS style hexadecimal values, like ``#ff0000`` for red." + "``#RRGGBBAA``", "A CSS style hexadecimal values with transparency, like ``#ff00007f`` for half-transparent red." + "``name``", "A name referring to one of the colours already specified under the ``[Base]`` section, like ``red``. Note that you should not use named colours in the ``[Base]`` section itself as that may have unintended results." + "``name:alpha``", "A name referring to one of the colours already specified under the ``[Base]`` section, with a transparency value added. The alpha value must be in the range ``0`` to ``255``, like ``red:127`` for half-transparent red." + "``r, g, b``", "A set of red, green and blue numbers in the range ``0`` to ``255``, like ``255, 0, 0`` for red." + "``r, g, b, a``", "A set of red, green, blue and alpha numbers in the range ``0`` to ``255``, like ``255, 0, 0, 127`` for half-transparent red." .. versionadded:: 2.5 The ``fadedtext`` and ``errortext`` theme colour entries were added. @@ -172,60 +165,84 @@ Additional shades of some of the colours are also computed. These are mainly use The ``icontheme`` setting was dropped as the icon theme is now its own setting. The ``[Icons]`` and ``[Project]`` sections were added, and the ``status*`` settings removed. +.. versionadded:: 2.8 + The ``[Syntax]`` section was moved into the main theme file. Previously, these settings were in + their own file. The ``[Icons]`` section was renamed to ``[Base]``. -Custom Syntax Theme -------------------- -A syntax theme ``.conf`` file consists of the following settings: +Icon Themes +=========== + +Icon themes are *not* straightforward to add, but if you want to make the effort, this section +describes how to do it. + +The existing icon themes are stored in +`novelwriter/assets/icons `_. + +As with colour themes, remember to change the name of your theme by modifying the ``name`` setting +at the top of the file, otherwise you may not be able to distinguish them in **Preferences**. + +For novelWriter to be able to locate the custom theme files, you must copy them to the +:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder +there ``icons`` for icon themes. These folders are created the first time you start novelWriter. + + +The Icons File Format +--------------------- + +Icon themes are kept in files with the ``.icons`` file extension. The file format is a custom +format with entries on the form ``section:key = value``. .. code-block:: cfg + :caption: The icons file for "Material Symbols - Rounded Medium" (truncated) - [Main] - name = My Syntax Theme - author = Jane Doe - credit = John Doe - url = https://example.com - license = CC BY-SA 4.0 - licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ + # Meta + meta:name = Material Symbols - Rounded Medium + meta:author = Google Inc + meta:license = Apache 2.0 - [Syntax] - background = 255, 255, 255 - text = 0, 0, 0 - link = 0, 0, 0 - headertext = 0, 0, 0 - headertag = 0, 0, 0 - emphasis = 0, 0, 0 - dialog = 0, 0, 0 - altdialog = 0, 0, 0 - note = 0, 0, 0 - hidden = 0, 0, 0 - shortcode = 0, 0, 0 - keyword = 0, 0, 0 - tag = 0, 0, 0 - value = 0, 0, 0 - optional = 0, 0, 0 - spellcheckline = 0, 0, 0 - errorline = 0, 0, 0 - replacetag = 0, 0, 0 - modifier = 0, 0, 0 - texthighlight = 255, 255, 255, 128 + # Icons + icon:alert_error = + icon:alert_info = + icon:alert_question = + icon:alert_warn = + icon:cls_archive = + icon:cls_character = + icon:cls_custom = + icon:cls_entity = + icon:cls_none = + icon:cls_novel = + icon:cls_object = + icon:cls_plot = + icon:cls_template = + icon:cls_timeline = + icon:cls_trash = + icon:cls_world = -In the Main section, you must define at least the ``name`` setting. The Syntax colour values are -RGB(A) numbers of the format ``r, g, b, a`` where each is an integer from ``0`` to ``255``. The -fourth value is the alpha channel, which can be omitted. +The icon keys are associated with icon placement locations inside novelWriter, and the template for +them is defined in the script that generates the default icon themes. -Omitted syntax colours default to black, except ``background`` which defaults to white, and -``texthighlight`` which defaults to white with half transparency. +The script can be found under +`utils/icon_themes.py `__ +in the source code. -.. versionadded:: 2.2 - The ``shortcode`` syntax colour entry was added. +This file includes all the code needed to generate the themes that are included in novelWriter. The +icon keys are mapped to icon keys from the specific themes in JSON files in the ``icon_themes`` +folder next to the script. This is the recommended way to generate these themes. Doing it manually +is not advisable. -.. versionadded:: 2.3 - The ``optional`` syntax colour entry was added. -.. versionadded:: 2.4 - The ``texthighlight`` syntax colour entry was added. +Icon Value Format +----------------- -.. versionadded:: 2.5 - The ``dialog``, ``altdialog``, ``note`` and ``tag`` syntax colour entries were added. - ``straightquotes``, ``doublequotes`` and ``singlequotes`` were removed. +As can be seen from the example, an icon is defined in the ``icon`` section with a key and an +in-line SVG XML block. The XML must fit on one line and obey the following rules: + +#. It must be single colour, that is, the fill colour attribute must be able to colourise the + entire icon. +#. The fill colour attribute *must* be defined and must be set to: ``fill="#000000"``. This value + is replaced by the relevant theme colour when the icon is processed in novelWriter. + +.. versionadded:: 2.7 + The icon theme files were added. Previously, icons were stored as individual SVG files with a + config file mapping the file names to the internal icon keys. diff --git a/docs/source/more/dictionaries.rst b/docs/source/more/dictionaries.rst new file mode 100644 index 00000000..1ca896d5 --- /dev/null +++ b/docs/source/more/dictionaries.rst @@ -0,0 +1,55 @@ +.. _docs_more_dictionaries: + +************************ +Spell Check Dictionaries +************************ + +.. _Enchant: https://rrthomas.github.io/enchant/ +.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/ + +Spell checking is provided by the Enchant_ library. Depending on your operating system, it may or +may not load all installed spell check dictionaries automatically. + + +Linux and MacOS +=============== + +On Linux and MacOS, you generally only have to install hunspell, aspell or myspell dictionaries on +your system like you do for other applications. See your distro or OS documentation for how to do +this. These dictionaries should show up as available spell check languages in novelWriter. + + +Windows +======= + +For Windows, English is included with the installation. For other languages you have to download +and add dictionaries yourself. + +**Install Tool** + +A small tool to assist with this can be found under **Tools > Add Dictionaries**. It will import +spell checking dictionaries from Free Office or Libre Office extensions. The dictionaries are then +installed in the install location for the Enchant library and should thus work for any application +that uses Enchant for spell checking. + +**Manual Install** + +If you prefer to do this manually or want to use a different source than the ones mentioned above, +You need to get compatible dictionary files for your language. You need two files files ending with +``.aff`` and ``.dic``. These files must then be copied to the following location: + +``C:\Users\\AppData\Local\enchant\hunspell`` + +This assumes your user profile is stored at ``C:\Users\``. The last one or two folders may +not exist, so you may need to create them. + +You can find the various dictionaries on the `Free Desktop`_ website. + +.. note:: + + The Free Desktop link points to a repository, and what may look like file links inside the + dictionary folder are actually links to web pages. If you right-click and download those, you + get HTML files, not dictionaries! + + In order to download the actual dictionary files, right-click the "plain" label at the end of + each line and download that. diff --git a/docs/source/user_interface/editor_viewer.rst b/docs/source/user_interface/editor_viewer.rst index 841be0b0..ee0c1eb3 100644 --- a/docs/source/user_interface/editor_viewer.rst +++ b/docs/source/user_interface/editor_viewer.rst @@ -55,7 +55,7 @@ edited from the **Project Word List** tool available from the **Tools** menu. .. note:: Generally, spell checking dictionaries are collected from your operating system, but on Windows - they are not. See :ref:`docs_more_custom_dict` for how to add spell checking dictionaries on + they are not. See :ref:`docs_more_dictionaries` for how to add spell checking dictionaries on Windows. From 638f93499bfd80384331ff24e28f7a35b1dd50b6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:31:52 +0200 Subject: [PATCH 17/20] Scan themes on startup instead --- novelwriter/__init__.py | 2 +- novelwriter/config.py | 4 +- novelwriter/dialogs/preferences.py | 14 +- novelwriter/gui/theme.py | 267 ++++++++++++++------------- novelwriter/guimain.py | 3 +- novelwriter/shared.py | 1 + tests/test_gui/test_gui_statusbar.py | 2 +- tests/test_gui/test_gui_theme.py | 25 ++- 8 files changed, 168 insertions(+), 150 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 4905f4b3..acc73ee6 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -49,7 +49,7 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.8 Alpha 0" +__version__ = "2.8a0" __hexversion__ = "0x020800a0" __date__ = "2025-06-01" __status__ = "Stable" diff --git a/novelwriter/config.py b/novelwriter/config.py index db173019..404391b1 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -405,7 +405,7 @@ class Config: else: font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) self.guiFont = fontMatcher(font) - logger.debug("GUI font set to: %s", describeFont(font)) + logger.debug("Main font set to: %s", describeFont(font)) QApplication.setFont(self.guiFont) return @@ -723,7 +723,7 @@ class Config: # Check Values # ============ - self._prepareFont(self.guiFont, "GUI") + self._prepareFont(self.guiFont, "main") self._prepareFont(self.textFont, "document") # If we're using straight quotes, disable auto-replace diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 9587db6a..3d284c2e 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -169,11 +169,12 @@ class GuiPreferences(NDialog): self.lightTheme.setMinimumWidth(200) self.darkTheme = NComboBox(self) self.darkTheme.setMinimumWidth(200) - for theme, name, dark in SHARED.theme.listThemes(): - if dark: - self.darkTheme.addItem(name, theme) + for key, theme in SHARED.theme.colourThemes.items(): + if theme.dark: + self.darkTheme.addItem(theme.name, key) else: - self.lightTheme.addItem(name, theme) + self.lightTheme.addItem(theme.name, key) + self.lightTheme.setCurrentData(CONFIG.lightTheme, DEF_GUI_LIGHT) self.darkTheme.setCurrentData(CONFIG.darkTheme, DEF_GUI_DARK) @@ -189,8 +190,9 @@ class GuiPreferences(NDialog): # Icon Theme self.iconTheme = NComboBox(self) self.iconTheme.setMinimumWidth(200) - for theme, name in SHARED.theme.iconCache.listThemes(): - self.iconTheme.addItem(name, theme) + for key, theme in SHARED.theme.iconCache.iconThemes.items(): + self.iconTheme.addItem(theme.name, key) + self.iconTheme.setCurrentData(CONFIG.iconTheme, DEF_ICONS) self.mainForm.addRow( diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index b5e2d2f2..463a8dc0 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -27,6 +27,7 @@ from __future__ import annotations import logging from configparser import ConfigParser +from dataclasses import dataclass from math import ceil from typing import TYPE_CHECKING, Final @@ -50,13 +51,19 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -T_ThemeEntry = tuple[str, str, bool] - STYLES_FLAT_TABS = "flatTabWidget" STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" +@dataclass +class ThemeEntry: + + name: str + dark: bool + path: Path + + class ThemeMeta: name: str = "" @@ -100,20 +107,18 @@ class GuiTheme: """ __slots__ = ( - "_availSyntax", "_availThemes", "_currentTheme", "_darkThemes", "_guiPalette", - "_lightThemes", "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "_themeList", - "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", - "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", + "_allThemes", "_currentTheme", "_darkThemes", "_guiPalette", "_lightThemes", "_meta", + "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "baseButtonHeight", + "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText", + "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth", - "themeMeta", ) def __init__(self) -> None: # Theme Objects - self.themeMeta = ThemeMeta() self.iconCache = GuiIcons(self) self.syntaxTheme = SyntaxColors() self.isDarkTheme = False @@ -123,12 +128,11 @@ class GuiTheme: self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) - # Load Themes + # Theme Data + self._meta = ThemeMeta() self._currentTheme = "" self._guiPalette = QPalette() - self._themeList: list[T_ThemeEntry] = [] - self._availThemes: dict[str, Path] = {} - self._availSyntax: dict[str, Path] = {} + self._allThemes: dict[str, ThemeEntry] = {} self._styleSheets: dict[str, str] = {} self._svgColors: dict[str, bytes] = {} self._qColors: dict[str, QColor] = {} @@ -180,16 +184,19 @@ class GuiTheme: logger.debug("Text 'N' Height: %d", self.textNHeight) logger.debug("Text 'N' Width: %d", self.textNWidth) - # Process Themes - _listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf") - _listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf") - - self.loadTheme() - return ## - # Methods + # Properties + ## + + @property + def colourThemes(self) -> dict[str, ThemeEntry]: + """Return a dictionary of all themes.""" + return self._allThemes + + ## + # Getters ## def getTextWidth(self, text: str, font: QFont | None = None) -> int: @@ -214,6 +221,19 @@ class GuiTheme: # Theme Methods ## + def initThemes(self) -> None: + """Initialise themes.""" + CONFIG.splashMessage("Scanning for colour themes ...") + themes: list[Path] = [] + _listContent(themes, CONFIG.assetPath("themes"), ".conf") + _listContent(themes, CONFIG.dataPath("themes"), ".conf") + self._scanThemes(themes) + + self.iconCache.initIcons() + self.loadTheme() + + return + def isDesktopDarkMode(self) -> bool: """Check if the desktop is in dark mode.""" if CONFIG.verQtValue >= 0x060500 and (hint := QGuiApplication.styleHints()): @@ -261,8 +281,8 @@ class GuiTheme: darkMode = self.isDesktopDarkMode() theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme - if theme not in self._availThemes: - logger.error("Could not find GUI theme '%s'", theme) + if theme not in self._allThemes: + logger.error("Could not find theme for key '%s'", theme) if darkMode: theme = DEF_GUI_DARK CONFIG.darkTheme = DEF_GUI_DARK @@ -274,18 +294,19 @@ class GuiTheme: logger.info("Theme '%s' is already loaded", theme) return False - if not (file := self._availThemes.get(theme)): + entry = self._allThemes.get(theme) + if not entry: logger.error("Could not load GUI theme") return False - CONFIG.splashMessage("Loading GUI theme ...") + CONFIG.splashMessage(f"Loading colour theme: {entry.name}") logger.info("Loading GUI theme '%s'", theme) parser = ConfigParser() try: - with open(file, mode="r", encoding="utf-8") as fo: + with open(entry.path, mode="r", encoding="utf-8") as fo: parser.read_file(fo) except Exception: - logger.error("Could not read file: %s", file) + logger.error("Could not read file: %s", entry.path) logException() return False @@ -305,9 +326,9 @@ class GuiTheme: meta.license = parser.get(sec, "license", fallback="N/A") meta.licenseUrl = parser.get(sec, "licenseurl", fallback="") - self.themeMeta = meta + self._meta = meta - # Icons + # Base sec = "Base" if parser.has_section(sec): self._setBaseColor("default", self._readColor(parser, sec, "default")) @@ -447,35 +468,18 @@ class GuiTheme: self._svgColors["scene"] = color self._svgColors["note"] = color + self.isDarkTheme = darkMode + self._currentTheme = theme + # Load icons after the theme is parsed self.iconCache.loadTheme(CONFIG.iconTheme) # Finalise - self.isDarkTheme = darkMode QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) - self._currentTheme = theme - CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}") - return True - def listThemes(self) -> list[T_ThemeEntry]: - """Scan the GUI themes folder and list all themes.""" - if self._themeList: - return self._themeList - - themes: list[T_ThemeEntry] = [] - parser = ConfigParser() - for key, path in self._availThemes.items(): - logger.debug("Checking theme config '%s'", key) - if meta := _loadInternalName(parser, path): - themes.append((key, meta[0], meta[1])) - - self._themeList = sorted(themes, key=_sortTheme) - - return self._themeList - def getStyleSheet(self, name: str) -> str: """Load a standard style sheet.""" return self._styleSheets.get(name, "") @@ -493,12 +497,10 @@ class GuiTheme: def _resetTheme(self) -> None: """Reset GUI colours to default values.""" palette = QPalette() - - text = palette.color(QPalette.ColorRole.Text) - window = palette.color(QPalette.ColorRole.Window) - isDark = text.lightnessF() > window.lightnessF() + isDark = self.isDesktopDarkMode() # Reset GUI Palette + default = palette.color(QPalette.ColorRole.Text) faded = QColor(128, 128, 128) dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41) @@ -520,7 +522,7 @@ class GuiTheme: self.iconCache.clear() self._svgColors = {} self._qColors = {} - self._setBaseColor("default", text) + self._setBaseColor("default", default) self._setBaseColor("faded", faded) self._setBaseColor("red", red) self._setBaseColor("orange", orange) @@ -531,7 +533,7 @@ class GuiTheme: self._setBaseColor("purple", purple) self._setBaseColor("root", blue) self._setBaseColor("folder", yellow) - self._setBaseColor("file", text) + self._setBaseColor("file", default) self._setBaseColor("title", green) self._setBaseColor("chapter", red) self._setBaseColor("scene", blue) @@ -582,6 +584,35 @@ class GuiTheme: return + def _scanThemes(self, files: list[Path]) -> None: + """Scan the GUI themes folder and list all themes.""" + parser = ConfigParser() + data: dict[str, tuple[str, str, bool, Path]] = {} + keys = [] + for file in files: + try: + parser.clear() + parser.read(file, encoding="utf-8") + name = parser.get("Main", "name", fallback="") + dark = parser.get("Main", "mode", fallback="light").lower() == "dark" + if name: + key = file.stem + prefix = "*" if key.startswith("default") else "" + lookup = f"{prefix}{name} {key}" + keys.append(lookup) + data[lookup] = (file.stem, name, dark, file) + except Exception: # noqa: PERF203 + logger.error("Could not read file: %s", file) + logException() + + self._allThemes = {} + for lookup in sorted(keys): + key, name, dark, item = data[lookup] + logger.debug("Checking theme config '%s'", key) + self._allThemes[key] = ThemeEntry(name, dark, item) + + return + class GuiIcons: """The icon class manages the content of the assets/icons folder, @@ -593,8 +624,8 @@ class GuiIcons: """ __slots__ = ( - "_availThemes", "_headerDec", "_headerDecNarrow", "_meta", "_noIcon", - "_qIcons", "_svgData", "_theme", "_themeList", + "_allThemes", "_headerDec", "_headerDecNarrow", "_meta", + "_noIcon", "_qIcons", "_svgData", "_theme", ) TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = { @@ -612,21 +643,15 @@ class GuiIcons: self._meta = ThemeMeta() # Storage + self._allThemes: dict[str, ThemeEntry] = {} self._svgData: dict[str, bytes] = {} self._qIcons: dict[str, QIcon] = {} self._headerDec: list[QPixmap] = [] self._headerDecNarrow: list[QPixmap] = [] - # Icon Theme Path - self._availThemes: dict[str, Path] = {} - self._themeList: list[tuple[str, str]] = [] - # None Icon self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg")) - _listConf(self._availThemes, CONFIG.assetPath("icons"), ".icons") - _listConf(self._availThemes, CONFIG.dataPath("icons"), ".icons") - return def clear(self) -> None: @@ -638,29 +663,48 @@ class GuiIcons: self._meta = ThemeMeta() return + ## + # Properties + ## + + @property + def iconThemes(self) -> dict[str, ThemeEntry]: + """Return a dictionary of all icon themes.""" + return self._allThemes + ## # Actions ## + def initIcons(self) -> None: + """Initialise icons.""" + CONFIG.splashMessage("Scanning for icon themes ...") + icons: list[Path] = [] + _listContent(icons, CONFIG.assetPath("icons"), ".icons") + _listContent(icons, CONFIG.dataPath("icons"), ".icons") + self._scanThemes(icons) + return + def loadTheme(self, theme: str) -> bool: """Update the theme map. This is more of an init, since many of the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. """ - if theme not in self._availThemes: + if theme not in self._allThemes: logger.error("Could not find icon theme '%s'", theme) theme = DEF_ICONS CONFIG.iconTheme = theme - if not (file := self._availThemes.get(theme)): + entry = self._allThemes.get(theme) + if not entry: logger.error("Could not load icon theme") return False - CONFIG.splashMessage("Loading icon theme ...") + CONFIG.splashMessage(f"Loading icon theme: {entry.name}") logger.info("Loading icon theme '%s'", theme) try: meta = ThemeMeta() - with open(file, mode="r", encoding="utf-8") as icons: + with open(entry.path, mode="r", encoding="utf-8") as icons: for icon in icons: bits = icon.partition("=") key = bits[0].strip() @@ -676,14 +720,12 @@ class GuiIcons: meta.license = value self._meta = meta except Exception: - logger.error("Could not read file: %s", file) + logger.error("Could not read file: %s", entry.path) logException() return False - CONFIG.splashMessage(f"Loaded icon theme: {meta.name}") - CONFIG.splashMessage("Generating additional icons ...") - # Populate generated icons cache + CONFIG.splashMessage("Generating additional icons ...") self.getHeaderDecoration(0) self.getHeaderDecorationNarrow(0) @@ -811,21 +853,6 @@ class GuiIcons: ] return self._headerDecNarrow[minmax(hLevel, 0, 5)] - def listThemes(self) -> list[tuple[str, str]]: - """Scan the GUI icons folder and list all themes.""" - if self._themeList: - return self._themeList - - themes = [] - for key, path in self._availThemes.items(): - logger.debug("Checking icon theme '%s'", key) - if name := _loadIconName(path): - themes.append((key, name)) - - self._themeList = sorted(themes, key=_sortTheme) - - return self._themeList - ## # Internal Functions ## @@ -870,49 +897,39 @@ class GuiIcons: tMode = Qt.TransformationMode.SmoothTransformation return pixmap.scaledToHeight(height, tMode) + def _scanThemes(self, entries: list[Path]) -> None: + """Scan the GUI themes folder and list all themes.""" + data: dict[str, tuple[str, str, Path]] = {} + keys = [] + for entry in entries: + try: + with open(entry, mode="r", encoding="utf-8") as fo: + for line in fo: + key, _, value = line.partition("=") + if key.strip() == "meta:name": + if name := value.strip(): + lookup = entry.stem + keys.append(lookup) + data[lookup] = (lookup, name, entry) + break + except Exception: + logger.error("Could not read file: %s", entry) + logException() + + self._allThemes = {} + for lookup in sorted(keys): + key, name, item = data[lookup] + logger.debug("Checking icon theme '%s'", key) + self._allThemes[key] = ThemeEntry(name, False, item) + + return + # Module Functions # ================ -def _listConf(target: dict, path: Path, extension: str) -> None: - """Scan for theme files and populate the dictionary.""" +def _listContent(data: list[Path], path: Path, extension: str) -> None: + """List files of a specific type and extend the list.""" if path.is_dir(): - for item in path.iterdir(): - if item.is_file() and item.name.endswith(extension): - target[item.stem] = item + data.extend(n for n in path.iterdir() if n.is_file() and n.suffix == extension) return - - -def _sortTheme(data: tuple) -> str: - """Key function for theme sorting.""" - key, name = data[:2] - return f"*{name}" if key.startswith("default_") else name - - -def _loadInternalName(parser: ConfigParser, path: str | Path) -> tuple[str, bool]: - """Open a conf file and read the 'name' setting.""" - try: - parser.clear() - with open(path, mode="r", encoding="utf-8") as inFile: - parser.read_file(inFile) - name = parser.get("Main", "name", fallback="") - dark = parser.get("Main", "mode", fallback="light").lower() == "dark" - return name, dark - except Exception: - logger.error("Could not read file: %s", path) - logException() - return "", False - - -def _loadIconName(path: Path) -> str: - """Open an icons file and read the name setting.""" - try: - with open(path, mode="r", encoding="utf-8") as icons: - for icon in icons: - key, _, value = icon.partition("=") - if key.strip() == "meta:name": - return value.strip() - except Exception: - logger.error("Could not read file: %s", path) - logException() - return "" diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 41edcf3e..25f997f6 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -937,8 +937,7 @@ class GuiMain(QMainWindow): def changeEvent(self, event: QEvent) -> None: """Capture application change events.""" - if int(event.type()) == 210: - # ThemeChange + if int(event.type()) == 210: # ThemeChange self.checkThemeUpdate() return diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 228b9678..c5b7025c 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -175,6 +175,7 @@ class SharedData(QObject): is created. """ self._theme = theme + self._theme.initThemes() return def initSharedData(self, gui: GuiMain) -> None: diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index a4d5d91f..efb19f48 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -31,7 +31,7 @@ from tests.tools import C, buildTestProject @pytest.mark.gui def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): - """Test the the various features of the status bar.""" + """Test the various features of the status bar.""" buildTestProject(nwGUI, projPath) cHandle = SHARED.project.newFile("A Note", C.hCharRoot) newDoc = SHARED.project.storage.getDocument(cHandle) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index f57a2c30..84ee736e 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -33,7 +33,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.config import DEF_GUI_LIGHT from novelwriter.constants import nwLabels from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType -from novelwriter.gui.theme import _listConf +from novelwriter.gui.theme import _listContent from tests.mocked import causeOSError from tests.tools import writeFile @@ -54,18 +54,17 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): # Scan for Themes # =============== - result = {} - _listConf({}, Path("not_a_path"), ".conf") - assert result == {} + result = [] + _listContent(result, Path("not_a_path"), ".conf") + assert result == [] themeOne = tstPaths.cnfDir / "themes" / "themeone.conf" themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf" writeFile(themeOne, "# Stuff") writeFile(themeTwo, "# Stuff") - _listConf(result, tstPaths.cnfDir / "themes", ".conf") - assert result["themeone"] == themeOne - assert result["themetwo"] == themeTwo + _listContent(result, tstPaths.cnfDir / "themes", ".conf") + assert result == [themeOne, themeTwo] # Parse Colours # ============= @@ -157,17 +156,17 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) theme._themeList = [] - assert theme.listThemes() == [] + assert theme.getColourThemes() == [] # Load the theme info, default themes first - themesList = theme.listThemes() + themesList = theme.getColourThemes() assert themesList[0] == ("default_dark", "Default Dark Theme") assert themesList[1] == ("default_light", "Default Light Theme") assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night") assert themesList[3] == ("dracula", "Dracula") # A second call should returned the cached list - assert theme.listThemes() == theme._themeList + assert theme.getColourThemes() == theme._themeList # Check handling of broken theme settings CONFIG.guiTheme = "not_a_theme" @@ -376,18 +375,18 @@ def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths): # Load error returns empty list with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - themes = iconCache.listThemes() + themes = iconCache.getIconThemes() assert themes == [] # Successful read - themes = iconCache.listThemes() + themes = iconCache.getIconThemes() assert len(themes) > 1 assert "material_rounded_normal" in dict(themes) # Load error doesn't matter on second read since list is cached with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - assert iconCache.listThemes() == themes + assert iconCache.getIconThemes() == themes # qtbot.stop() From 5fee3ecdb89a730acbe4f2ba0fb92ebb59ecdc89 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:39:21 +0200 Subject: [PATCH 18/20] Fix broken tests --- tests/mocked.py | 3 +++ tests/test_dialogs/test_dlg_preferences.py | 13 +++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/mocked.py b/tests/mocked.py index d28cdbba..e1e4097c 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -65,6 +65,9 @@ class MockTheme: self.guiFontBU = QFont() return + def initThemes(self) -> None: + return + def getPixmap(self, *a) -> QPixmap: return QPixmap() diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index ccb77133..1c54da98 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -31,6 +31,7 @@ from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT from novelwriter.constants import nwUnicode from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.quotes import GuiQuoteSelect +from novelwriter.gui.theme import ThemeEntry from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone KEY_DELAY = 1 @@ -147,12 +148,12 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): (fncPath / "nw_en_US.qm").touch() (fncPath / "project_en_US.json").touch() CONFIG._nwLangPath = fncPath - SHARED.theme._themeList = [ - ("theme1", "Theme 1", False), - ("theme2", "Theme 2", False), - ("theme3", "Theme 3", True), - ("theme4", "Theme 4", True), - ] + SHARED.theme._allThemes = { + "theme1": ThemeEntry(name="Theme 1", dark=False, path=fncPath), + "theme2": ThemeEntry(name="Theme 2", dark=False, path=fncPath), + "theme3": ThemeEntry(name="Theme 3", dark=True, path=fncPath), + "theme4": ThemeEntry(name="Theme 4", dark=True, path=fncPath), + } prefs = GuiPreferences(nwGUI) with qtbot.waitExposed(prefs): From bd2353f7bce78bae7aa7f7b2c85971ff5dfb7362 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:36:53 +0200 Subject: [PATCH 19/20] Add tests for sidebar and themes --- novelwriter/gui/theme.py | 43 +- tests/test_gui/test_gui_sidebar.py | 53 +++ tests/test_gui/test_gui_theme.py | 626 +++++++++++++++-------------- 3 files changed, 389 insertions(+), 333 deletions(-) create mode 100644 tests/test_gui/test_gui_sidebar.py diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 463a8dc0..5c7533ae 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -240,8 +240,8 @@ class GuiTheme: return hint.colorScheme() == Qt.ColorScheme.Dark palette = QPalette() - text = palette.color(QPalette.ColorRole.WindowText) - window = palette.color(QPalette.ColorRole.Window) + text = palette.windowText().color() + window = palette.window().color() return text.lightnessF() > window.lightnessF() def parseColor(self, value: str, default: QColor = QtBlack) -> QColor: @@ -270,7 +270,7 @@ class GuiTheme: return QColor(*result) return default - def loadTheme(self, force: bool = False) -> bool: + def loadTheme(self, force: bool = False) -> None: """Load the currently specified GUI theme.""" match CONFIG.themeMode: case nwTheme.LIGHT: @@ -282,7 +282,7 @@ class GuiTheme: theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme if theme not in self._allThemes: - logger.error("Could not find theme for key '%s'", theme) + logger.error("Could not find theme '%s'", theme) if darkMode: theme = DEF_GUI_DARK CONFIG.darkTheme = DEF_GUI_DARK @@ -292,23 +292,22 @@ class GuiTheme: if theme == self._currentTheme and not force: logger.info("Theme '%s' is already loaded", theme) - return False + return entry = self._allThemes.get(theme) if not entry: logger.error("Could not load GUI theme") - return False + return CONFIG.splashMessage(f"Loading colour theme: {entry.name}") logger.info("Loading GUI theme '%s'", theme) parser = ConfigParser() try: - with open(entry.path, mode="r", encoding="utf-8") as fo: - parser.read_file(fo) + parser.read(entry.path, encoding="utf-8") except Exception: logger.error("Could not read file: %s", entry.path) logException() - return False + return # Reset Palette self._resetTheme() @@ -458,15 +457,15 @@ class GuiTheme: # Set project override colours if (override := CONFIG.iconColTree) != "theme": - color = self._svgColors.get(override, b"#000000") - self._svgColors["root"] = color - self._svgColors["folder"] = color + color = self._qColors.get(override, QtBlack) + self._setBaseColor("root", color) + self._setBaseColor("folder", color) if not CONFIG.iconColDocs: - self._svgColors["file"] = color - self._svgColors["title"] = color - self._svgColors["chapter"] = color - self._svgColors["scene"] = color - self._svgColors["note"] = color + self._setBaseColor("file", color) + self._setBaseColor("title", color) + self._setBaseColor("chapter", color) + self._setBaseColor("scene", color) + self._setBaseColor("note", color) self.isDarkTheme = darkMode self._currentTheme = theme @@ -478,7 +477,7 @@ class GuiTheme: QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) - return True + return def getStyleSheet(self, name: str) -> str: """Load a standard style sheet.""" @@ -685,7 +684,7 @@ class GuiIcons: self._scanThemes(icons) return - def loadTheme(self, theme: str) -> bool: + def loadTheme(self, theme: str) -> None: """Update the theme map. This is more of an init, since many of the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. @@ -698,7 +697,7 @@ class GuiIcons: entry = self._allThemes.get(theme) if not entry: logger.error("Could not load icon theme") - return False + return CONFIG.splashMessage(f"Loading icon theme: {entry.name}") logger.info("Loading icon theme '%s'", theme) @@ -722,14 +721,14 @@ class GuiIcons: except Exception: logger.error("Could not read file: %s", entry.path) logException() - return False + return # Populate generated icons cache CONFIG.splashMessage("Generating additional icons ...") self.getHeaderDecoration(0) self.getHeaderDecorationNarrow(0) - return True + return ## # Access Functions diff --git a/tests/test_gui/test_gui_sidebar.py b/tests/test_gui/test_gui_sidebar.py new file mode 100644 index 00000000..45e372db --- /dev/null +++ b/tests/test_gui/test_gui_sidebar.py @@ -0,0 +1,53 @@ +""" +novelWriter – Side Bar Class Tester +=================================== + +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 pytest + +from novelwriter import CONFIG +from novelwriter.constants import nwLabels +from novelwriter.enum import nwTheme + + +@pytest.mark.gui +def testGuiSideBar_CycleColourTheme(nwGUI): + """Test theme cycle feature on the side bar.""" + CONFIG.themeMode = nwTheme.AUTO + sidebar = nwGUI.sideBar + sidebar.mainGui.checkThemeUpdate = lambda *a: None + + # Run 3 Cycles + for _ in range(3): + + # Cycle Light + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.LIGHT + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.LIGHT] + + # Cycle Dark + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.DARK + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.DARK] + + # Cycle Auto + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.AUTO + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.AUTO] diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 84ee736e..107fd125 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -20,99 +20,31 @@ along with this program. If not, see . """ from __future__ import annotations -import sys - from configparser import ConfigParser from pathlib import Path +from unittest.mock import MagicMock, Mock import pytest -from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor, QFont, QFontDatabase, QIcon, QPalette, QPixmap, QStyleHints -from novelwriter import CONFIG, SHARED -from novelwriter.config import DEF_GUI_LIGHT +from novelwriter import CONFIG +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS from novelwriter.constants import nwLabels -from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType -from novelwriter.gui.theme import _listContent +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme +from novelwriter.gui.theme import ( + STYLES_BIG_TOOLBUTTON, STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON, GuiTheme, + ThemeMeta, _listContent +) from tests.mocked import causeOSError -from tests.tools import writeFile @pytest.mark.gui -def testGuiTheme_Main(qtbot, nwGUI, tstPaths): - """Test the theme class init.""" - theme = SHARED.theme - - # Methods - # ======= - - mSize = theme.getTextWidth("m") - assert mSize > 0 - assert theme.getTextWidth("m", theme.guiFont) == mSize - - # Scan for Themes - # =============== - - result = [] - _listContent(result, Path("not_a_path"), ".conf") - assert result == [] - - themeOne = tstPaths.cnfDir / "themes" / "themeone.conf" - themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf" - writeFile(themeOne, "# Stuff") - writeFile(themeTwo, "# Stuff") - - _listContent(result, tstPaths.cnfDir / "themes", ".conf") - assert result == [themeOne, themeTwo] - - # Parse Colours - # ============= - - parser = ConfigParser() - parser["Palette"] = { - "colour1": "100, 150, 200", # Valid - "colour2": "100, 150, 200, 250", # With alpha - "colour3": "100, 150, 200, 250, 300", # Too many values - "colour4": "250, 250", # Missing blue - "colour5": "-10, 127, 300", # Invalid red and blue - "colour6": "bob, 127, 255", # Invalid red - } - - # Test the parser for several valid and invalid values - assert theme._readColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255) - assert theme._readColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250) - assert theme._readColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250) - assert theme._readColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255) - assert theme._readColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0) - assert theme._readColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255) - - # The palette should load with the parsed values - theme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255) - theme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - theme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - theme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255) - theme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0) - theme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255) - - # Non-existing value should return default colour - theme._setBaseColor("default", QColor(64, 64, 64, 255)) - theme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (64, 64, 64, 255) - - # qtbot.stop() - - -@pytest.mark.gui -def testGuiTheme_ParseColor(qtbot, nwGUI): +def testGuiTheme_ParseColor(): """Test the colour parsing.""" - theme = SHARED.theme + theme = GuiTheme() # Pre-Populate theme._qColors["red"] = QColor(255, 0, 0) @@ -144,258 +76,333 @@ def testGuiTheme_ParseColor(qtbot, nwGUI): @pytest.mark.gui -@pytest.mark.skip -def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): - """Test the theme part of the class.""" - theme = SHARED.theme +def testGuiTheme_ScanThemes(monkeypatch): + """Test the themes scanning.""" + theme = GuiTheme() - # List Themes + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("themes"), ".conf") + assert len(files) > 0 + + # Block reading theme files + with monkeypatch.context() as mp: + mp.setattr(ConfigParser, "read", causeOSError) + theme._scanThemes(files) + assert theme.colourThemes == {} + + # Read all themes correctly + theme._scanThemes(files) + assert len(theme.colourThemes) > 0 + + dark = theme.colourThemes[DEF_GUI_DARK] + light = theme.colourThemes[DEF_GUI_LIGHT] + + assert dark.name == "Default Dark Theme" + assert dark.dark is True + assert light.name == "Default Light Theme" + assert light.dark is False + + +@pytest.mark.gui +def testGuiTheme_LoadThemes(monkeypatch): + """Test loading themes.""" + theme = GuiTheme() + theme.iconCache = MagicMock() + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.darkTheme = DEF_GUI_DARK + + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("themes"), ".conf") + theme._scanThemes(files) + assert DEF_GUI_LIGHT in theme._allThemes + assert DEF_GUI_DARK in theme._allThemes + assert theme._currentTheme == "" + + # Load light theme + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + # Load dark theme + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Let auto switch back to light, then dark + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light) + CONFIG.themeMode = nwTheme.AUTO + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark) + CONFIG.themeMode = nwTheme.AUTO + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Error Cases # =========== - # Block the reading of the files + # Invalid light theme + CONFIG.lightTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + # Invalid dark theme + CONFIG.darkTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Clear meta and check exit early cases + theme._meta = ThemeMeta() + assert theme._meta.name == "" + + # Reload dark should not load anything + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._meta.name == "" + + # Force reload, but fail parsing with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - theme._themeList = [] - assert theme.getColourThemes() == [] + mp.setattr(ConfigParser, "read", causeOSError) + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Load the theme info, default themes first - themesList = theme.getColourThemes() - assert themesList[0] == ("default_dark", "Default Dark Theme") - assert themesList[1] == ("default_light", "Default Light Theme") - assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night") - assert themesList[3] == ("dracula", "Dracula") + # Invalid theme, and defaults are missing + del theme._allThemes[DEF_GUI_DARK] + del theme._allThemes[DEF_GUI_LIGHT] - # A second call should returned the cached list - assert theme.getColourThemes() == theme._themeList + CONFIG.lightTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Check handling of broken theme settings - CONFIG.guiTheme = "not_a_theme" - availThemes = theme._availThemes - theme._availThemes = {} - assert theme.loadTheme() is False - theme._availThemes = availThemes + CONFIG.darkTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Check handling of unreadable file - CONFIG.guiTheme = DEF_GUI_LIGHT - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert theme.loadTheme() is False - # Load Default Theme - # ================== +@pytest.mark.gui +def testGuiTheme_SpecialColors(tstPaths): + """Test handling special cases for colours.""" + theme = GuiTheme() + theme.iconCache = MagicMock() - if sys.platform != "win32": - # Set a mock colour for the window background - theme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0) - - # Load the default theme - CONFIG.guiTheme = DEF_GUI_LIGHT - assert theme.loadTheme() is True - - # This should load a standard palette - wCol = QPalette().color(QPalette.ColorRole.Window).getRgb() - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol - - # Mock Dark Theme - # =============== - - mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf" - mockTheme.write_text( + testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf" + testTheme.write_text( "[Main]\n" "name = Test\n" + "mode = light\n" + "\n" + "[Base]\n" + "default = #cccccc\n" + "faded = #949494\n" + "red = #ff0000\n" + "orange = #ff7f00\n" + "yellow = #ffff00\n" + "green = #00ff00\n" + "cyan = #00ffff\n" + "blue = #0000ff\n" + "purple = #ff00ff\n" + "\n" + "[Project]\n" + "root = blue\n" + "folder = yellow\n" + "file = default\n" + "title = green\n" + "chapter = red\n" + "scene = blue\n" + "note = yellow\n" "\n" "[Palette]\n" - "window = 0, 0, 0\n" - "text = 255, 255, 255\n" + "window = #000000\n" + "text = #ffffff\n" ) - theme._availThemes["test"] = mockTheme + theme._scanThemes([testTheme]) + assert len(theme.colourThemes) == 1 + CONFIG.themeMode = nwTheme.LIGHT + CONFIG.lightTheme = "test" - CONFIG.guiTheme = "test" - assert theme.loadTheme() is True - assert theme._guiPalette.window().color().getRgb() == (0, 0, 0, 255) - assert theme._guiPalette.text().color().getRgb() == (255, 255, 255, 255) + # Load theme + theme.loadTheme() + + # Since window is black, a lighter version should be generated assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255) - assert theme.isDarkTheme is True - # Load Default Light Theme - # ======================== + # Reload with project override to red + CONFIG.iconColTree = "red" + CONFIG.iconColDocs = True + theme.loadTheme(force=True) - CONFIG.guiTheme = "default_light" - assert theme.loadTheme() is True + assert theme.getBaseColor("root").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("folder").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("file").getRgb() == (204, 204, 204, 255) + assert theme.getBaseColor("title").getRgb() == (0, 255, 0, 255) + assert theme.getBaseColor("chapter").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("scene").getRgb() == (0, 0, 255, 255) + assert theme.getBaseColor("note").getRgb() == (255, 255, 0, 255) - # Check a few values - assert theme._guiPalette.color( - QPalette.ColorRole.Window - ).getRgb() == (239, 239, 239, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.WindowText - ).getRgb() == (0, 0, 0, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.Base - ).getRgb() == (255, 255, 255, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.AlternateBase - ).getRgb() == (224, 224, 224, 255) - - # Load Default Dark Theme - # ======================= - - CONFIG.guiTheme = "default_dark" - assert theme.loadTheme() is True - - # Check a few values - assert theme._guiPalette.color( - QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255) - - # qtbot.stop() - - -@pytest.mark.skip -@pytest.mark.gui -def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): - """Test the syntax part of the class.""" - mainTheme = SHARED.theme - - # List Themes - # =========== - - # Block the reading of the files - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.listThemes() == [] - - # Load the syntax info - syntaxList = mainTheme.listSyntax() - assert syntaxList[0] == ("default_dark", "Default Dark") - assert syntaxList[1] == ("default_light", "Default Light") - - # A second call should returned the cached list - assert mainTheme.listSyntax() == mainTheme._syntaxList - - # Check handling of broken theme settings - availSyntax = mainTheme._availSyntax - mainTheme._availSyntax = {} - CONFIG.guiSyntax = "not_a_syntax" - assert mainTheme.loadSyntax() is False - mainTheme._availSyntax = availSyntax - - # Check handling of unreadable file - CONFIG.guiSyntax = "default_light" - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.loadSyntax() is False - - # Load Default Light Syntax - # ========================= - - # Load the default syntax - CONFIG.guiSyntax = "default_light" - assert mainTheme.loadSyntax() is True - - # Check some values - assert mainTheme.syntaxMeta.name == "Default Light" - assert mainTheme.syntaxTheme.back == QColor(255, 255, 255) - assert mainTheme.syntaxTheme.text == QColor(0, 0, 0) - assert mainTheme.syntaxTheme.link == QColor(0, 0, 200) - - # Load Default Dark Theme - # ======================= - - # Load the default syntax - CONFIG.guiSyntax = "default_dark" - assert mainTheme.loadSyntax() is True - - # Check some values - assert mainTheme.syntaxMeta.name == "Default Dark" - assert mainTheme.syntaxTheme.back == QColor(42, 42, 42) - assert mainTheme.syntaxTheme.text == QColor(204, 204, 204) - assert mainTheme.syntaxTheme.link == QColor(102, 153, 204) - - # qtbot.stop() - - -@pytest.mark.skip -@pytest.mark.gui -def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths): - """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - - # Load Theme - # ========== - - # Invalid theme name - availThemes = iconCache._availThemes - iconCache._availThemes = {} - assert iconCache.loadTheme("not_a_theme") is False - iconCache._availThemes = availThemes - - # Check handling of unreadable file - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert iconCache.loadTheme("material_rounded_normal") is False - - # Load working theme file - assert iconCache.loadTheme("material_rounded_normal") is True - assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium" - - # Load with project colour override - purple = iconCache._svgColors["purple"] - assert iconCache._svgColors["root"] != purple - assert iconCache._svgColors["folder"] != purple - assert iconCache._svgColors["file"] != purple - assert iconCache._svgColors["title"] != purple - assert iconCache._svgColors["chapter"] != purple - assert iconCache._svgColors["scene"] != purple - assert iconCache._svgColors["note"] != purple + assert theme.getRawBaseColor("root") == b"#ff0000" + assert theme.getRawBaseColor("folder") == b"#ff0000" + assert theme.getRawBaseColor("file") == b"#cccccc" + assert theme.getRawBaseColor("title") == b"#00ff00" + assert theme.getRawBaseColor("chapter") == b"#ff0000" + assert theme.getRawBaseColor("scene") == b"#0000ff" + assert theme.getRawBaseColor("note") == b"#ffff00" + # Reload with project override to purple, also for docs CONFIG.iconColTree = "purple" - assert iconCache.loadTheme("material_rounded_normal") is True - assert iconCache._svgColors["root"] == purple - assert iconCache._svgColors["folder"] == purple - assert iconCache._svgColors["file"] == purple - assert iconCache._svgColors["title"] == purple - assert iconCache._svgColors["chapter"] == purple - assert iconCache._svgColors["scene"] == purple - assert iconCache._svgColors["note"] == purple + CONFIG.iconColDocs = False + theme.loadTheme(force=True) - # Change some colours - iconCache.setIconColor("root", QColor(255, 255, 255)) - assert iconCache._svgColors["root"] != purple - assert iconCache._svgColors["root"] == b"#ffffff" + assert theme.getBaseColor("root").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("folder").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("file").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("title").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("chapter").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("scene").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("note").getRgb() == (255, 0, 255, 255) - # List Themes - # =========== - - # Load error returns empty list - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - themes = iconCache.getIconThemes() - assert themes == [] - - # Successful read - themes = iconCache.getIconThemes() - assert len(themes) > 1 - assert "material_rounded_normal" in dict(themes) - - # Load error doesn't matter on second read since list is cached - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert iconCache.getIconThemes() == themes - - # qtbot.stop() + assert theme.getRawBaseColor("root") == b"#ff00ff" + assert theme.getRawBaseColor("folder") == b"#ff00ff" + assert theme.getRawBaseColor("file") == b"#ff00ff" + assert theme.getRawBaseColor("title") == b"#ff00ff" + assert theme.getRawBaseColor("chapter") == b"#ff00ff" + assert theme.getRawBaseColor("scene") == b"#ff00ff" + assert theme.getRawBaseColor("note") == b"#ff00ff" @pytest.mark.gui -def testGuiTheme_LoadIcons(qtbot, nwGUI): +def testGuiTheme_Methods(monkeypatch): + """Test other themes methods.""" + theme = GuiTheme() + theme.iconCache = MagicMock() + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + + # Init theme + assert theme.colourThemes == {} + theme.initThemes() + assert theme._meta.name == "Default Dark Theme" + + # Text width + theme.guiFont = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) + assert theme.getTextWidth("MMMMM") > theme.getTextWidth("MMM") + font = QFont(theme.guiFont) + font.setPointSizeF(0.5*font.pointSizeF()) + assert theme.getTextWidth("MMMMM", font) < theme.getTextWidth("MMMMM") + + # Detect desktop mode Qt 6.5+ + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light) + assert theme.isDesktopDarkMode() is False + + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark) + assert theme.isDesktopDarkMode() is True + + # Detect desktop mode Qt 6.4 + mockWhite = Mock() + mockWhite.color.return_value = QColor(255, 255, 255) + + mockBlack = Mock() + mockBlack.color.return_value = QColor(0, 0, 0) + + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060400) + + mp.setattr(QPalette, "window", lambda *a: mockWhite) + mp.setattr(QPalette, "windowText", lambda *a: mockBlack) + assert theme.isDesktopDarkMode() is False + + mp.setattr(QPalette, "window", lambda *a: mockBlack) + mp.setattr(QPalette, "windowText", lambda *a: mockWhite) + assert theme.isDesktopDarkMode() is True + + # Stylesheets + assert theme.getStyleSheet(STYLES_FLAT_TABS) != "" + assert theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) != "" + assert theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) != "" + assert theme.getStyleSheet("stuff") == "" + + +@pytest.mark.gui +def testGuiTheme_ScanIcons(monkeypatch): + """Test the icon theme scanning.""" + theme = GuiTheme() + + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("icons"), ".icons") + assert len(files) > 0 + + # Block reading theme files + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + theme.iconCache._scanThemes(files) + assert theme.iconCache.iconThemes == {} + + # Read all themes correctly + theme.iconCache._scanThemes(files) + assert len(theme.iconCache.iconThemes) > 0 + + +@pytest.mark.gui +def testGuiTheme_IconThemes(monkeypatch): + """Test loading icon theme.""" + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.themeMode = nwTheme.LIGHT + CONFIG.iconTheme = DEF_ICONS + + theme = GuiTheme() + + # Init should load default theme + theme.initThemes() + assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium" + assert DEF_ICONS in theme.iconCache.iconThemes + + # Load default theme directly + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("DEF_ICONS") + assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium" + + # Failed loading should load nothing + theme.iconCache._meta = ThemeMeta() + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + theme.iconCache.loadTheme("DEF_ICONS") + assert theme.iconCache._meta.name == "" + + # Reload with non-existent theme should reload default + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("not_a_theme") + assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium" + + # If default theme is missing, load nothing + del theme.iconCache._allThemes[DEF_ICONS] + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("not_a_theme") + assert theme.iconCache._meta.name == "" + + +@pytest.mark.gui +def testGuiTheme_LoadIcons(): """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - assert iconCache.loadTheme("material_rounded_normal") is True + theme = GuiTheme() + theme.initThemes() + iconCache = theme.iconCache # Load Icons # ========== @@ -493,14 +500,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI): nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0" ) == iconCache._noIcon - # qtbot.stop() - @pytest.mark.gui -def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): +def testGuiTheme_LoadDecorations(monkeypatch): """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - assert iconCache.loadTheme("material_rounded_normal") is True + theme = GuiTheme() + theme.initThemes() + iconCache = theme.iconCache # Load Decorations # ================ @@ -557,5 +563,3 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): assert iconCache.getHeaderDecorationNarrow(4) == iconCache._headerDecNarrow[4] assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5] assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5] - - # qtbot.stop() From 334f946870d491d9068f3282dfec4722a020f007 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:47:17 +0200 Subject: [PATCH 20/20] Add remaining missing coverage and revert loadTheme return type --- novelwriter/gui/theme.py | 14 ++++++++------ tests/test_base/test_base_common.py | 6 ++++++ tests/test_gui/test_gui_guimain.py | 25 ++++++++++++++++++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 5c7533ae..4ab4f7d8 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -270,8 +270,10 @@ class GuiTheme: return QColor(*result) return default - def loadTheme(self, force: bool = False) -> None: - """Load the currently specified GUI theme.""" + def loadTheme(self, force: bool = False) -> bool: + """Load the currently specified GUI theme. The boolean return + can be used to determine if the GUI needs refreshing. + """ match CONFIG.themeMode: case nwTheme.LIGHT: darkMode = False @@ -292,12 +294,12 @@ class GuiTheme: if theme == self._currentTheme and not force: logger.info("Theme '%s' is already loaded", theme) - return + return False entry = self._allThemes.get(theme) if not entry: logger.error("Could not load GUI theme") - return + return False CONFIG.splashMessage(f"Loading colour theme: {entry.name}") logger.info("Loading GUI theme '%s'", theme) @@ -307,7 +309,7 @@ class GuiTheme: except Exception: logger.error("Could not read file: %s", entry.path) logException() - return + return False # Reset Palette self._resetTheme() @@ -477,7 +479,7 @@ class GuiTheme: QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) - return + return True def getStyleSheet(self, name: str) -> str: """Load a standard style sheet.""" diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 006daa42..ef16baaa 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -41,6 +41,7 @@ from novelwriter.common import ( readTextFile, simplified, transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem, yesNo ) +from novelwriter.enum import nwItemClass from tests.mocked import causeOSError from tests.tools import writeFile @@ -804,6 +805,7 @@ def testBaseCommon_NWConfigParser(fncPath): "list1 = a, b, c\n" "list2 = 17, 18, 19\n" "float1 = 4.2\n" + "enum1 = NOVEL\n" f"path1 = {fncPath}\n" )) @@ -875,3 +877,7 @@ def testBaseCommon_NWConfigParser(fncPath): assert cfgParser.rdIntList("nope", "list2", [1]) == [1] assert cfgParser.rdIntList("main", "blabla", [1]) == [1] + + # Read Enum + assert cfgParser.rdEnum("main", "enum1", nwItemClass.NO_CLASS) == nwItemClass.NOVEL + assert cfgParser.rdEnum("main", "blabla", nwItemClass.NO_CLASS) == nwItemClass.NO_CLASS diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 91207d09..5010094d 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -24,6 +24,7 @@ import shutil from pathlib import Path from shutil import copyfile +from unittest.mock import Mock import pytest @@ -32,6 +33,7 @@ from PyQt6.QtGui import QPalette from PyQt6.QtWidgets import QInputDialog, QMessageBox from novelwriter import CONFIG, SHARED +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT from novelwriter.constants import nwFiles from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwTheme, nwView @@ -179,22 +181,35 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): @pytest.mark.gui def testGuiMain_UpdateTheme(qtbot, nwGUI): """Test updating the theme in the GUI.""" - mainTheme = SHARED.theme + theme = SHARED.theme CONFIG.themeMode = nwTheme.DARK - CONFIG.darkTheme = "default_dark" - CONFIG.lightTheme = "default_light" - mainTheme.loadTheme() + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.lightTheme = DEF_GUI_LIGHT + theme.loadTheme() + assert theme.isDarkTheme is True nwGUI._processConfigChanges(False, True, False, False) nwGUI._processConfigChanges(True, True, True, True) + # Check editor syntax syntax = SHARED.theme.syntaxTheme - assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back + # Update by check + CONFIG.themeMode = nwTheme.LIGHT + nwGUI.checkThemeUpdate() + assert theme.isDarkTheme is False + + # Through change event + event = Mock() + event.type.return_value = 210 + CONFIG.themeMode = nwTheme.DARK + nwGUI.changeEvent(event) + assert theme.isDarkTheme is True + # qtbot.stop()