diff --git a/novelwriter/assets/icons/material_filled_bold.icons b/novelwriter/assets/icons/material_filled_bold.icons index 9dc5a503..71152699 100644 --- a/novelwriter/assets/icons/material_filled_bold.icons +++ b/novelwriter/assets/icons/material_filled_bold.icons @@ -3,7 +3,7 @@ # Meta meta:name = Material Symbols - Filled Bold meta:author = Google -meta:licence = Apache License Version 2.0 +meta:license = Apache License Version 2.0 # Icons icon:alert_error = @@ -75,6 +75,7 @@ icon:margin_right = icon:maximise = icon:minimise = +icon:more_arrow = icon:more_vertical = icon:noncheckable = icon:novel_view = diff --git a/novelwriter/assets/icons/material_filled_normal.icons b/novelwriter/assets/icons/material_filled_normal.icons index c729da16..601a2c1e 100644 --- a/novelwriter/assets/icons/material_filled_normal.icons +++ b/novelwriter/assets/icons/material_filled_normal.icons @@ -3,7 +3,7 @@ # Meta meta:name = Material Symbols - Filled meta:author = Google -meta:licence = Apache License Version 2.0 +meta:license = Apache License Version 2.0 # Icons icon:alert_error = @@ -75,6 +75,7 @@ icon:margin_right = icon:maximise = icon:minimise = +icon:more_arrow = icon:more_vertical = icon:noncheckable = icon:novel_view = diff --git a/novelwriter/assets/icons/material_outline_bold.icons b/novelwriter/assets/icons/material_outline_bold.icons index a1ae5ea5..18f89bc2 100644 --- a/novelwriter/assets/icons/material_outline_bold.icons +++ b/novelwriter/assets/icons/material_outline_bold.icons @@ -3,7 +3,7 @@ # Meta meta:name = Material Symbols - Outlined Bold meta:author = Google -meta:licence = Apache License Version 2.0 +meta:license = Apache License Version 2.0 # Icons icon:alert_error = @@ -75,6 +75,7 @@ icon:margin_right = icon:maximise = icon:minimise = +icon:more_arrow = icon:more_vertical = icon:noncheckable = icon:novel_view = diff --git a/novelwriter/assets/icons/material_outline_normal.icons b/novelwriter/assets/icons/material_outline_normal.icons index 14d66542..590e6ee6 100644 --- a/novelwriter/assets/icons/material_outline_normal.icons +++ b/novelwriter/assets/icons/material_outline_normal.icons @@ -3,7 +3,7 @@ # Meta meta:name = Material Symbols - Outlined meta:author = Google -meta:licence = Apache License Version 2.0 +meta:license = Apache License Version 2.0 # Icons icon:alert_error = @@ -75,6 +75,7 @@ icon:margin_right = icon:maximise = icon:minimise = +icon:more_arrow = icon:more_vertical = icon:noncheckable = icon:novel_view = diff --git a/novelwriter/assets/icons/material_rounded_bold.icons b/novelwriter/assets/icons/material_rounded_bold.icons index 7d28ed46..7f1a4b3b 100644 --- a/novelwriter/assets/icons/material_rounded_bold.icons +++ b/novelwriter/assets/icons/material_rounded_bold.icons @@ -3,7 +3,7 @@ # Meta meta:name = Material Symbols - Rounded Bold meta:author = Google -meta:licence = Apache License Version 2.0 +meta:license = Apache License Version 2.0 # Icons icon:alert_error = @@ -75,6 +75,7 @@ icon:margin_right = icon:maximise = icon:minimise = +icon:more_arrow = icon:more_vertical = icon:noncheckable = icon:novel_view = diff --git a/novelwriter/assets/icons/material_rounded_normal.icons b/novelwriter/assets/icons/material_rounded_normal.icons index c4022290..ff312875 100644 --- a/novelwriter/assets/icons/material_rounded_normal.icons +++ b/novelwriter/assets/icons/material_rounded_normal.icons @@ -3,7 +3,7 @@ # Meta meta:name = Material Symbols - Rounded meta:author = Google -meta:licence = Apache License Version 2.0 +meta:license = Apache License Version 2.0 # Icons icon:alert_error = @@ -75,6 +75,7 @@ icon:margin_right = icon:maximise = icon:minimise = +icon:more_arrow = icon:more_vertical = icon:noncheckable = icon:novel_view = diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 4e5a0edf..f8569532 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -37,7 +37,7 @@ from PyQt5.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygon from novelwriter import SHARED from novelwriter.common import simplified from novelwriter.enum import nwStatusShape -from novelwriter.types import QtPaintAnitAlias, QtTransparent +from novelwriter.types import QtPaintAntiAlias, QtTransparent if TYPE_CHECKING: # pragma: no cover from typing import TypeGuard # Requires Python 3.10 @@ -195,7 +195,7 @@ class NWStatus: pixmap.fill(QtTransparent) painter = QPainter(pixmap) - painter.setRenderHint(QtPaintAnitAlias) + painter.setRenderHint(QtPaintAntiAlias) painter.fillPath(_SHAPES.getShape(shape), color) painter.end() diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 8f3063e7..319cfaaf 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.types import ( - QtAlignLeft, QtMouseOver, QtNoBrush, QtNoPen, QtPaintAnitAlias, + QtAlignLeft, QtMouseOver, QtNoBrush, QtNoPen, QtPaintAntiAlias, QtSizeExpanding, QtSizeFixed ) @@ -146,7 +146,7 @@ class _PagedToolButton(QToolButton): opt.initFrom(self) paint = QPainter(self) - paint.setRenderHint(QtPaintAnitAlias, True) + paint.setRenderHint(QtPaintAntiAlias, True) paint.setPen(QtNoPen) paint.setBrush(QtNoBrush) @@ -213,7 +213,7 @@ class _NPagedToolLabel(QLabel): label that matches the button style. """ paint = QPainter(self) - paint.setRenderHint(QtPaintAnitAlias, True) + paint.setRenderHint(QtPaintAntiAlias, True) paint.setPen(QtNoPen) width = self.width() diff --git a/novelwriter/extensions/progressbars.py b/novelwriter/extensions/progressbars.py index b08d4808..1c7684fb 100644 --- a/novelwriter/extensions/progressbars.py +++ b/novelwriter/extensions/progressbars.py @@ -31,7 +31,7 @@ from PyQt5.QtGui import QBrush, QColor, QPainter, QPaintEvent, QPen from PyQt5.QtWidgets import QProgressBar, QWidget from novelwriter.types import ( - QtAlignCenter, QtPaintAnitAlias, QtRoundCap, QtSizeFixed, QtSolidLine, + QtAlignCenter, QtPaintAntiAlias, QtRoundCap, QtSizeFixed, QtSolidLine, QtTransparent ) @@ -91,7 +91,7 @@ class NProgressCircle(QProgressBar): progress = 100.0*self.value()/self.maximum() angle = ceil(16*3.6*progress) painter = QPainter(self) - painter.setRenderHint(QtPaintAnitAlias, True) + painter.setRenderHint(QtPaintAntiAlias, True) painter.setPen(self._dPen) painter.setBrush(self._dBrush) painter.drawEllipse(self._dRect) @@ -119,7 +119,7 @@ class NProgressSimple(QProgressBar): if (value := self.value()) > 0: progress = ceil(self.width()*float(value)/self.maximum()) painter = QPainter(self) - painter.setRenderHint(QtPaintAnitAlias, True) + painter.setRenderHint(QtPaintAntiAlias, True) painter.setPen(self.palette().highlight().color()) painter.setBrush(self.palette().highlight()) painter.drawRect(0, 0, progress, self.height()) diff --git a/novelwriter/extensions/statusled.py b/novelwriter/extensions/statusled.py index 1aeb2065..e9a88104 100644 --- a/novelwriter/extensions/statusled.py +++ b/novelwriter/extensions/statusled.py @@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QAbstractButton, QWidget from novelwriter import CONFIG from novelwriter.enum import nwTrinary -from novelwriter.types import QtBlack, QtPaintAnitAlias +from novelwriter.types import QtBlack, QtPaintAntiAlias logger = logging.getLogger(__name__) @@ -81,7 +81,7 @@ class StatusLED(QAbstractButton): def paintEvent(self, event: QPaintEvent) -> None: """Draw the LED.""" painter = QPainter(self) - painter.setRenderHint(QtPaintAnitAlias, True) + painter.setRenderHint(QtPaintAntiAlias, True) painter.setPen(self.palette().windowText().color()) painter.setBrush(self._color) painter.setOpacity(1.0) diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index 20a2b2f1..43d60fc6 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -28,7 +28,7 @@ from PyQt5.QtGui import QMouseEvent, QPainter, QPaintEvent, QResizeEvent from PyQt5.QtWidgets import QAbstractButton, QWidget from novelwriter import CONFIG, SHARED -from novelwriter.types import QtMouseLeft, QtNoPen, QtPaintAnitAlias, QtSizeFixed +from novelwriter.types import QtMouseLeft, QtNoPen, QtPaintAntiAlias, QtSizeFixed class NSwitch(QAbstractButton): @@ -90,7 +90,7 @@ class NSwitch(QAbstractButton): def paintEvent(self, event: QPaintEvent) -> None: """Drawing the switch itself.""" painter = QPainter(self) - painter.setRenderHint(QtPaintAnitAlias, True) + painter.setRenderHint(QtPaintAntiAlias, True) painter.setPen(QtNoPen) palette = self.palette() diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 96617d71..190bcafd 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -452,7 +452,7 @@ class GuiNovelTree(QTreeWidget): def updateTheme(self) -> None: """Update theme elements.""" iPx = SHARED.theme.baseIconHeight - self._pMore = SHARED.theme.loadDecoration("deco_doc_more", h=iPx) + self._pMore = SHARED.theme.getPixmap("more_arrow", (iPx, iPx)) return ## diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index f78dde59..2ef76954 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -30,7 +30,10 @@ from math import ceil from pathlib import Path from PyQt5.QtCore import QSize, Qt -from PyQt5.QtGui import QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPalette, QPixmap +from PyQt5.QtGui import ( + QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPainter, QPainterPath, + QPalette, QPixmap +) from PyQt5.QtWidgets import QApplication from novelwriter import CONFIG @@ -38,7 +41,7 @@ from novelwriter.common import NWConfigParser, cssCol, minmax from novelwriter.constants import nwLabels from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException -from novelwriter.types import QtTransparent +from novelwriter.types import QtPaintAntiAlias, QtTransparent logger = logging.getLogger(__name__) @@ -68,7 +71,6 @@ class GuiTheme: self.themeUrl = "" self.themeLicense = "" self.themeLicenseUrl = "" - self.themeIcons = "" self.isLightTheme = True # GUI @@ -244,7 +246,6 @@ class GuiTheme: 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", "") # Icons sec = "Icons" @@ -304,12 +305,10 @@ class GuiTheme: self.helpText.red(), self.helpText.green(), self.helpText.blue() ) - # Icons - defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark" - self.iconCache.loadTheme(self.themeIcons or defaultIcons) - self.iconCache.loadNewTheme(CONFIG.guiIcons) + # Load icons after theme is parsed + self.iconCache.loadTheme(CONFIG.guiIcons) - # Apply Styles + # Apply styles QApplication.setPalette(self._guiPalette) # Reset stylesheets so that they are regenerated @@ -526,27 +525,20 @@ class GuiIcons: # Storage self._svgData: dict[str, bytes] = {} self._svgColours: dict[str, bytes] = {} - self._qIcons: dict[str, QIcon] = {} - self._themeMap: dict[str, Path] = {} self._headerDec: list[QPixmap] = [] self._headerDecNarrow: list[QPixmap] = [] # Icon Theme Path - self._confName = "icons.conf" self._iconPath = CONFIG.assetPath("icons") # None Icon self._noIcon = QIcon(str(self._iconPath / "none.svg")) # Icon Theme Meta - self.themeName = "" - self.themeDescription = "" - self.themeAuthor = "" - self.themeCredit = "" - self.themeUrl = "" - self.themeLicense = "" - self.themeLicenseUrl = "" + self.themeName = "" + self.themeAuthor = "" + self.themeLicense = "" return @@ -559,74 +551,7 @@ class GuiIcons: 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 = [] - self._headerDecNarrow = [] - - return True - - def loadNewTheme(self, iconTheme: str) -> bool: - """Load new style theme.""" themePath = self._iconPath / f"{iconTheme}.icons" try: with open(themePath, mode="r", encoding="utf-8") as icons: @@ -637,11 +562,26 @@ class GuiIcons: if key and value: if key.startswith("icon:"): self._svgData[key[5:]] = value.encode("utf-8") + elif key == "meta:name": + self.themeName = value + elif key == "meta:author": + self.themeAuthor = value + elif key == "meta:license": + self.themeLicense = value except Exception: logger.error("Could not load icon theme settings from: %s", themePath) logException() return False + # Refresh icons + for iconKey in self._qIcons: + logger.debug("Reloading icon: '%s'", iconKey) + qIcon = self._loadIcon(iconKey) + self._qIcons[iconKey] = qIcon + + self._headerDec = [] + self._headerDecNarrow = [] + return True def setIconColor(self, key: str, color: QColor) -> None: @@ -657,9 +597,7 @@ class GuiIcons: """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: + if name in self.IMAGE_MAP: idx = 0 if self.mainTheme.isLightTheme else 1 imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx] else: @@ -743,11 +681,11 @@ class GuiIcons: if not self._headerDec: iPx = self.mainTheme.baseIconHeight 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), + self._generateDecoration("default", iPx, 0), + self._generateDecoration("green", iPx, 0), + self._generateDecoration("red", iPx, 1), + self._generateDecoration("blue", iPx, 2), + self._generateDecoration("default", iPx, 3), ] return self._headerDec[minmax(hLevel, 0, 4)] @@ -756,12 +694,12 @@ class GuiIcons: if not self._headerDecNarrow: iPx = self.mainTheme.baseIconHeight self._headerDecNarrow = [ - self.loadDecoration("deco_doc_h0_n", h=iPx), - self.loadDecoration("deco_doc_h1_n", h=iPx), - self.loadDecoration("deco_doc_h2_n", h=iPx), - self.loadDecoration("deco_doc_h3_n", h=iPx), - self.loadDecoration("deco_doc_h4_n", h=iPx), - self.loadDecoration("deco_doc_nt_n", h=iPx), + self._generateDecoration("default", iPx, 0), + self._generateDecoration("green", iPx, 0), + self._generateDecoration("red", iPx, 0), + self._generateDecoration("blue", iPx, 0), + self._generateDecoration("default", iPx, 0), + self._generateDecoration("yellow", iPx, 0), ] return self._headerDecNarrow[minmax(hLevel, 0, 5)] @@ -787,16 +725,28 @@ class GuiIcons: pixmap.loadFromData(svg, "svg") return QIcon(pixmap) - # 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 self._noIcon + def _generateDecoration(self, color: str, height: int, indent: int = 0) -> QPixmap: + """Generate a decoration pixmap for novel headers.""" + pixmap = QPixmap(48*indent + 12, 48) + pixmap.fill(QtTransparent) + + path = QPainterPath() + path.addRoundedRect(48.0*indent, 2.0, 12.0, 44.0, 4.0, 4.0) + + painter = QPainter(pixmap) + painter.setRenderHint(QtPaintAntiAlias) + if fill := self._svgColours.get(color or "default"): + painter.fillPath(path, QColor(fill.decode(encoding="utf-8"))) + painter.end() + + tMode = Qt.TransformationMode.SmoothTransformation + return pixmap.scaledToHeight(height, tMode) + # Module Functions # ================ diff --git a/novelwriter/types.py b/novelwriter/types.py index a692a8c6..0732f5a8 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -66,7 +66,7 @@ QtNoBrush = Qt.BrushStyle.NoBrush QtNoPen = Qt.PenStyle.NoPen QtRoundCap = Qt.PenCapStyle.RoundCap QtSolidLine = Qt.PenStyle.SolidLine -QtPaintAnitAlias = QPainter.RenderHint.Antialiasing +QtPaintAntiAlias = QPainter.RenderHint.Antialiasing QtMouseOver = QStyle.StateFlag.State_MouseOver QtSelected = QStyle.StateFlag.State_Selected diff --git a/utils/material_icons.py b/utils/material_icons.py index 49a62579..7758759c 100644 --- a/utils/material_icons.py +++ b/utils/material_icons.py @@ -104,6 +104,7 @@ ICON_MAP = { "margin_top": "vertical_align_top", "maximise": "fullscreen", "minimise": "fullscreen_exit", + "more_arrow": "arrow_right", "more_vertical": "more_vert", "noncheckable": "indeterminate_check_box", "novel_view": "book_4_spark", @@ -161,7 +162,7 @@ def processMaterialIcons(workDir: Path, iconsDir: Path, jobs: dict) -> None: icons.write("# Meta\n") icons.write(f"meta:name = {name}\n") icons.write("meta:author = Google\n") - icons.write("meta:licence = Apache License Version 2.0\n") + icons.write("meta:license = Apache License Version 2.0\n") icons.write("\n") icons.write("# Icons\n") iconSrc = srcRepo / "symbols" / "web"