diff --git a/novelwriter/assets/themes/default.conf b/novelwriter/assets/themes/default.conf index aa3691e1..9cd9cd5b 100644 --- a/novelwriter/assets/themes/default.conf +++ b/novelwriter/assets/themes/default.conf @@ -1,4 +1,3 @@ [Main] -name = Default Theme +name = Qt Default Theme description = Qt standard colours -icontheme = typicons_light \ No newline at end of file diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 05a9b4ce..ed8e0485 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -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 diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf new file mode 100644 index 00000000..6c28f979 --- /dev/null +++ b/novelwriter/assets/themes/default_light.conf @@ -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 diff --git a/novelwriter/assets/themes/solarized_dark.conf b/novelwriter/assets/themes/solarized_dark.conf index cf46d519..a132f9a6 100644 --- a/novelwriter/assets/themes/solarized_dark.conf +++ b/novelwriter/assets/themes/solarized_dark.conf @@ -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 diff --git a/novelwriter/assets/themes/solarized_light.conf b/novelwriter/assets/themes/solarized_light.conf index c9fb50c0..614622b6 100644 --- a/novelwriter/assets/themes/solarized_light.conf +++ b/novelwriter/assets/themes/solarized_light.conf @@ -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 diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 39383c9a..7afd727f 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -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(): diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 52bbfb78..f951fc79 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -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 . """ +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 diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 3ba5f405..17797aab 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -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)