Fix a few minor issues with the new icon themes, and Qt6 theme support (#2190)
This commit is contained in:
@@ -30,11 +30,11 @@ note = 255, 255, 0
|
|||||||
window = 0, 0, 0
|
window = 0, 0, 0
|
||||||
windowtext = 150, 150, 150
|
windowtext = 150, 150, 150
|
||||||
base = 0, 0, 0
|
base = 0, 0, 0
|
||||||
alternatebase = 30, 20, 45
|
alternatebase = 40, 40, 40
|
||||||
text = 150, 150, 150
|
text = 150, 150, 150
|
||||||
tooltipbase = 40, 20, 70
|
tooltipbase = 40, 20, 70
|
||||||
tooltiptext = 255, 255, 255
|
tooltiptext = 255, 255, 255
|
||||||
button = 5, 0, 10
|
button = 5, 0, 10
|
||||||
buttontext = 150, 150, 150
|
buttontext = 150, 150, 150
|
||||||
brighttext = 255, 255, 255
|
brighttext = 255, 255, 255
|
||||||
highlight = 50, 30, 80
|
highlight = 50, 30, 80
|
||||||
@@ -45,7 +45,4 @@ linkvisited = 50, 0, 80
|
|||||||
[GUI]
|
[GUI]
|
||||||
helptext = 97, 97, 97
|
helptext = 97, 97, 97
|
||||||
fadedtext = 97, 97, 97
|
fadedtext = 97, 97, 97
|
||||||
errortext = 255, 77, 77
|
errortext = 242, 72, 23
|
||||||
statusnone = 50, 50, 50
|
|
||||||
statussaved = 77, 255, 77
|
|
||||||
statusunsaved = 255, 77, 77
|
|
||||||
|
|||||||
@@ -47,6 +47,3 @@ linkvisited = 102, 153, 204
|
|||||||
helptext = 164, 164, 164
|
helptext = 164, 164, 164
|
||||||
fadedtext = 148, 148, 148
|
fadedtext = 148, 148, 148
|
||||||
errortext = 255, 164, 164
|
errortext = 255, 164, 164
|
||||||
statusnone = 150, 152, 150
|
|
||||||
statussaved = 39, 135, 78
|
|
||||||
statusunsaved = 138, 32, 32
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ note = 234, 183, 0
|
|||||||
window = 239, 239, 239
|
window = 239, 239, 239
|
||||||
windowtext = 0, 0, 0
|
windowtext = 0, 0, 0
|
||||||
base = 255, 255, 255
|
base = 255, 255, 255
|
||||||
alternatebase = 239, 239, 239
|
alternatebase = 224, 224, 224
|
||||||
text = 0, 0, 0
|
text = 0, 0, 0
|
||||||
tooltipbase = 255, 255, 220
|
tooltipbase = 255, 255, 220
|
||||||
tooltiptext = 0, 0, 0
|
tooltiptext = 0, 0, 0
|
||||||
@@ -47,6 +47,3 @@ linkvisited = 66, 113, 174
|
|||||||
helptext = 92, 92, 92
|
helptext = 92, 92, 92
|
||||||
fadedtext = 108, 108, 108
|
fadedtext = 108, 108, 108
|
||||||
errortext = 255, 92, 92
|
errortext = 255, 92, 92
|
||||||
statusnone = 120, 120, 120
|
|
||||||
statussaved = 2, 133, 37
|
|
||||||
statusunsaved = 200, 15, 39
|
|
||||||
|
|||||||
@@ -62,6 +62,3 @@ linkvisited = 139, 233, 253
|
|||||||
helptext = 204, 172, 249
|
helptext = 204, 172, 249
|
||||||
fadedtext = 98, 114, 164
|
fadedtext = 98, 114, 164
|
||||||
errortext = 255, 85, 85
|
errortext = 255, 85, 85
|
||||||
statusnone = 98, 114, 164
|
|
||||||
statussaved = 80, 250, 123
|
|
||||||
statusunsaved = 255, 85, 85
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ yellow = 181, 137, 0
|
|||||||
green = 133, 153, 0
|
green = 133, 153, 0
|
||||||
aqua = 42, 161, 152
|
aqua = 42, 161, 152
|
||||||
blue = 38, 139, 210
|
blue = 38, 139, 210
|
||||||
purple = 108, 113, 196
|
purple = 211, 54, 130
|
||||||
|
|
||||||
[Project]
|
[Project]
|
||||||
root = 42, 161, 152
|
root = 42, 161, 152
|
||||||
@@ -30,7 +30,7 @@ note = 181, 137, 0
|
|||||||
window = 0, 43, 54
|
window = 0, 43, 54
|
||||||
windowtext = 253, 246, 227
|
windowtext = 253, 246, 227
|
||||||
base = 7, 54, 66
|
base = 7, 54, 66
|
||||||
alternatebase = 0, 43, 54
|
alternatebase = 88, 110, 117
|
||||||
text = 253, 246, 227
|
text = 253, 246, 227
|
||||||
tooltipbase = 133, 153, 0
|
tooltipbase = 133, 153, 0
|
||||||
tooltiptext = 0, 43, 54
|
tooltiptext = 0, 43, 54
|
||||||
@@ -43,9 +43,6 @@ link = 38, 139, 210
|
|||||||
linkvisited = 38, 139, 210
|
linkvisited = 38, 139, 210
|
||||||
|
|
||||||
[GUI]
|
[GUI]
|
||||||
helptext = 166, 161, 149
|
helptext = 101, 123, 131
|
||||||
fadedtext = 166, 161, 149
|
fadedtext = 101, 123, 131
|
||||||
errortext = 255, 161, 149
|
errortext = 220, 50, 47
|
||||||
statusnone = 88, 110, 117
|
|
||||||
statussaved = 42, 161, 152
|
|
||||||
statusunsaved = 203, 75, 22
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ yellow = 181, 137, 0
|
|||||||
green = 133, 153, 0
|
green = 133, 153, 0
|
||||||
aqua = 42, 161, 152
|
aqua = 42, 161, 152
|
||||||
blue = 38, 139, 210
|
blue = 38, 139, 210
|
||||||
purple = 108, 113, 196
|
purple = 211, 54, 130
|
||||||
|
|
||||||
[Project]
|
[Project]
|
||||||
root = 42, 161, 152
|
root = 42, 161, 152
|
||||||
@@ -30,7 +30,7 @@ note = 181, 137, 0
|
|||||||
window = 238, 232, 213
|
window = 238, 232, 213
|
||||||
windowtext = 0, 43, 54
|
windowtext = 0, 43, 54
|
||||||
base = 253, 246, 227
|
base = 253, 246, 227
|
||||||
alternatebase = 238, 232, 213
|
alternatebase = 147, 161, 161
|
||||||
text = 0, 43, 54
|
text = 0, 43, 54
|
||||||
tooltipbase = 133, 153, 0
|
tooltipbase = 133, 153, 0
|
||||||
tooltiptext = 0, 43, 54
|
tooltiptext = 0, 43, 54
|
||||||
@@ -45,7 +45,4 @@ linkvisited = 38, 139, 210
|
|||||||
[GUI]
|
[GUI]
|
||||||
helptext = 78, 91, 95
|
helptext = 78, 91, 95
|
||||||
fadedtext = 78, 91, 95
|
fadedtext = 78, 91, 95
|
||||||
errortext = 255, 91, 95
|
errortext = 220, 50, 47
|
||||||
statusnone = 88, 110, 117
|
|
||||||
statussaved = 42, 161, 152
|
|
||||||
statusunsaved = 203, 75, 22
|
|
||||||
|
|||||||
+18
-12
@@ -49,6 +49,11 @@ from novelwriter.error import formatException, logException
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEF_GUI = "default"
|
||||||
|
DEF_SYNTAX = "default_light"
|
||||||
|
DEF_ICONS = "material_rounded_normal"
|
||||||
|
DEF_TREECOL = "theme"
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
||||||
@@ -137,18 +142,18 @@ class Config:
|
|||||||
|
|
||||||
# General GUI Settings
|
# General GUI Settings
|
||||||
self.guiLocale = self._qLocale.name()
|
self.guiLocale = self._qLocale.name()
|
||||||
self.guiTheme = "default" # GUI theme
|
self.guiTheme = DEF_GUI # GUI theme
|
||||||
self.guiSyntax = "default_light" # Syntax theme
|
self.guiSyntax = DEF_SYNTAX # Syntax theme
|
||||||
self.guiFont = QFont() # Main GUI font
|
self.guiFont = QFont() # Main GUI font
|
||||||
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
||||||
self.hideHScroll = False # Hide horizontal 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.lastNotes = "0x0" # The latest release notes that have been shown
|
||||||
self.nativeFont = True # Use native font dialog
|
self.nativeFont = True # Use native font dialog
|
||||||
|
|
||||||
# Icons
|
# Icons
|
||||||
self.iconTheme = "material_rounded_normal" # Icons theme
|
self.iconTheme = DEF_ICONS # Icons theme
|
||||||
self.iconColTree = "theme" # Project tree icon colours
|
self.iconColTree = DEF_TREECOL # Project tree icon colours
|
||||||
self.iconColDocs = False # Keep theme colours on documents
|
self.iconColDocs = False # Keep theme colours on documents
|
||||||
|
|
||||||
# Size Settings
|
# Size Settings
|
||||||
self.mainWinSize = [1200, 650] # Last size of the main GUI window
|
self.mainWinSize = [1200, 650] # Last size of the main GUI window
|
||||||
@@ -477,8 +482,9 @@ class Config:
|
|||||||
# Config Actions
|
# Config Actions
|
||||||
##
|
##
|
||||||
|
|
||||||
def initConfig(self, confPath: str | Path | None = None,
|
def initConfig(
|
||||||
dataPath: str | Path | None = None) -> None:
|
self, confPath: str | Path | None = None, dataPath: str | Path | None = None
|
||||||
|
) -> None:
|
||||||
"""Initialise the config class. The manual setting of confPath
|
"""Initialise the config class. The manual setting of confPath
|
||||||
and dataPath is mainly intended for the test suite.
|
and dataPath is mainly intended for the test suite.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class GuiAbout(NDialog):
|
|||||||
self.resize(700, 500)
|
self.resize(700, 500)
|
||||||
|
|
||||||
# Logo and Banner
|
# Logo and Banner
|
||||||
self.nwImage = SHARED.theme.loadDecoration("nw-text", h=36)
|
self.nwImage = SHARED.theme.getDecoration("nw-text", h=36)
|
||||||
|
|
||||||
self.nwLogo = QLabel(self)
|
self.nwLogo = QLabel(self)
|
||||||
self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (128, 128)))
|
self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (128, 128)))
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from PyQt6.QtWidgets import (
|
|||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.common import compact, describeFont, uniqueCompact
|
from novelwriter.common import compact, describeFont, uniqueCompact
|
||||||
|
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL
|
||||||
from novelwriter.constants import nwLabels, nwUnicode, trConst
|
from novelwriter.constants import nwLabels, nwUnicode, trConst
|
||||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||||
from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
|
from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
|
||||||
@@ -167,7 +168,7 @@ class GuiPreferences(NDialog):
|
|||||||
self.guiTheme.setMinimumWidth(200)
|
self.guiTheme.setMinimumWidth(200)
|
||||||
for theme, name in SHARED.theme.listThemes():
|
for theme, name in SHARED.theme.listThemes():
|
||||||
self.guiTheme.addItem(name, theme)
|
self.guiTheme.addItem(name, theme)
|
||||||
self.guiTheme.setCurrentData(CONFIG.guiTheme, "default")
|
self.guiTheme.setCurrentData(CONFIG.guiTheme, DEF_GUI)
|
||||||
|
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Colour theme"), self.guiTheme,
|
self.tr("Colour theme"), self.guiTheme,
|
||||||
@@ -179,7 +180,7 @@ class GuiPreferences(NDialog):
|
|||||||
self.iconTheme.setMinimumWidth(200)
|
self.iconTheme.setMinimumWidth(200)
|
||||||
for theme, name in SHARED.theme.iconCache.listThemes():
|
for theme, name in SHARED.theme.iconCache.listThemes():
|
||||||
self.iconTheme.addItem(name, theme)
|
self.iconTheme.addItem(name, theme)
|
||||||
self.iconTheme.setCurrentData(CONFIG.iconTheme, "material_rounded_bold")
|
self.iconTheme.setCurrentData(CONFIG.iconTheme, DEF_ICONS)
|
||||||
|
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Icon theme"), self.iconTheme,
|
self.tr("Icon theme"), self.iconTheme,
|
||||||
@@ -191,7 +192,7 @@ class GuiPreferences(NDialog):
|
|||||||
self.iconColTree.setMinimumWidth(200)
|
self.iconColTree.setMinimumWidth(200)
|
||||||
for key, label in nwLabels.THEME_COLORS.items():
|
for key, label in nwLabels.THEME_COLORS.items():
|
||||||
self.iconColTree.addItem(trConst(label), key)
|
self.iconColTree.addItem(trConst(label), key)
|
||||||
self.iconColTree.setCurrentData(CONFIG.iconColTree, "theme")
|
self.iconColTree.setCurrentData(CONFIG.iconColTree, DEF_TREECOL)
|
||||||
|
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Project tree icon colours"), self.iconColTree,
|
self.tr("Project tree icon colours"), self.iconColTree,
|
||||||
@@ -257,7 +258,7 @@ class GuiPreferences(NDialog):
|
|||||||
self.guiSyntax.setMinimumWidth(200)
|
self.guiSyntax.setMinimumWidth(200)
|
||||||
for syntax, name in SHARED.theme.listSyntax():
|
for syntax, name in SHARED.theme.listSyntax():
|
||||||
self.guiSyntax.addItem(name, syntax)
|
self.guiSyntax.addItem(name, syntax)
|
||||||
self.guiSyntax.setCurrentData(CONFIG.guiSyntax, "default_light")
|
self.guiSyntax.setCurrentData(CONFIG.guiSyntax, DEF_SYNTAX)
|
||||||
|
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Document colour theme"), self.guiSyntax,
|
self.tr("Document colour theme"), self.guiSyntax,
|
||||||
|
|||||||
@@ -88,31 +88,20 @@ class NSwitch(QAbstractButton):
|
|||||||
|
|
||||||
def paintEvent(self, event: QPaintEvent) -> None:
|
def paintEvent(self, event: QPaintEvent) -> None:
|
||||||
"""Drawing the switch itself."""
|
"""Drawing the switch itself."""
|
||||||
|
palette = self.palette()
|
||||||
|
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
painter.setRenderHint(QtPaintAntiAlias, True)
|
painter.setRenderHint(QtPaintAntiAlias, True)
|
||||||
painter.setPen(QtNoPen)
|
painter.setOpacity(1.0 if self.isEnabled() else 0.5)
|
||||||
|
|
||||||
palette = self.palette()
|
painter.setPen(palette.mid().color())
|
||||||
if self.isChecked():
|
painter.setBrush(palette.highlight() if self.isChecked() else palette.alternateBase())
|
||||||
trackBrush = palette.highlight()
|
|
||||||
thumbBrush = palette.highlightedText()
|
|
||||||
else:
|
|
||||||
trackBrush = palette.midlight()
|
|
||||||
thumbBrush = palette.light()
|
|
||||||
|
|
||||||
if self.isEnabled():
|
|
||||||
trackOpacity = 1.0
|
|
||||||
else:
|
|
||||||
trackOpacity = 0.6
|
|
||||||
trackBrush = palette.mid()
|
|
||||||
thumbBrush = palette.mid()
|
|
||||||
|
|
||||||
painter.setBrush(trackBrush)
|
|
||||||
painter.setOpacity(trackOpacity)
|
|
||||||
painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR)
|
painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR)
|
||||||
|
|
||||||
painter.setBrush(thumbBrush)
|
painter.setPen(QtNoPen)
|
||||||
|
painter.setBrush(palette.highlightedText())
|
||||||
painter.drawEllipse(self._offset - self._rR, 2, self._rH, self._rH)
|
painter.drawEllipse(self._offset - self._rR, 2, self._rH, self._rH)
|
||||||
|
|
||||||
painter.end()
|
painter.end()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -133,9 +133,9 @@ class GuiMainStatus(QStatusBar):
|
|||||||
self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx))
|
self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx))
|
||||||
self.timeIcon.setPixmap(self.timePixmap)
|
self.timeIcon.setPixmap(self.timePixmap)
|
||||||
|
|
||||||
colNone = SHARED.theme.statNone
|
colNone = SHARED.theme.getIconColor("default").darker(150)
|
||||||
colSaved = SHARED.theme.statSaved
|
colSaved = SHARED.theme.getIconColor("green").darker(150)
|
||||||
colUnsaved = SHARED.theme.statUnsaved
|
colUnsaved = SHARED.theme.getIconColor("red").darker(150)
|
||||||
self.docIcon.setColors(colNone, colSaved, colUnsaved)
|
self.docIcon.setColors(colNone, colSaved, colUnsaved)
|
||||||
self.projIcon.setColors(colNone, colSaved, colUnsaved)
|
self.projIcon.setColors(colNone, colSaved, colUnsaved)
|
||||||
|
|
||||||
|
|||||||
+162
-152
@@ -38,10 +38,11 @@ from PyQt6.QtWidgets import QApplication
|
|||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.common import NWConfigParser, cssCol, minmax
|
from novelwriter.common import NWConfigParser, cssCol, minmax
|
||||||
|
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX
|
||||||
from novelwriter.constants import nwLabels
|
from novelwriter.constants import nwLabels
|
||||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
from novelwriter.types import QtPaintAntiAlias, QtTransparent
|
from novelwriter.types import QtBlack, QtPaintAntiAlias, QtTransparent
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -93,20 +94,17 @@ class GuiTheme:
|
|||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
# Attributes
|
# Attributes
|
||||||
"iconCache", "themeMeta", "isDarkTheme", "statNone", "statUnsaved",
|
"iconCache", "themeMeta", "isDarkTheme", "helpText", "fadedText", "errorText",
|
||||||
"statSaved", "helpText", "fadedText", "errorText", "syntaxMeta",
|
"syntaxMeta", "syntaxTheme", "guiFont", "guiFontB", "guiFontBU", "guiFontSmall",
|
||||||
"syntaxTheme", "guiFont", "guiFontB", "guiFontBU", "guiFontSmall",
|
"fontPointSize", "fontPixelSize", "baseIconHeight", "baseButtonHeight", "textNHeight",
|
||||||
"fontPointSize", "fontPixelSize", "baseIconHeight", "baseButtonHeight",
|
"textNWidth", "baseIconSize", "buttonIconSize", "guiFontFixed",
|
||||||
"textNHeight", "textNWidth", "baseIconSize", "buttonIconSize",
|
|
||||||
"guiFontFixed",
|
|
||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
"getIcon", "getPixmap", "getItemIcon", "getToggleIcon",
|
"getIcon", "getPixmap", "getItemIcon", "getIconColor", "getToggleIcon", "getDecoration",
|
||||||
"loadDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow",
|
"getHeaderDecoration", "getHeaderDecorationNarrow",
|
||||||
|
|
||||||
# Internal
|
# Internal
|
||||||
"_guiPalette", "_themeList", "_syntaxList", "_availThemes",
|
"_guiPalette", "_themeList", "_syntaxList", "_availThemes", "_availSyntax", "_styleSheets",
|
||||||
"_availSyntax", "_styleSheets",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -117,12 +115,10 @@ class GuiTheme:
|
|||||||
self.themeMeta = ThemeMeta()
|
self.themeMeta = ThemeMeta()
|
||||||
self.isDarkTheme = False
|
self.isDarkTheme = False
|
||||||
|
|
||||||
self.statNone = QColor(0, 0, 0)
|
# Special Text Colours
|
||||||
self.statUnsaved = QColor(0, 0, 0)
|
self.helpText = QColor(0, 0, 0)
|
||||||
self.statSaved = QColor(0, 0, 0)
|
self.fadedText = QColor(0, 0, 0)
|
||||||
self.helpText = QColor(0, 0, 0)
|
self.errorText = QColor(255, 0, 0)
|
||||||
self.fadedText = QColor(0, 0, 0)
|
|
||||||
self.errorText = QColor(255, 0, 0)
|
|
||||||
|
|
||||||
# Syntax Theme
|
# Syntax Theme
|
||||||
self.syntaxMeta = ThemeMeta()
|
self.syntaxMeta = ThemeMeta()
|
||||||
@@ -136,10 +132,10 @@ class GuiTheme:
|
|||||||
self._availSyntax: dict[str, Path] = {}
|
self._availSyntax: dict[str, Path] = {}
|
||||||
self._styleSheets: dict[str, str] = {}
|
self._styleSheets: dict[str, str] = {}
|
||||||
|
|
||||||
self._listConf(self._availSyntax, CONFIG.assetPath("syntax"))
|
_listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
|
||||||
self._listConf(self._availThemes, CONFIG.assetPath("themes"))
|
_listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
|
||||||
self._listConf(self._availSyntax, CONFIG.dataPath("syntax"))
|
_listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
|
||||||
self._listConf(self._availThemes, CONFIG.dataPath("themes"))
|
_listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
|
||||||
|
|
||||||
self.loadTheme()
|
self.loadTheme()
|
||||||
self.loadSyntax()
|
self.loadSyntax()
|
||||||
@@ -148,8 +144,9 @@ class GuiTheme:
|
|||||||
self.getIcon = self.iconCache.getIcon
|
self.getIcon = self.iconCache.getIcon
|
||||||
self.getPixmap = self.iconCache.getPixmap
|
self.getPixmap = self.iconCache.getPixmap
|
||||||
self.getItemIcon = self.iconCache.getItemIcon
|
self.getItemIcon = self.iconCache.getItemIcon
|
||||||
|
self.getIconColor = self.iconCache.getIconColor
|
||||||
self.getToggleIcon = self.iconCache.getToggleIcon
|
self.getToggleIcon = self.iconCache.getToggleIcon
|
||||||
self.loadDecoration = self.iconCache.loadDecoration
|
self.getDecoration = self.iconCache.getDecoration
|
||||||
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
|
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
|
||||||
self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
|
self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
|
||||||
|
|
||||||
@@ -213,25 +210,23 @@ class GuiTheme:
|
|||||||
|
|
||||||
def loadTheme(self) -> bool:
|
def loadTheme(self) -> bool:
|
||||||
"""Load the currently specified GUI theme."""
|
"""Load the currently specified GUI theme."""
|
||||||
guiTheme = CONFIG.guiTheme
|
theme = CONFIG.guiTheme
|
||||||
if guiTheme not in self._availThemes:
|
if theme not in self._availThemes:
|
||||||
logger.error("Could not find GUI theme '%s'", guiTheme)
|
logger.error("Could not find GUI theme '%s'", theme)
|
||||||
guiTheme = "default"
|
theme = DEF_GUI
|
||||||
CONFIG.guiTheme = guiTheme
|
CONFIG.guiTheme = theme
|
||||||
|
|
||||||
themeFile = self._availThemes.get(guiTheme, None)
|
if not (file := self._availThemes.get(theme)):
|
||||||
if themeFile is None:
|
|
||||||
logger.error("Could not load GUI theme")
|
logger.error("Could not load GUI theme")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Config File
|
logger.info("Loading GUI theme '%s'", theme)
|
||||||
logger.info("Loading GUI theme '%s'", guiTheme)
|
|
||||||
parser = NWConfigParser()
|
parser = NWConfigParser()
|
||||||
try:
|
try:
|
||||||
with open(themeFile, mode="r", encoding="utf-8") as inFile:
|
with open(file, mode="r", encoding="utf-8") as fo:
|
||||||
parser.read_file(inFile)
|
parser.read_file(fo)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not load theme settings from: %s", themeFile)
|
logger.error("Could not read file: %s", file)
|
||||||
logException()
|
logException()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -297,12 +292,9 @@ class GuiTheme:
|
|||||||
# GUI
|
# GUI
|
||||||
sec = "GUI"
|
sec = "GUI"
|
||||||
if parser.has_section(sec):
|
if parser.has_section(sec):
|
||||||
self.helpText = self._parseColour(parser, sec, "helptext")
|
self.helpText = self._parseColour(parser, sec, "helptext")
|
||||||
self.fadedText = self._parseColour(parser, sec, "fadedtext")
|
self.fadedText = self._parseColour(parser, sec, "fadedtext")
|
||||||
self.errorText = self._parseColour(parser, sec, "errortext")
|
self.errorText = self._parseColour(parser, sec, "errortext")
|
||||||
self.statNone = self._parseColour(parser, sec, "statusnone")
|
|
||||||
self.statUnsaved = self._parseColour(parser, sec, "statusunsaved")
|
|
||||||
self.statSaved = self._parseColour(parser, sec, "statussaved")
|
|
||||||
|
|
||||||
# Update Dependant Colours
|
# Update Dependant Colours
|
||||||
# Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp
|
# Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp
|
||||||
@@ -315,18 +307,24 @@ class GuiTheme:
|
|||||||
QtColInactive = QPalette.ColorGroup.Inactive
|
QtColInactive = QPalette.ColorGroup.Inactive
|
||||||
QtColDisabled = QPalette.ColorGroup.Disabled
|
QtColDisabled = QPalette.ColorGroup.Disabled
|
||||||
|
|
||||||
light = window.lighter(150)
|
if window.lightnessF() < 0.15:
|
||||||
mid = window.darker(130)
|
# If window is too dark, we need a lighter ref colour for shades
|
||||||
|
ref = QColor.fromHslF(window.hueF(), window.saturationF(), 0.15, window.alphaF())
|
||||||
|
else:
|
||||||
|
ref = window
|
||||||
|
|
||||||
|
light = ref.lighter(150)
|
||||||
|
mid = ref.darker(130)
|
||||||
midLight = mid.lighter(110)
|
midLight = mid.lighter(110)
|
||||||
dark = window.darker(150)
|
dark = ref.darker(150)
|
||||||
shadow = dark.darker(135)
|
shadow = dark.darker(135)
|
||||||
darkOff = dark.darker(150)
|
darkOff = dark.darker(150)
|
||||||
shadowOff = shadow.darker(150)
|
shadowOff = ref.darker(150)
|
||||||
|
|
||||||
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
|
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
|
||||||
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
||||||
|
|
||||||
placeholder = text
|
placeholder = QColor(text)
|
||||||
placeholder.setAlpha(128)
|
placeholder.setAlpha(128)
|
||||||
|
|
||||||
self._guiPalette.setBrush(QPalette.ColorRole.Light, light)
|
self._guiPalette.setBrush(QPalette.ColorRole.Light, light)
|
||||||
@@ -353,7 +351,7 @@ class GuiTheme:
|
|||||||
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
|
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
|
||||||
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
|
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
|
||||||
|
|
||||||
# Load icons after theme is parsed
|
# Load icons after the theme is parsed
|
||||||
self.iconCache.loadTheme(CONFIG.iconTheme)
|
self.iconCache.loadTheme(CONFIG.iconTheme)
|
||||||
|
|
||||||
# Finalise
|
# Finalise
|
||||||
@@ -365,25 +363,23 @@ class GuiTheme:
|
|||||||
|
|
||||||
def loadSyntax(self) -> bool:
|
def loadSyntax(self) -> bool:
|
||||||
"""Load the currently specified syntax highlighter theme."""
|
"""Load the currently specified syntax highlighter theme."""
|
||||||
guiSyntax = CONFIG.guiSyntax
|
theme = CONFIG.guiSyntax
|
||||||
if guiSyntax not in self._availSyntax:
|
if theme not in self._availSyntax:
|
||||||
logger.error("Could not find syntax theme '%s'", guiSyntax)
|
logger.error("Could not find syntax theme '%s'", theme)
|
||||||
guiSyntax = "default_light"
|
theme = DEF_SYNTAX
|
||||||
CONFIG.guiSyntax = guiSyntax
|
CONFIG.guiSyntax = theme
|
||||||
|
|
||||||
syntaxFile = self._availSyntax.get(guiSyntax, None)
|
if not (file := self._availSyntax.get(theme)):
|
||||||
if syntaxFile is None:
|
|
||||||
logger.error("Could not load syntax theme")
|
logger.error("Could not load syntax theme")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info("Loading syntax theme '%s'", guiSyntax)
|
logger.info("Loading syntax theme '%s'", theme)
|
||||||
|
|
||||||
parser = NWConfigParser()
|
parser = NWConfigParser()
|
||||||
try:
|
try:
|
||||||
with open(syntaxFile, mode="r", encoding="utf-8") as inFile:
|
with open(file, mode="r", encoding="utf-8") as fo:
|
||||||
parser.read_file(inFile)
|
parser.read_file(fo)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not load syntax colours from: %s", syntaxFile)
|
logger.error("Could not read file: %s", file)
|
||||||
logException()
|
logException()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -434,13 +430,14 @@ class GuiTheme:
|
|||||||
if self._themeList:
|
if self._themeList:
|
||||||
return self._themeList
|
return self._themeList
|
||||||
|
|
||||||
|
themes = []
|
||||||
parser = NWConfigParser()
|
parser = NWConfigParser()
|
||||||
for key, path in self._availThemes.items():
|
for key, path in self._availThemes.items():
|
||||||
logger.debug("Checking theme config for '%s'", key)
|
logger.debug("Checking theme config '%s'", key)
|
||||||
if name := _loadInternalName(parser, path):
|
if name := _loadInternalName(parser, path):
|
||||||
self._themeList.append((key, name))
|
themes.append((key, name))
|
||||||
|
|
||||||
self._themeList = sorted(self._themeList, key=_sortTheme)
|
self._themeList = sorted(themes, key=_sortTheme)
|
||||||
|
|
||||||
return self._themeList
|
return self._themeList
|
||||||
|
|
||||||
@@ -449,13 +446,14 @@ class GuiTheme:
|
|||||||
if self._syntaxList:
|
if self._syntaxList:
|
||||||
return self._syntaxList
|
return self._syntaxList
|
||||||
|
|
||||||
|
themes = []
|
||||||
parser = NWConfigParser()
|
parser = NWConfigParser()
|
||||||
for key, path in self._availSyntax.items():
|
for key, path in self._availSyntax.items():
|
||||||
logger.debug("Checking theme syntax for '%s'", key)
|
logger.debug("Checking theme syntax '%s'", key)
|
||||||
if name := _loadInternalName(parser, path):
|
if name := _loadInternalName(parser, path):
|
||||||
self._syntaxList.append((key, name))
|
themes.append((key, name))
|
||||||
|
|
||||||
self._syntaxList = sorted(self._syntaxList, key=_sortTheme)
|
self._syntaxList = sorted(themes, key=_sortTheme)
|
||||||
|
|
||||||
return self._syntaxList
|
return self._syntaxList
|
||||||
|
|
||||||
@@ -478,7 +476,6 @@ class GuiTheme:
|
|||||||
# Reset GUI Palette
|
# Reset GUI Palette
|
||||||
faded = QColor(128, 128, 128)
|
faded = QColor(128, 128, 128)
|
||||||
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
||||||
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
|
|
||||||
red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41)
|
red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41)
|
||||||
orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31)
|
orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31)
|
||||||
yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0)
|
yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0)
|
||||||
@@ -487,12 +484,10 @@ class GuiTheme:
|
|||||||
blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174)
|
blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174)
|
||||||
purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168)
|
purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168)
|
||||||
|
|
||||||
self.statNone = grey
|
# Text Colours
|
||||||
self.statUnsaved = red
|
self.helpText = dimmed
|
||||||
self.statSaved = green
|
self.fadedText = faded
|
||||||
self.helpText = dimmed
|
self.errorText = red
|
||||||
self.fadedText = faded
|
|
||||||
self.errorText = red
|
|
||||||
|
|
||||||
self._guiPalette = palette
|
self._guiPalette = palette
|
||||||
|
|
||||||
@@ -518,17 +513,6 @@ class GuiTheme:
|
|||||||
|
|
||||||
return
|
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) -> QColor:
|
def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor:
|
||||||
"""Parse a colour value from a config string."""
|
"""Parse a colour value from a config string."""
|
||||||
return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
|
return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
|
||||||
@@ -581,8 +565,9 @@ class GuiIcons:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"mainTheme", "themeMeta", "_svgData", "_svgColours", "_qIcons",
|
"mainTheme", "themeMeta", "_svgData", "_svgColors", "_qColors",
|
||||||
"_headerDec", "_headerDecNarrow", "_themeList", "_iconPath", "_noIcon",
|
"_qIcons", "_headerDec", "_headerDecNarrow", "_availThemes",
|
||||||
|
"_themeList", "_noIcon",
|
||||||
)
|
)
|
||||||
|
|
||||||
TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = {
|
TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = {
|
||||||
@@ -601,24 +586,29 @@ class GuiIcons:
|
|||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
self._svgData: dict[str, bytes] = {}
|
self._svgData: dict[str, bytes] = {}
|
||||||
self._svgColours: dict[str, bytes] = {}
|
self._svgColors: dict[str, bytes] = {}
|
||||||
|
self._qColors: dict[str, QColor] = {}
|
||||||
self._qIcons: dict[str, QIcon] = {}
|
self._qIcons: dict[str, QIcon] = {}
|
||||||
self._headerDec: list[QPixmap] = []
|
self._headerDec: list[QPixmap] = []
|
||||||
self._headerDecNarrow: list[QPixmap] = []
|
self._headerDecNarrow: list[QPixmap] = []
|
||||||
|
|
||||||
# Icon Theme Path
|
# Icon Theme Path
|
||||||
|
self._availThemes: dict[str, Path] = {}
|
||||||
self._themeList: list[tuple[str, str]] = []
|
self._themeList: list[tuple[str, str]] = []
|
||||||
self._iconPath = CONFIG.assetPath("icons")
|
|
||||||
|
|
||||||
# None Icon
|
# None Icon
|
||||||
self._noIcon = QIcon(str(self._iconPath / "none.svg"))
|
self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
|
||||||
|
|
||||||
|
_listConf(self._availThemes, CONFIG.assetPath("icons"), ".icons")
|
||||||
|
_listConf(self._availThemes, CONFIG.dataPath("icons"), ".icons")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""Clear the icon cache."""
|
"""Clear the icon cache."""
|
||||||
self._svgData = {}
|
self._svgData = {}
|
||||||
self._svgColours = {}
|
self._svgColors = {}
|
||||||
|
self._qColors = {}
|
||||||
self._qIcons = {}
|
self._qIcons = {}
|
||||||
self._headerDec = []
|
self._headerDec = []
|
||||||
self._headerDecNarrow = []
|
self._headerDecNarrow = []
|
||||||
@@ -629,16 +619,24 @@ class GuiIcons:
|
|||||||
# Actions
|
# Actions
|
||||||
##
|
##
|
||||||
|
|
||||||
def loadTheme(self, iconTheme: str) -> bool:
|
def loadTheme(self, theme: str) -> bool:
|
||||||
"""Update the theme map. This is more of an init, since many of
|
"""Update the theme map. This is more of an init, since many of
|
||||||
the GUI icons cannot really be replaced without writing specific
|
the GUI icons cannot really be replaced without writing specific
|
||||||
update functions for the classes where they're used.
|
update functions for the classes where they're used.
|
||||||
"""
|
"""
|
||||||
logger.info("Loading icon theme '%s'", iconTheme)
|
if theme not in self._availThemes:
|
||||||
themePath = self._iconPath / f"{iconTheme}.icons"
|
logger.error("Could not find icon theme '%s'", theme)
|
||||||
|
theme = DEF_ICONS
|
||||||
|
CONFIG.iconTheme = theme
|
||||||
|
|
||||||
|
if not (file := self._availThemes.get(theme)):
|
||||||
|
logger.error("Could not load icon theme")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("Loading icon theme '%s'", theme)
|
||||||
try:
|
try:
|
||||||
meta = ThemeMeta()
|
meta = ThemeMeta()
|
||||||
with open(themePath, mode="r", encoding="utf-8") as icons:
|
with open(file, mode="r", encoding="utf-8") as icons:
|
||||||
for icon in icons:
|
for icon in icons:
|
||||||
bits = icon.partition("=")
|
bits = icon.partition("=")
|
||||||
key = bits[0].strip()
|
key = bits[0].strip()
|
||||||
@@ -654,58 +652,37 @@ class GuiIcons:
|
|||||||
meta.license = value
|
meta.license = value
|
||||||
self.themeMeta = meta
|
self.themeMeta = meta
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not load icon theme from: %s", themePath)
|
logger.error("Could not read file: %s", file)
|
||||||
logException()
|
logException()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Set colour overrides for project item icons
|
# Set colour overrides for project item icons
|
||||||
if (override := CONFIG.iconColTree) != "theme":
|
if (override := CONFIG.iconColTree) != "theme":
|
||||||
color = self._svgColours.get(override, b"#000000")
|
color = self._svgColors.get(override, b"#000000")
|
||||||
self._svgColours["root"] = color
|
self._svgColors["root"] = color
|
||||||
self._svgColours["folder"] = color
|
self._svgColors["folder"] = color
|
||||||
if not CONFIG.iconColDocs:
|
if not CONFIG.iconColDocs:
|
||||||
self._svgColours["file"] = color
|
self._svgColors["file"] = color
|
||||||
self._svgColours["title"] = color
|
self._svgColors["title"] = color
|
||||||
self._svgColours["chapter"] = color
|
self._svgColors["chapter"] = color
|
||||||
self._svgColours["scene"] = color
|
self._svgColors["scene"] = color
|
||||||
self._svgColours["note"] = color
|
self._svgColors["note"] = color
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setIconColor(self, key: str, color: QColor) -> None:
|
def setIconColor(self, key: str, color: QColor) -> None:
|
||||||
"""Set an icon colour for a named colour."""
|
"""Set an icon colour for a named colour."""
|
||||||
self._svgColours[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
|
self._qColors[key] = QColor(color)
|
||||||
|
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Access Functions
|
# Access Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def loadDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
|
def getIconColor(self, name: str) -> QColor:
|
||||||
"""Load graphical decoration element based on the decoration
|
"""Return an icon color."""
|
||||||
map or the icon map. This function always returns a QPixmap.
|
return QColor(self._qColors.get(name) or QtBlack)
|
||||||
"""
|
|
||||||
if name in self.IMAGE_MAP:
|
|
||||||
idx = int(self.mainTheme.isDarkTheme)
|
|
||||||
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
|
|
||||||
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))
|
|
||||||
tMode = Qt.TransformationMode.SmoothTransformation
|
|
||||||
if w is not None and h is not None:
|
|
||||||
return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
|
|
||||||
elif w is None and h is not None:
|
|
||||||
return pixmap.scaledToHeight(h, tMode)
|
|
||||||
elif w is not None and h is None:
|
|
||||||
return pixmap.scaledToWidth(w, tMode)
|
|
||||||
|
|
||||||
return pixmap
|
|
||||||
|
|
||||||
def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
|
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."""
|
"""Return an icon from the icon buffer, or load it."""
|
||||||
@@ -729,13 +706,6 @@ class GuiIcons:
|
|||||||
return icon
|
return icon
|
||||||
return self._noIcon
|
return self._noIcon
|
||||||
|
|
||||||
def getPixmap(self, name: str, size: tuple[int, int], color: str | None = None) -> QPixmap:
|
|
||||||
"""Return an icon from the icon buffer as a QPixmap. If it
|
|
||||||
doesn't exist, return an empty QPixmap.
|
|
||||||
"""
|
|
||||||
w, h = size
|
|
||||||
return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal)
|
|
||||||
|
|
||||||
def getItemIcon(
|
def getItemIcon(
|
||||||
self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0"
|
self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0"
|
||||||
) -> QIcon:
|
) -> QIcon:
|
||||||
@@ -772,6 +742,39 @@ class GuiIcons:
|
|||||||
|
|
||||||
return self.getIcon(name, color)
|
return self.getIcon(name, color)
|
||||||
|
|
||||||
|
def getPixmap(self, name: str, size: tuple[int, int], color: str | None = None) -> QPixmap:
|
||||||
|
"""Return an icon from the icon buffer as a QPixmap. If it
|
||||||
|
doesn't exist, return an empty QPixmap.
|
||||||
|
"""
|
||||||
|
w, h = size
|
||||||
|
return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal)
|
||||||
|
|
||||||
|
def getDecoration(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.IMAGE_MAP:
|
||||||
|
idx = int(self.mainTheme.isDarkTheme)
|
||||||
|
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
|
||||||
|
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))
|
||||||
|
tMode = Qt.TransformationMode.SmoothTransformation
|
||||||
|
if w is not None and h is not None:
|
||||||
|
return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
|
||||||
|
elif w is None and h is not None:
|
||||||
|
return pixmap.scaledToHeight(h, tMode)
|
||||||
|
elif w is not None and h is None:
|
||||||
|
return pixmap.scaledToWidth(w, tMode)
|
||||||
|
|
||||||
|
return pixmap
|
||||||
|
|
||||||
def getHeaderDecoration(self, hLevel: int) -> QPixmap:
|
def getHeaderDecoration(self, hLevel: int) -> QPixmap:
|
||||||
"""Get the decoration for a specific heading level."""
|
"""Get the decoration for a specific heading level."""
|
||||||
if not self._headerDec:
|
if not self._headerDec:
|
||||||
@@ -804,12 +807,13 @@ class GuiIcons:
|
|||||||
if self._themeList:
|
if self._themeList:
|
||||||
return self._themeList
|
return self._themeList
|
||||||
|
|
||||||
for item in self._iconPath.iterdir():
|
themes = []
|
||||||
if item.is_file() and item.suffix == ".icons":
|
for key, path in self._availThemes.items():
|
||||||
if name := _loadIconName(item):
|
logger.debug("Checking icon theme '%s'", key)
|
||||||
self._themeList.append((item.stem, name))
|
if name := _loadIconName(path):
|
||||||
|
themes.append((key, name))
|
||||||
|
|
||||||
self._themeList = sorted(self._themeList, key=_sortTheme)
|
self._themeList = sorted(themes, key=_sortTheme)
|
||||||
|
|
||||||
return self._themeList
|
return self._themeList
|
||||||
|
|
||||||
@@ -823,12 +827,12 @@ class GuiIcons:
|
|||||||
"""
|
"""
|
||||||
# If we just want the app icons, return right away
|
# If we just want the app icons, return right away
|
||||||
if name == "novelwriter":
|
if name == "novelwriter":
|
||||||
return QIcon(str(self._iconPath / "novelwriter.svg"))
|
return QIcon(str(CONFIG.assetPath("icons") / "novelwriter.svg"))
|
||||||
elif name == "proj_nwx":
|
elif name == "proj_nwx":
|
||||||
return QIcon(str(self._iconPath / "x-novelwriter-project.svg"))
|
return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg"))
|
||||||
|
|
||||||
if svg := self._svgData.get(name, b""):
|
if svg := self._svgData.get(name, b""):
|
||||||
if fill := self._svgColours.get(color or "default"):
|
if fill := self._svgColors.get(color or "default"):
|
||||||
svg = svg.replace(b"#000000", fill)
|
svg = svg.replace(b"#000000", fill)
|
||||||
pixmap = QPixmap(w, h)
|
pixmap = QPixmap(w, h)
|
||||||
pixmap.fill(QtTransparent)
|
pixmap.fill(QtTransparent)
|
||||||
@@ -850,7 +854,7 @@ class GuiIcons:
|
|||||||
|
|
||||||
painter = QPainter(pixmap)
|
painter = QPainter(pixmap)
|
||||||
painter.setRenderHint(QtPaintAntiAlias)
|
painter.setRenderHint(QtPaintAntiAlias)
|
||||||
if fill := self._svgColours.get(color or "default"):
|
if fill := self._svgColors.get(color or "default"):
|
||||||
painter.fillPath(path, QColor(fill.decode(encoding="utf-8")))
|
painter.fillPath(path, QColor(fill.decode(encoding="utf-8")))
|
||||||
painter.end()
|
painter.end()
|
||||||
|
|
||||||
@@ -861,6 +865,14 @@ class GuiIcons:
|
|||||||
# Module Functions
|
# Module Functions
|
||||||
# ================
|
# ================
|
||||||
|
|
||||||
|
def _listConf(target: dict, path: Path, extension: str) -> None:
|
||||||
|
"""Scan for theme files and populate the dictionary."""
|
||||||
|
if path.is_dir():
|
||||||
|
for item in path.iterdir():
|
||||||
|
if item.is_file() and item.name.endswith(extension):
|
||||||
|
target[item.stem] = item
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def _sortTheme(data: tuple[str, str]) -> str:
|
def _sortTheme(data: tuple[str, str]) -> str:
|
||||||
"""Key function for theme sorting."""
|
"""Key function for theme sorting."""
|
||||||
@@ -868,17 +880,16 @@ def _sortTheme(data: tuple[str, str]) -> str:
|
|||||||
return f"*{name}" if key.startswith("default_") else name
|
return f"*{name}" if key.startswith("default_") else name
|
||||||
|
|
||||||
|
|
||||||
def _loadInternalName(confParser: NWConfigParser, confFile: str | Path) -> str:
|
def _loadInternalName(parser: NWConfigParser, path: str | Path) -> str:
|
||||||
"""Open a conf file and read the 'name' setting."""
|
"""Open a conf file and read the 'name' setting."""
|
||||||
try:
|
try:
|
||||||
with open(confFile, mode="r", encoding="utf-8") as inFile:
|
with open(path, mode="r", encoding="utf-8") as inFile:
|
||||||
confParser.read_file(inFile)
|
parser.read_file(inFile)
|
||||||
|
return parser.rdStr("Main", "name", "")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not load file: %s", confFile)
|
logger.error("Could not read file: %s", path)
|
||||||
logException()
|
logException()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
return confParser.rdStr("Main", "name", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _loadIconName(path: Path) -> str:
|
def _loadIconName(path: Path) -> str:
|
||||||
@@ -890,7 +901,6 @@ def _loadIconName(path: Path) -> str:
|
|||||||
if key.strip() == "meta:name":
|
if key.strip() == "meta:name":
|
||||||
return value.strip()
|
return value.strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not load file: %s", path)
|
logger.error("Could not read file: %s", path)
|
||||||
logException()
|
logException()
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ class GuiWelcome(NDialog):
|
|||||||
# Elements
|
# Elements
|
||||||
# ========
|
# ========
|
||||||
|
|
||||||
self.bgImage = SHARED.theme.loadDecoration("welcome")
|
self.bgImage = SHARED.theme.getDecoration("welcome")
|
||||||
self.nwImage = SHARED.theme.loadDecoration("nw-text", h=36)
|
self.nwImage = SHARED.theme.getDecoration("nw-text", h=36)
|
||||||
self.bgColor = QColor(54, 54, 54) if SHARED.theme.isDarkTheme else QColor(255, 255, 255)
|
self.bgColor = QColor(54, 54, 54) if SHARED.theme.isDarkTheme else QColor(255, 255, 255)
|
||||||
|
|
||||||
self.nwLogo = QLabel(self)
|
self.nwLogo = QLabel(self)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent
|
|||||||
from PyQt6.QtWidgets import QFileDialog, QFontDialog
|
from PyQt6.QtWidgets import QFileDialog, QFontDialog
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
|
from novelwriter.config import DEF_GUI
|
||||||
from novelwriter.constants import nwUnicode
|
from novelwriter.constants import nwUnicode
|
||||||
from novelwriter.dialogs.preferences import GuiPreferences
|
from novelwriter.dialogs.preferences import GuiPreferences
|
||||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||||
@@ -56,7 +57,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
# Check GUI Themes
|
# Check GUI Themes
|
||||||
themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())]
|
themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())]
|
||||||
assert len(themes) >= 5
|
assert len(themes) >= 5
|
||||||
assert "default" in themes
|
assert DEF_GUI in themes
|
||||||
|
|
||||||
# Check GUI Syntax
|
# Check GUI Syntax
|
||||||
syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())]
|
syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())]
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap
|
|||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.common import NWConfigParser
|
from novelwriter.common import NWConfigParser
|
||||||
|
from novelwriter.config import DEF_GUI
|
||||||
from novelwriter.constants import nwLabels
|
from novelwriter.constants import nwLabels
|
||||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||||
|
from novelwriter.gui.theme import _listConf
|
||||||
|
|
||||||
from tests.mocked import causeOSError
|
from tests.mocked import causeOSError
|
||||||
from tests.tools import writeFile
|
from tests.tools import writeFile
|
||||||
@@ -50,15 +52,16 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
|
|||||||
# Scan for Themes
|
# Scan for Themes
|
||||||
# ===============
|
# ===============
|
||||||
|
|
||||||
assert mainTheme._listConf({}, Path("not_a_path")) is False
|
result = {}
|
||||||
|
_listConf({}, Path("not_a_path"), ".conf")
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
themeOne = tstPaths.cnfDir / "themes" / "themeone.conf"
|
themeOne = tstPaths.cnfDir / "themes" / "themeone.conf"
|
||||||
themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf"
|
themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf"
|
||||||
writeFile(themeOne, "# Stuff")
|
writeFile(themeOne, "# Stuff")
|
||||||
writeFile(themeTwo, "# Stuff")
|
writeFile(themeTwo, "# Stuff")
|
||||||
|
|
||||||
result = {}
|
_listConf(result, tstPaths.cnfDir / "themes", ".conf")
|
||||||
assert mainTheme._listConf(result, tstPaths.cnfDir / "themes") is True
|
|
||||||
assert result["themeone"] == themeOne
|
assert result["themeone"] == themeOne
|
||||||
assert result["themetwo"] == themeTwo
|
assert result["themetwo"] == themeTwo
|
||||||
|
|
||||||
@@ -135,7 +138,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
mainTheme._availThemes = availThemes
|
mainTheme._availThemes = availThemes
|
||||||
|
|
||||||
# Check handling of unreadable file
|
# Check handling of unreadable file
|
||||||
CONFIG.guiTheme = "default"
|
CONFIG.guiTheme = DEF_GUI
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("builtins.open", causeOSError)
|
mp.setattr("builtins.open", causeOSError)
|
||||||
assert mainTheme.loadTheme() is False
|
assert mainTheme.loadTheme() is False
|
||||||
@@ -147,7 +150,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
|
mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
|
||||||
|
|
||||||
# Load the default theme
|
# Load the default theme
|
||||||
CONFIG.guiTheme = "default"
|
CONFIG.guiTheme = DEF_GUI
|
||||||
assert mainTheme.loadTheme() is True
|
assert mainTheme.loadTheme() is True
|
||||||
|
|
||||||
# This should load a standard palette
|
# This should load a standard palette
|
||||||
@@ -181,13 +184,17 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
|
|
||||||
# Check a few values
|
# Check a few values
|
||||||
assert mainTheme._guiPalette.color(
|
assert mainTheme._guiPalette.color(
|
||||||
QPalette.ColorRole.Window).getRgb() == (239, 239, 239, 255)
|
QPalette.ColorRole.Window
|
||||||
|
).getRgb() == (239, 239, 239, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert mainTheme._guiPalette.color(
|
||||||
QPalette.ColorRole.WindowText).getRgb() == (0, 0, 0, 255)
|
QPalette.ColorRole.WindowText
|
||||||
|
).getRgb() == (0, 0, 0, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert mainTheme._guiPalette.color(
|
||||||
QPalette.ColorRole.Base).getRgb() == (255, 255, 255, 255)
|
QPalette.ColorRole.Base
|
||||||
|
).getRgb() == (255, 255, 255, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert mainTheme._guiPalette.color(
|
||||||
QPalette.ColorRole.AlternateBase).getRgb() == (239, 239, 239, 255)
|
QPalette.ColorRole.AlternateBase
|
||||||
|
).getRgb() == (224, 224, 224, 255)
|
||||||
|
|
||||||
# Load Default Dark Theme
|
# Load Default Dark Theme
|
||||||
# =======================
|
# =======================
|
||||||
@@ -280,7 +287,10 @@ def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
|
|||||||
# ==========
|
# ==========
|
||||||
|
|
||||||
# Invalid theme name
|
# Invalid theme name
|
||||||
|
availThemes = iconCache._availThemes
|
||||||
|
iconCache._availThemes = {}
|
||||||
assert iconCache.loadTheme("not_a_theme") is False
|
assert iconCache.loadTheme("not_a_theme") is False
|
||||||
|
iconCache._availThemes = availThemes
|
||||||
|
|
||||||
# Check handling of unreadable file
|
# Check handling of unreadable file
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
@@ -292,29 +302,29 @@ def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
|
|||||||
assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium"
|
assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium"
|
||||||
|
|
||||||
# Load with project colour override
|
# Load with project colour override
|
||||||
purple = iconCache._svgColours["purple"]
|
purple = iconCache._svgColors["purple"]
|
||||||
assert iconCache._svgColours["root"] != purple
|
assert iconCache._svgColors["root"] != purple
|
||||||
assert iconCache._svgColours["folder"] != purple
|
assert iconCache._svgColors["folder"] != purple
|
||||||
assert iconCache._svgColours["file"] != purple
|
assert iconCache._svgColors["file"] != purple
|
||||||
assert iconCache._svgColours["title"] != purple
|
assert iconCache._svgColors["title"] != purple
|
||||||
assert iconCache._svgColours["chapter"] != purple
|
assert iconCache._svgColors["chapter"] != purple
|
||||||
assert iconCache._svgColours["scene"] != purple
|
assert iconCache._svgColors["scene"] != purple
|
||||||
assert iconCache._svgColours["note"] != purple
|
assert iconCache._svgColors["note"] != purple
|
||||||
|
|
||||||
CONFIG.iconColTree = "purple"
|
CONFIG.iconColTree = "purple"
|
||||||
assert iconCache.loadTheme("material_rounded_normal") is True
|
assert iconCache.loadTheme("material_rounded_normal") is True
|
||||||
assert iconCache._svgColours["root"] == purple
|
assert iconCache._svgColors["root"] == purple
|
||||||
assert iconCache._svgColours["folder"] == purple
|
assert iconCache._svgColors["folder"] == purple
|
||||||
assert iconCache._svgColours["file"] == purple
|
assert iconCache._svgColors["file"] == purple
|
||||||
assert iconCache._svgColours["title"] == purple
|
assert iconCache._svgColors["title"] == purple
|
||||||
assert iconCache._svgColours["chapter"] == purple
|
assert iconCache._svgColors["chapter"] == purple
|
||||||
assert iconCache._svgColours["scene"] == purple
|
assert iconCache._svgColors["scene"] == purple
|
||||||
assert iconCache._svgColours["note"] == purple
|
assert iconCache._svgColors["note"] == purple
|
||||||
|
|
||||||
# Change some colours
|
# Change some colours
|
||||||
iconCache.setIconColor("root", QColor(255, 255, 255))
|
iconCache.setIconColor("root", QColor(255, 255, 255))
|
||||||
assert iconCache._svgColours["root"] != purple
|
assert iconCache._svgColors["root"] != purple
|
||||||
assert iconCache._svgColours["root"] == b"#ffffff"
|
assert iconCache._svgColors["root"] == b"#ffffff"
|
||||||
|
|
||||||
# List Themes
|
# List Themes
|
||||||
# ===========
|
# ===========
|
||||||
@@ -453,31 +463,31 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
|
|||||||
# ================
|
# ================
|
||||||
|
|
||||||
# Invalid name should return empty pixmap
|
# Invalid name should return empty pixmap
|
||||||
qPix = iconCache.loadDecoration("stuff")
|
qPix = iconCache.getDecoration("stuff")
|
||||||
assert qPix.isNull() is True
|
assert qPix.isNull() is True
|
||||||
|
|
||||||
# Load an image
|
# Load an image
|
||||||
qPix = iconCache.loadDecoration("welcome")
|
qPix = iconCache.getDecoration("welcome")
|
||||||
assert qPix.isNull() is False
|
assert qPix.isNull() is False
|
||||||
|
|
||||||
# Fail finding the file
|
# Fail finding the file
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("pathlib.Path.is_file", lambda *a: False)
|
mp.setattr("pathlib.Path.is_file", lambda *a: False)
|
||||||
qPix = iconCache.loadDecoration("welcome")
|
qPix = iconCache.getDecoration("welcome")
|
||||||
assert qPix.isNull() is True
|
assert qPix.isNull() is True
|
||||||
|
|
||||||
# Test image sizes
|
# Test image sizes
|
||||||
qPix = iconCache.loadDecoration("welcome", w=100, h=None)
|
qPix = iconCache.getDecoration("welcome", w=100, h=None)
|
||||||
assert qPix.isNull() is False
|
assert qPix.isNull() is False
|
||||||
assert qPix.width() == 100
|
assert qPix.width() == 100
|
||||||
assert qPix.height() > 50
|
assert qPix.height() > 50
|
||||||
|
|
||||||
qPix = iconCache.loadDecoration("welcome", w=None, h=100)
|
qPix = iconCache.getDecoration("welcome", w=None, h=100)
|
||||||
assert qPix.isNull() is False
|
assert qPix.isNull() is False
|
||||||
assert qPix.width() > 100
|
assert qPix.width() > 100
|
||||||
assert qPix.height() == 100
|
assert qPix.height() == 100
|
||||||
|
|
||||||
qPix = iconCache.loadDecoration("welcome", w=100, h=100)
|
qPix = iconCache.getDecoration("welcome", w=100, h=100)
|
||||||
assert qPix.isNull() is False
|
assert qPix.isNull() is False
|
||||||
assert qPix.width() == 100
|
assert qPix.width() == 100
|
||||||
assert qPix.height() == 100
|
assert qPix.height() == 100
|
||||||
|
|||||||
Reference in New Issue
Block a user