Theme updates (#1475)

This commit is contained in:
Veronica Berglyd Olsen
2023-07-19 17:43:57 +02:00
committed by GitHub
8 changed files with 139 additions and 93 deletions
+1 -2
View File
@@ -1,4 +1,3 @@
[Main] [Main]
name = Default Theme name = Qt Default Theme
description = Qt standard colours description = Qt standard colours
icontheme = typicons_light
+5 -4
View File
@@ -10,21 +10,22 @@ icontheme = typicons_dark
[Palette] [Palette]
window = 54, 54, 54 window = 54, 54, 54
windowtext = 174, 174, 174 windowtext = 204, 204, 204
base = 62, 62, 62 base = 62, 62, 62
alternatebase = 78, 78, 78 alternatebase = 78, 78, 78
text = 174, 174, 174 text = 204, 204, 204
tooltipbase = 255, 255, 192 tooltipbase = 255, 255, 192
tooltiptext = 21, 21, 13 tooltiptext = 21, 21, 13
button = 62, 62, 62 button = 62, 62, 62
buttontext = 174, 174, 174 buttontext = 204, 204, 204
brighttext = 174, 174, 174 brighttext = 62, 62, 62
highlight = 44, 152, 247 highlight = 44, 152, 247
highlightedtext = 255, 255, 255 highlightedtext = 255, 255, 255
link = 44, 152, 247 link = 44, 152, 247
linkvisited = 44, 152, 247 linkvisited = 44, 152, 247
[GUI] [GUI]
helptext = 164, 164, 164
statusnone = 150, 152, 150 statusnone = 150, 152, 150
statussaved = 39, 135, 78 statussaved = 39, 135, 78
statusunsaved = 138, 32, 32 statusunsaved = 138, 32, 32
@@ -0,0 +1,31 @@
[Main]
name = Default Light Theme
description = The novelWriter standard light theme
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
icontheme = typicons_light
[Palette]
window = 239, 239, 239
windowtext = 0, 0, 0
base = 255, 255, 255
alternatebase = 239, 239, 239
text = 0, 0, 0
tooltipbase = 255, 255, 220
tooltiptext = 0, 0, 0
button = 239, 239, 239
buttontext = 0, 0, 0
brighttext = 255, 255, 255
highlight = 48, 135, 198
highlightedtext = 255, 255, 255
link = 0, 84, 255
linkvisited = 0, 84, 255
[GUI]
helptext = 92, 92, 92
statusnone = 120, 120, 120
statussaved = 200, 15, 39
statusunsaved = 2, 133, 37
@@ -17,13 +17,14 @@ tooltipbase = 133, 153, 0
tooltiptext = 0, 43, 54 tooltiptext = 0, 43, 54
button = 7, 54, 66 button = 7, 54, 66
buttontext = 253, 246, 227 buttontext = 253, 246, 227
brighttext = 253, 246, 227 brighttext = 7, 54, 66
highlight = 42, 161, 152 highlight = 42, 161, 152
highlightedtext = 0, 43, 54 highlightedtext = 0, 43, 54
link = 38, 139, 210 link = 38, 139, 210
linkvisited = 38, 139, 210 linkvisited = 38, 139, 210
[GUI] [GUI]
helptext = 166, 161, 149
statusnone = 88, 110, 117 statusnone = 88, 110, 117
statussaved = 42, 161, 152 statussaved = 42, 161, 152
statusunsaved = 203, 75, 22 statusunsaved = 203, 75, 22
@@ -17,13 +17,14 @@ tooltipbase = 133, 153, 0
tooltiptext = 0, 43, 54 tooltiptext = 0, 43, 54
button = 238, 232, 213 button = 238, 232, 213
buttontext = 0, 43, 54 buttontext = 0, 43, 54
brighttext = 0, 43, 54 brighttext = 253, 246, 227
highlight = 42, 161, 152 highlight = 42, 161, 152
highlightedtext = 253, 246, 227 highlightedtext = 253, 246, 227
link = 38, 139, 210 link = 38, 139, 210
linkvisited = 38, 139, 210 linkvisited = 38, 139, 210
[GUI] [GUI]
helptext = 78, 91, 95
statusnone = 88, 110, 117 statusnone = 88, 110, 117
statussaved = 42, 161, 152 statussaved = 42, 161, 152
statusunsaved = 203, 75, 22 statusunsaved = 203, 75, 22
+3 -3
View File
@@ -135,7 +135,7 @@ class NPagedToolButton(QToolButton):
def paintEvent(self, event): def paintEvent(self, event):
"""Overload the paint event to draw a simple, left aligned text """Overload the paint event to draw a simple, left aligned text
label, with a highlight when selected and alternative base label, with a highlight when selected and a transparent base
colour when hovered. colour when hovered.
""" """
opt = QStyleOptionToolButton() opt = QStyleOptionToolButton()
@@ -150,9 +150,9 @@ class NPagedToolButton(QToolButton):
palette = self.palette() palette = self.palette()
if opt.state & QStyle.State_MouseOver == QStyle.State_MouseOver: if opt.state & QStyle.State_MouseOver == QStyle.State_MouseOver:
backCol = palette.alternateBase() backCol = palette.base()
paint.setBrush(backCol) paint.setBrush(backCol)
paint.setOpacity(0.5) paint.setOpacity(0.75)
paint.drawRoundedRect(0, 0, width, height, self._cR, self._cR) paint.drawRoundedRect(0, 0, width, height, self._cR, self._cR)
if self.isChecked(): if self.isChecked():
+79 -78
View File
@@ -23,10 +23,12 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import logging import logging
from math import ceil from math import ceil
from pathlib import Path
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import qApp from PyQt5.QtWidgets import qApp
@@ -111,10 +113,10 @@ class GuiTheme:
# Load Themes # Load Themes
self._guiPalette = QPalette() self._guiPalette = QPalette()
self._themeList = [] self._themeList: list[tuple[str, str]] = []
self._syntaxList = [] self._syntaxList: list[tuple[str, str]] = []
self._availThemes = {} self._availThemes: dict[str, Path] = {}
self._availSyntax = {} self._availSyntax: dict[str, Path] = {}
self._listConf(self._availSyntax, CONFIG.assetPath("syntax")) self._listConf(self._availSyntax, CONFIG.assetPath("syntax"))
self._listConf(self._availThemes, CONFIG.assetPath("themes")) self._listConf(self._availThemes, CONFIG.assetPath("themes"))
@@ -166,22 +168,22 @@ class GuiTheme:
# Methods # Methods
## ##
def getTextWidth(self, theText, theFont=None): def getTextWidth(self, text: str, font: QFont | None = None) -> int:
"""Returns the width needed to contain a given piece of text. """Returns the width needed to contain a given piece of text in
pixels.
""" """
if isinstance(theFont, QFont): if isinstance(font, QFont):
qMetrics = QFontMetrics(theFont) qMetrics = QFontMetrics(font)
else: else:
qMetrics = QFontMetrics(self.guiFont) qMetrics = QFontMetrics(self.guiFont)
return int(ceil(qMetrics.boundingRect(theText).width())) return int(ceil(qMetrics.boundingRect(text).width()))
## ##
# Theme Methods # Theme Methods
## ##
def loadTheme(self): def loadTheme(self):
"""Load the currently specified GUI theme. """Load the currently specified GUI theme."""
"""
guiTheme = CONFIG.guiTheme guiTheme = CONFIG.guiTheme
if guiTheme not in self._availThemes: if guiTheme not in self._availThemes:
logger.error("Could not find GUI theme '%s'", guiTheme) logger.error("Could not find GUI theme '%s'", guiTheme)
@@ -195,70 +197,72 @@ class GuiTheme:
# Config File # Config File
logger.info("Loading GUI theme '%s'", guiTheme) logger.info("Loading GUI theme '%s'", guiTheme)
confParser = NWConfigParser() parser = NWConfigParser()
try: try:
with open(themeFile, mode="r", encoding="utf-8") as inFile: with open(themeFile, mode="r", encoding="utf-8") as inFile:
confParser.read_file(inFile) parser.read_file(inFile)
except Exception: except Exception:
logger.error("Could not load theme settings from: %s", themeFile) logger.error("Could not load theme settings from: %s", themeFile)
logException() logException()
return False return False
# Main # Main
cnfSec = "Main" sec = "Main"
if confParser.has_section(cnfSec): if parser.has_section(sec):
self.themeName = confParser.rdStr(cnfSec, "name", "") self.themeName = parser.rdStr(sec, "name", "")
self.themeDescription = confParser.rdStr(cnfSec, "description", "N/A") self.themeDescription = parser.rdStr(sec, "description", "N/A")
self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A") self.themeAuthor = parser.rdStr(sec, "author", "N/A")
self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A") self.themeCredit = parser.rdStr(sec, "credit", "N/A")
self.themeUrl = confParser.rdStr(cnfSec, "url", "") self.themeUrl = parser.rdStr(sec, "url", "")
self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A") self.themeLicense = parser.rdStr(sec, "license", "N/A")
self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") self.themeLicenseUrl = parser.rdStr(sec, "licenseurl", "")
self.themeIcons = confParser.rdStr(cnfSec, "icontheme", "") self.themeIcons = parser.rdStr(sec, "icontheme", "")
# Palette # Palette
cnfSec = "Palette" sec = "Palette"
if confParser.has_section(cnfSec): if parser.has_section(sec):
self._setPalette(confParser, cnfSec, "window", QPalette.Window) self._setPalette(parser, sec, "window", QPalette.ColorRole.Window)
self._setPalette(confParser, cnfSec, "windowtext", QPalette.WindowText) self._setPalette(parser, sec, "windowtext", QPalette.ColorRole.WindowText)
self._setPalette(confParser, cnfSec, "base", QPalette.Base) self._setPalette(parser, sec, "base", QPalette.ColorRole.Base)
self._setPalette(confParser, cnfSec, "alternatebase", QPalette.AlternateBase) self._setPalette(parser, sec, "alternatebase", QPalette.ColorRole.AlternateBase)
self._setPalette(confParser, cnfSec, "text", QPalette.Text) self._setPalette(parser, sec, "text", QPalette.ColorRole.Text)
self._setPalette(confParser, cnfSec, "tooltipbase", QPalette.ToolTipBase) self._setPalette(parser, sec, "tooltipbase", QPalette.ColorRole.ToolTipBase)
self._setPalette(confParser, cnfSec, "tooltiptext", QPalette.ToolTipText) self._setPalette(parser, sec, "tooltiptext", QPalette.ColorRole.ToolTipText)
self._setPalette(confParser, cnfSec, "button", QPalette.Button) self._setPalette(parser, sec, "button", QPalette.ColorRole.Button)
self._setPalette(confParser, cnfSec, "buttontext", QPalette.ButtonText) self._setPalette(parser, sec, "buttontext", QPalette.ColorRole.ButtonText)
self._setPalette(confParser, cnfSec, "brighttext", QPalette.BrightText) self._setPalette(parser, sec, "brighttext", QPalette.ColorRole.BrightText)
self._setPalette(confParser, cnfSec, "highlight", QPalette.Highlight) self._setPalette(parser, sec, "highlight", QPalette.ColorRole.Highlight)
self._setPalette(confParser, cnfSec, "highlightedtext", QPalette.HighlightedText) self._setPalette(parser, sec, "highlightedtext", QPalette.ColorRole.HighlightedText)
self._setPalette(confParser, cnfSec, "link", QPalette.Link) self._setPalette(parser, sec, "link", QPalette.ColorRole.Link)
self._setPalette(confParser, cnfSec, "linkvisited", QPalette.LinkVisited) self._setPalette(parser, sec, "linkvisited", QPalette.ColorRole.LinkVisited)
else: else:
self._guiPalette = qApp.style().standardPalette() self._guiPalette = qApp.style().standardPalette()
# GUI # GUI
cnfSec = "GUI" sec = "GUI"
if confParser.has_section(cnfSec): if parser.has_section(sec):
self.statNone = self._parseColour(confParser, cnfSec, "statusnone") self.helpText = self._parseColour(parser, sec, "helptext")
self.statUnsaved = self._parseColour(confParser, cnfSec, "statusunsaved") self.statNone = self._parseColour(parser, sec, "statusnone")
self.statSaved = self._parseColour(confParser, cnfSec, "statussaved") self.statUnsaved = self._parseColour(parser, sec, "statusunsaved")
self.statSaved = self._parseColour(parser, sec, "statussaved")
# Icons
self.iconCache.loadTheme(self.themeIcons or "typicons_light")
# Update Dependant Colours # Update Dependant Colours
backCol = self._guiPalette.window().color() backCol = self._guiPalette.window().color()
textCol = self._guiPalette.windowText().color() textCol = self._guiPalette.windowText().color()
backLCol = backCol.lightnessF() backLNess = backCol.lightnessF()
textLCol = textCol.lightnessF() textLNess = textCol.lightnessF()
if backLCol > textLCol: if self.helpText == [0, 0, 0]:
helpLCol = textLCol + 0.65*(backLCol - textLCol) if backLNess > textLNess:
else: helpLCol = textLNess + 0.35*(backLNess - textLNess)
helpLCol = backLCol + 0.65*(textLCol - backLCol) else:
helpLCol = backLNess + 0.65*(textLNess - backLNess)
self.helpText = [int(255*helpLCol)]*3
self.helpText = [int(255*helpLCol)]*3 # Icons
defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark"
self.iconCache.loadTheme(self.themeIcons or defaultIcons)
# Apply Styles # Apply Styles
qApp.setPalette(self._guiPalette) qApp.setPalette(self._guiPalette)
@@ -266,8 +270,7 @@ class GuiTheme:
return True return True
def loadSyntax(self): def loadSyntax(self):
"""Load the currently specified syntax highlighter theme. """Load the currently specified syntax highlighter theme."""
"""
guiSyntax = CONFIG.guiSyntax guiSyntax = CONFIG.guiSyntax
if guiSyntax not in self._availSyntax: if guiSyntax not in self._availSyntax:
logger.error("Could not find syntax theme '%s'", guiSyntax) logger.error("Could not find syntax theme '%s'", guiSyntax)
@@ -323,9 +326,8 @@ class GuiTheme:
return True return True
def listThemes(self): def listThemes(self) -> list[tuple[str, str]]:
"""Scan the GUI themes folder and list all themes. """Scan the GUI themes folder and list all themes."""
"""
if self._themeList: if self._themeList:
return self._themeList return self._themeList
@@ -340,9 +342,8 @@ class GuiTheme:
return self._themeList return self._themeList
def listSyntax(self): def listSyntax(self) -> list[tuple[str, str]]:
"""Scan the syntax themes folder and list all themes. """Scan the syntax themes folder and list all themes."""
"""
if self._syntaxList: if self._syntaxList:
return self._syntaxList return self._syntaxList
@@ -362,8 +363,7 @@ class GuiTheme:
## ##
def _setGuiFont(self): def _setGuiFont(self):
"""Update the GUI's font style from settings. """Update the GUI's font style from settings."""
"""
theFont = QFont() theFont = QFont()
fontDB = QFontDatabase() fontDB = QFontDatabase()
if CONFIG.guiFont not in fontDB.families(): if CONFIG.guiFont not in fontDB.families():
@@ -383,9 +383,8 @@ class GuiTheme:
return return
def _listConf(self, targetDict, checkDir): def _listConf(self, targetDict: dict, checkDir: Path) -> bool:
"""Scan for theme config files and populate the dictionary. """Scan for theme config files and populate the dictionary."""
"""
if not checkDir.is_dir(): if not checkDir.is_dir():
return False return False
@@ -395,29 +394,31 @@ class GuiTheme:
return True return True
def _parseColour(self, confParser, cnfSec, cnfName): def _parseColour(
"""Parse a colour value from a config string. self, parser: NWConfigParser, section: str, name: str
""" ) -> list[int]:
if confParser.has_option(cnfSec, cnfName): """Parse a colour value from a config string."""
values = confParser.get(cnfSec, cnfName).split(",") if parser.has_option(section, name):
values = parser.get(section, name).split(",")
result = [] result = []
try: try:
result.append(minmax(int(values[0]), 0, 255)) result.append(minmax(int(values[0]), 0, 255))
result.append(minmax(int(values[1]), 0, 255)) result.append(minmax(int(values[1]), 0, 255))
result.append(minmax(int(values[2]), 0, 255)) result.append(minmax(int(values[2]), 0, 255))
except Exception: except Exception:
logger.error("Could not load theme colours for '%s' from config file", cnfName) logger.error("Could not load theme colours for '%s' from config file", name)
result = [0, 0, 0] result = [0, 0, 0]
else: else:
logger.warning("Could not find theme colours for '%s' in config file", cnfName) logger.warning("Could not find theme colours for '%s' in config file", name)
result = [0, 0, 0] result = [0, 0, 0]
return result return result
def _setPalette(self, confParser, cnfSec, cnfName, paletteVal): def _setPalette(
"""Set a palette colour value from a config string. self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole
""" ):
"""Set a palette colour value from a config string."""
self._guiPalette.setColor( self._guiPalette.setColor(
paletteVal, QColor(*self._parseColour(confParser, cnfSec, cnfName)) value, QColor(*self._parseColour(parser, section, name))
) )
return return
+16 -4
View File
@@ -122,8 +122,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI): def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
"""Test the theme part of the class. """Test the theme part of the class."""
"""
mainTheme: GuiTheme = nwGUI.mainTheme mainTheme: GuiTheme = nwGUI.mainTheme
# List Themes # List Themes
@@ -137,7 +136,8 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
# Load the theme info # Load the theme info
themesList = mainTheme.listThemes() themesList = mainTheme.listThemes()
assert themesList[0] == ("default_dark", "Default Dark Theme") assert themesList[0] == ("default_dark", "Default Dark Theme")
assert themesList[1] == ("default", "Default Theme") assert themesList[1] == ("default_light", "Default Light Theme")
assert themesList[2] == ("default", "Qt Default Theme")
# A second call should returned the cached list # A second call should returned the cached list
assert mainTheme.listThemes() == mainTheme._themeList assert mainTheme.listThemes() == mainTheme._themeList
@@ -169,6 +169,18 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
wCol = QApplication.style().standardPalette().color(QPalette.Window).getRgb() wCol = QApplication.style().standardPalette().color(QPalette.Window).getRgb()
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == wCol assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == wCol
# Load Default Light Theme
# ========================
CONFIG.guiTheme = "default_light"
assert mainTheme.loadTheme() is True
# Check a few values
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (239, 239, 239, 255)
assert mainTheme._guiPalette.color(QPalette.WindowText).getRgb() == (0, 0, 0, 255)
assert mainTheme._guiPalette.color(QPalette.Base).getRgb() == (255, 255, 255, 255)
assert mainTheme._guiPalette.color(QPalette.AlternateBase).getRgb() == (239, 239, 239, 255)
# Load Default Dark Theme # Load Default Dark Theme
# ======================= # =======================
@@ -177,7 +189,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
# Check a few values # Check a few values
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (54, 54, 54, 255) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (54, 54, 54, 255)
assert mainTheme._guiPalette.color(QPalette.WindowText).getRgb() == (174, 174, 174, 255) assert mainTheme._guiPalette.color(QPalette.WindowText).getRgb() == (204, 204, 204, 255)
assert mainTheme._guiPalette.color(QPalette.Base).getRgb() == (62, 62, 62, 255) assert mainTheme._guiPalette.color(QPalette.Base).getRgb() == (62, 62, 62, 255)
assert mainTheme._guiPalette.color(QPalette.AlternateBase).getRgb() == (78, 78, 78, 255) assert mainTheme._guiPalette.color(QPalette.AlternateBase).getRgb() == (78, 78, 78, 255)