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