Files
novelWriter/novelwriter/gui/theme.py
T
2023-11-14 22:41:39 +01:00

729 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter Theme and Icons Classes
=====================================
File History:
Created: 2019-05-18 [0.1.3] GuiTheme
Created: 2019-11-08 [0.4] GuiIcons
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from math import ceil
from pathlib import Path
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import qApp
from PyQt5.QtGui import (
QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase, QPixmap
)
from novelwriter import CONFIG
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
from novelwriter.common import NWConfigParser, minmax
from novelwriter.constants import nwLabels
logger = logging.getLogger(__name__)
# =============================================================================================== #
# Gui Theme Class
# Handles the look and feel of novelWriter
# =============================================================================================== #
class GuiTheme:
def __init__(self) -> None:
self.iconCache = GuiIcons(self)
# Loaded Theme Settings
# =====================
# Theme
self.themeName = ""
self.themeDescription = ""
self.themeAuthor = ""
self.themeCredit = ""
self.themeUrl = ""
self.themeLicense = ""
self.themeLicenseUrl = ""
self.themeIcons = ""
# GUI
self.statNone = [120, 120, 120]
self.statUnsaved = [200, 15, 39]
self.statSaved = [2, 133, 37]
self.helpText = [0, 0, 0]
# Loaded Syntax Settings
# ======================
# Main
self.syntaxName = ""
self.syntaxDescription = ""
self.syntaxAuthor = ""
self.syntaxCredit = ""
self.syntaxUrl = ""
self.syntaxLicense = ""
self.syntaxLicenseUrl = ""
# Colours
self.colBack = [255, 255, 255]
self.colText = [0, 0, 0]
self.colLink = [0, 0, 0]
self.colHead = [0, 0, 0]
self.colHeadH = [0, 0, 0]
self.colEmph = [0, 0, 0]
self.colDialN = [0, 0, 0]
self.colDialD = [0, 0, 0]
self.colDialS = [0, 0, 0]
self.colHidden = [0, 0, 0]
self.colCode = [0, 0, 0]
self.colKey = [0, 0, 0]
self.colVal = [0, 0, 0]
self.colSpell = [0, 0, 0]
self.colError = [0, 0, 0]
self.colRepTag = [0, 0, 0]
self.colMod = [0, 0, 0]
# Class Setup
# ===========
# Init GUI Font
self._setGuiFont()
# Load Themes
self._guiPalette = QPalette()
self._themeList: list[tuple[str, str]] = []
self._syntaxList: list[tuple[str, str]] = []
self._availThemes: dict[str, Path] = {}
self._availSyntax: dict[str, Path] = {}
self._listConf(self._availSyntax, CONFIG.assetPath("syntax"))
self._listConf(self._availThemes, CONFIG.assetPath("themes"))
self._listConf(self._availSyntax, CONFIG.dataPath("syntax"))
self._listConf(self._availThemes, CONFIG.dataPath("themes"))
self.loadTheme()
self.loadSyntax()
# Icon Functions
self.getIcon = self.iconCache.getIcon
self.getPixmap = self.iconCache.getPixmap
self.getItemIcon = self.iconCache.getItemIcon
self.getToggleIcon = self.iconCache.getToggleIcon
self.loadDecoration = self.iconCache.loadDecoration
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
# Extract Other Info
self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
self.guiScale = qApp.primaryScreen().logicalDotsPerInchX()/96.0
CONFIG.guiScale = self.guiScale
logger.debug("GUI DPI: %.1f", self.guiDPI)
logger.debug("GUI Scale: %.2f", self.guiScale)
# Fonts
self.guiFont = qApp.font()
qMetric = QFontMetrics(self.guiFont)
self.fontPointSize = self.guiFont.pointSizeF()
self.fontPixelSize = int(round(qMetric.height()))
self.baseIconSize = int(round(qMetric.ascent()))
self.textNHeight = qMetric.boundingRect("N").height()
self.textNWidth = qMetric.boundingRect("N").width()
# Monospace Font
self.guiFontFixed = QFont()
self.guiFontFixed.setPointSizeF(0.95*self.fontPointSize)
self.guiFontFixed.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
logger.debug("GUI Font Family: %s", self.guiFont.family())
logger.debug("GUI Font Point Size: %.2f", self.fontPointSize)
logger.debug("GUI Font Pixel Size: %d", self.fontPixelSize)
logger.debug("GUI Base Icon Size: %d", self.baseIconSize)
logger.debug("Text 'N' Height: %d", self.textNHeight)
logger.debug("Text 'N' Width: %d", self.textNWidth)
return
##
# Methods
##
def getTextWidth(self, text: str, font: QFont | None = None) -> int:
"""Returns the width needed to contain a given piece of text in
pixels.
"""
if isinstance(font, QFont):
qMetrics = QFontMetrics(font)
else:
qMetrics = QFontMetrics(self.guiFont)
return int(ceil(qMetrics.boundingRect(text).width()))
##
# Theme Methods
##
def loadTheme(self) -> bool:
"""Load the currently specified GUI theme."""
guiTheme = CONFIG.guiTheme
if guiTheme not in self._availThemes:
logger.error("Could not find GUI theme '%s'", guiTheme)
guiTheme = "default"
CONFIG.guiTheme = guiTheme
themeFile = self._availThemes.get(guiTheme, None)
if themeFile is None:
logger.error("Could not load GUI theme")
return False
# Config File
logger.info("Loading GUI theme '%s'", guiTheme)
parser = NWConfigParser()
try:
with open(themeFile, mode="r", encoding="utf-8") as inFile:
parser.read_file(inFile)
except Exception:
logger.error("Could not load theme settings from: %s", themeFile)
logException()
return False
# Main
sec = "Main"
if parser.has_section(sec):
self.themeName = parser.rdStr(sec, "name", "")
self.themeDescription = parser.rdStr(sec, "description", "N/A")
self.themeAuthor = parser.rdStr(sec, "author", "N/A")
self.themeCredit = parser.rdStr(sec, "credit", "N/A")
self.themeUrl = parser.rdStr(sec, "url", "")
self.themeLicense = parser.rdStr(sec, "license", "N/A")
self.themeLicenseUrl = parser.rdStr(sec, "licenseurl", "")
self.themeIcons = parser.rdStr(sec, "icontheme", "")
# Palette
sec = "Palette"
if parser.has_section(sec):
self._setPalette(parser, sec, "window", QPalette.ColorRole.Window)
self._setPalette(parser, sec, "windowtext", QPalette.ColorRole.WindowText)
self._setPalette(parser, sec, "base", QPalette.ColorRole.Base)
self._setPalette(parser, sec, "alternatebase", QPalette.ColorRole.AlternateBase)
self._setPalette(parser, sec, "text", QPalette.ColorRole.Text)
self._setPalette(parser, sec, "tooltipbase", QPalette.ColorRole.ToolTipBase)
self._setPalette(parser, sec, "tooltiptext", QPalette.ColorRole.ToolTipText)
self._setPalette(parser, sec, "button", QPalette.ColorRole.Button)
self._setPalette(parser, sec, "buttontext", QPalette.ColorRole.ButtonText)
self._setPalette(parser, sec, "brighttext", QPalette.ColorRole.BrightText)
self._setPalette(parser, sec, "highlight", QPalette.ColorRole.Highlight)
self._setPalette(parser, sec, "highlightedtext", QPalette.ColorRole.HighlightedText)
self._setPalette(parser, sec, "link", QPalette.ColorRole.Link)
self._setPalette(parser, sec, "linkvisited", QPalette.ColorRole.LinkVisited)
else:
self._guiPalette = qApp.style().standardPalette()
# GUI
sec = "GUI"
if parser.has_section(sec):
self.helpText = self._parseColour(parser, sec, "helptext")
self.statNone = self._parseColour(parser, sec, "statusnone")
self.statUnsaved = self._parseColour(parser, sec, "statusunsaved")
self.statSaved = self._parseColour(parser, sec, "statussaved")
# Update Dependant Colours
backCol = self._guiPalette.window().color()
textCol = self._guiPalette.windowText().color()
backLNess = backCol.lightnessF()
textLNess = textCol.lightnessF()
if self.helpText == [0, 0, 0]:
if backLNess > textLNess:
helpLCol = textLNess + 0.35*(backLNess - textLNess)
else:
helpLCol = backLNess + 0.65*(textLNess - backLNess)
self.helpText = [int(255*helpLCol)]*3
# Icons
defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark"
self.iconCache.loadTheme(self.themeIcons or defaultIcons)
# Apply Styles
qApp.setPalette(self._guiPalette)
return True
def loadSyntax(self) -> bool:
"""Load the currently specified syntax highlighter theme."""
guiSyntax = CONFIG.guiSyntax
if guiSyntax not in self._availSyntax:
logger.error("Could not find syntax theme '%s'", guiSyntax)
guiSyntax = "default_light"
CONFIG.guiSyntax = guiSyntax
syntaxFile = self._availSyntax.get(guiSyntax, None)
if syntaxFile is None:
logger.error("Could not load syntax theme")
return False
logger.info("Loading syntax theme '%s'", guiSyntax)
confParser = NWConfigParser()
try:
with open(syntaxFile, mode="r", encoding="utf-8") as inFile:
confParser.read_file(inFile)
except Exception:
logger.error("Could not load syntax colours from: %s", syntaxFile)
logException()
return False
# Main
cnfSec = "Main"
if confParser.has_section(cnfSec):
self.syntaxName = confParser.rdStr(cnfSec, "name", "")
self.syntaxDescription = confParser.rdStr(cnfSec, "description", "N/A")
self.syntaxAuthor = confParser.rdStr(cnfSec, "author", "N/A")
self.syntaxCredit = confParser.rdStr(cnfSec, "credit", "N/A")
self.syntaxUrl = confParser.rdStr(cnfSec, "url", "")
self.syntaxLicense = confParser.rdStr(cnfSec, "license", "N/A")
self.syntaxLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
# Syntax
cnfSec = "Syntax"
if confParser.has_section(cnfSec):
self.colBack = self._parseColour(confParser, cnfSec, "background")
self.colText = self._parseColour(confParser, cnfSec, "text")
self.colLink = self._parseColour(confParser, cnfSec, "link")
self.colHead = self._parseColour(confParser, cnfSec, "headertext")
self.colHeadH = self._parseColour(confParser, cnfSec, "headertag")
self.colEmph = self._parseColour(confParser, cnfSec, "emphasis")
self.colDialN = self._parseColour(confParser, cnfSec, "straightquotes")
self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
self.colKey = self._parseColour(confParser, cnfSec, "keyword")
self.colVal = self._parseColour(confParser, cnfSec, "value")
self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
self.colError = self._parseColour(confParser, cnfSec, "errorline")
self.colRepTag = self._parseColour(confParser, cnfSec, "replacetag")
self.colMod = self._parseColour(confParser, cnfSec, "modifier")
return True
def listThemes(self) -> list[tuple[str, str]]:
"""Scan the GUI themes folder and list all themes."""
if self._themeList:
return self._themeList
confParser = NWConfigParser()
for themeKey, themePath in self._availThemes.items():
logger.debug("Checking theme config for '%s'", themeKey)
themeName = _loadInternalName(confParser, themePath)
if themeName:
self._themeList.append((themeKey, themeName))
self._themeList = sorted(self._themeList, key=lambda x: x[1])
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
confParser = NWConfigParser()
for syntaxKey, syntaxPath in self._availSyntax.items():
logger.debug("Checking theme syntax for '%s'", syntaxKey)
syntaxName = _loadInternalName(confParser, syntaxPath)
if syntaxName:
self._syntaxList.append((syntaxKey, syntaxName))
self._syntaxList = sorted(self._syntaxList, key=lambda x: x[1])
return self._syntaxList
##
# Internal Functions
##
def _setGuiFont(self) -> None:
"""Update the GUI's font style from settings."""
theFont = QFont()
fontDB = QFontDatabase()
if CONFIG.guiFont not in fontDB.families():
if CONFIG.osWindows and "Arial" in fontDB.families():
# On Windows we default to Arial if possible
theFont.setFamily("Arial")
theFont.setPointSize(10)
else:
theFont = fontDB.systemFont(QFontDatabase.GeneralFont)
CONFIG.guiFont = theFont.family()
CONFIG.guiFontSize = theFont.pointSize()
else:
theFont.setFamily(CONFIG.guiFont)
theFont.setPointSize(CONFIG.guiFontSize)
qApp.setFont(theFont)
return
def _listConf(self, targetDict: dict, checkDir: Path) -> bool:
"""Scan for theme config files and populate the dictionary."""
if not checkDir.is_dir():
return False
for checkFile in checkDir.iterdir():
if checkFile.is_file() and checkFile.name.endswith(".conf"):
targetDict[checkFile.name[:-5]] = checkFile
return True
def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> list[int]:
"""Parse a colour value from a config string."""
if parser.has_option(section, name):
values = parser.get(section, name).split(",")
result = []
try:
result.append(minmax(int(values[0]), 0, 255))
result.append(minmax(int(values[1]), 0, 255))
result.append(minmax(int(values[2]), 0, 255))
except Exception:
logger.error("Could not load theme colours for '%s' from config file", name)
result = [0, 0, 0]
else:
logger.warning("Could not find theme colours for '%s' in config file", name)
result = [0, 0, 0]
return result
def _setPalette(self, parser: NWConfigParser, section: str,
name: str, value: QPalette.ColorRole) -> None:
"""Set a palette colour value from a config string."""
self._guiPalette.setColor(
value, QColor(*self._parseColour(parser, section, name))
)
return
# End Class GuiTheme
# =============================================================================================== #
# Icons Class
# =============================================================================================== #
class GuiIcons:
"""The icon class manages the content of the assets/icons folder,
and provides a simple interface for requesting icons. Only icons
listed in the ICON_KEYS are handled.
Icons are loaded on first request, and then cached for further
requests. Each icon key in the ICON_KEYS set has standard icon set
in the icon theme conf file. The existence of the file, and the
definition of all keys are checked when the theme is loaded.
When an icon is requested, the icon is loaded and cached. If it is
missing, a blank icon is returned and a warning issued.
"""
ICON_KEYS = {
# Project and GUI Icons
"novelwriter", "alert_error", "alert_info", "alert_question", "alert_warn",
"build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details",
"proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene",
"proj_stats", "proj_title", "status_idle", "status_lang", "status_lines", "status_stats",
"status_time", "view_build", "view_editor", "view_novel", "view_outline",
# Class Icons
"cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
"cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world",
# Search Icons
"search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
"search_regex", "search_word",
# Format Icons
"fmt_bold", "fmt_italic", "fmt_mode-md", "fmt_mode-sc", "fmt_strike", "fmt_subscript",
"fmt_superscript", "fmt_underline",
# General Button Icons
"add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
"export", "forward", "maximise", "menu", "minimise", "noncheckable", "panel", "refresh",
"remove", "revert", "search_replace", "search", "settings", "unchecked", "up",
# Switches
"sticky-on", "sticky-off",
"bullet-on", "bullet-off",
# Decorations
"deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more",
}
TOGGLE_ICON_KEYS = {
"sticky": ("sticky-on", "sticky-off"),
"bullet": ("bullet-on", "bullet-off"),
"fmt_mode": ("fmt_mode-sc", "fmt_mode-md"),
}
IMAGE_MAP = {
"wiz-back": "wizard-back.jpg",
}
def __init__(self, mainTheme: GuiTheme) -> None:
self.mainTheme = mainTheme
# Storage
self._qIcons: dict[str, QIcon] = {}
self._themeMap: dict[str, Path] = {}
self._headerDec: list[QPixmap] = []
# Icon Theme Path
self._confName = "icons.conf"
self._iconPath = CONFIG.assetPath("icons")
# Icon Theme Meta
self.themeName = ""
self.themeDescription = ""
self.themeAuthor = ""
self.themeCredit = ""
self.themeUrl = ""
self.themeLicense = ""
self.themeLicenseUrl = ""
return
##
# Actions
##
def loadTheme(self, iconTheme: 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.
"""
self._themeMap = {}
themePath = self._iconPath / iconTheme
if not themePath.is_dir():
themePath = CONFIG.dataPath("icons") / iconTheme
if not themePath.is_dir():
logger.warning("No icons loaded for '%s'", iconTheme)
return False
themeConf = themePath / self._confName
logger.info("Loading icon theme '%s'", iconTheme)
# Config File
confParser = NWConfigParser()
try:
with open(themeConf, mode="r", encoding="utf-8") as inFile:
confParser.read_file(inFile)
except Exception:
logger.error("Could not load icon theme settings from: %s", themeConf)
logException()
return False
# Main
cnfSec = "Main"
if confParser.has_section(cnfSec):
self.themeName = confParser.rdStr(cnfSec, "name", "")
self.themeDescription = confParser.rdStr(cnfSec, "description", "")
self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A")
self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A")
self.themeUrl = confParser.rdStr(cnfSec, "url", "")
self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A")
self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
# Populate Icon Map
cnfSec = "Map"
if confParser.has_section(cnfSec):
for iconName, iconFile in confParser.items(cnfSec):
if iconName not in self.ICON_KEYS:
logger.error("Unknown icon name '%s' in config file", iconName)
else:
iconPath = themePath / iconFile
if iconPath.is_file():
self._themeMap[iconName] = iconPath
logger.debug("Icon slot '%s' using file '%s'", iconName, iconFile)
else:
logger.error("Icon file '%s' not in theme folder", iconFile)
# Check that icons have been defined
logger.debug("Scanning theme icons")
for iconKey in self.ICON_KEYS:
if iconKey in ("novelwriter", "proj_nwx"):
# These are not part of the theme itself
continue
if iconKey not in self._themeMap:
logger.error("No icon file specified for '%s'", iconKey)
# Refresh icons
for iconKey in self._qIcons:
logger.debug("Reloading icon: '%s'", iconKey)
qIcon = self._loadIcon(iconKey)
self._qIcons[iconKey] = qIcon
self._headerDec = []
return True
##
# Access Functions
##
def loadDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
"""Load graphical decoration element based on the decoration
map or the icon map. This function always returns a QPixmap.
"""
if name in self._themeMap:
imgPath = self._themeMap[name]
elif name in self.IMAGE_MAP:
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name]
else:
logger.error("Decoration with name '%s' does not exist", name)
return QPixmap()
if not imgPath.is_file():
logger.error("Asset not found: %s", imgPath)
return QPixmap()
pixmap = QPixmap(str(imgPath))
if w is not None and h is not None:
return pixmap.scaled(w, h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
elif w is None and h is not None:
return pixmap.scaledToHeight(h, Qt.SmoothTransformation)
elif w is not None and h is None:
return pixmap.scaledToWidth(w, Qt.SmoothTransformation)
return pixmap
def getIcon(self, name: str) -> QIcon:
"""Return an icon from the icon buffer, or load it."""
if name in self._qIcons:
return self._qIcons[name]
else:
icon = self._loadIcon(name)
self._qIcons[name] = icon
return icon
def getToggleIcon(self, name: str, size: tuple[int, int]) -> QIcon:
"""Return a toggle icon from the icon buffer. or load it."""
if name in self.TOGGLE_ICON_KEYS:
pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size)
pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size)
icon = QIcon()
icon.addPixmap(pOne, QIcon.Normal, QIcon.On)
icon.addPixmap(pTwo, QIcon.Normal, QIcon.Off)
return icon
return QIcon()
def getPixmap(self, name: str, size: tuple[int, int]) -> QPixmap:
"""Return an icon from the icon buffer as a QPixmap. If it
doesn't exist, return an empty QPixmap.
"""
return self.getIcon(name).pixmap(size[0], size[1], QIcon.Normal)
def getItemIcon(self, tType: nwItemType, tClass: nwItemClass,
tLayout: nwItemLayout, hLevel: str = "H0") -> QIcon:
"""Get the correct icon for a project item based on type, class
and header level
"""
iconName = None
if tType == nwItemType.ROOT:
iconName = nwLabels.CLASS_ICON[tClass]
elif tType == nwItemType.FOLDER:
iconName = "proj_folder"
elif tType == nwItemType.FILE:
iconName = "proj_document"
if tLayout == nwItemLayout.DOCUMENT:
if hLevel == "H1":
iconName = "proj_title"
elif hLevel == "H2":
iconName = "proj_chapter"
elif hLevel == "H3":
iconName = "proj_scene"
elif hLevel == "H4":
iconName = "proj_section"
elif tLayout == nwItemLayout.NOTE:
iconName = "proj_note"
if iconName is None:
return QIcon()
return self.getIcon(iconName)
def getHeaderDecoration(self, hLevel: int) -> QPixmap:
"""Get the decoration for a specific header level."""
if not self._headerDec:
iPx = self.mainTheme.baseIconSize
self._headerDec = [
self.loadDecoration("deco_doc_h0", h=iPx),
self.loadDecoration("deco_doc_h1", h=iPx),
self.loadDecoration("deco_doc_h2", h=iPx),
self.loadDecoration("deco_doc_h3", h=iPx),
self.loadDecoration("deco_doc_h4", h=iPx),
]
return self._headerDec[minmax(hLevel, 0, 4)]
##
# Internal Functions
##
def _loadIcon(self, name: str) -> QIcon:
"""Load an icon from the assets themes folder. Is guaranteed to
return a QIcon.
"""
if name not in self.ICON_KEYS:
logger.error("Requested unknown icon name '%s'", name)
return QIcon()
# If we just want the app icons, return right away
if name == "novelwriter":
return QIcon(str(self._iconPath / "novelwriter.svg"))
elif name == "proj_nwx":
return QIcon(str(self._iconPath / "x-novelwriter-project.svg"))
# Otherwise, we load from the theme folder
if name in self._themeMap:
logger.debug("Loading: %s", self._themeMap[name].name)
return QIcon(str(self._themeMap[name]))
# If we didn't find one, give up and return an empty icon
logger.warning("Did not load an icon for '%s'", name)
return QIcon()
# END Class GuiIcons
# =============================================================================================== #
# Module Functions
# =============================================================================================== #
def _loadInternalName(confParser: NWConfigParser, confFile: str | Path) -> str:
"""Open a conf file and read the 'name' setting."""
try:
with open(confFile, mode="r", encoding="utf-8") as inFile:
confParser.read_file(inFile)
except Exception:
logger.error("Could not load file: %s", confFile)
logException()
return ""
return confParser.rdStr("Main", "name", "")