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"