Add dark/light toggle button to sidebar
This commit is contained in:
@@ -28,7 +28,8 @@ from typing import Final
|
|||||||
from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
|
from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
|
||||||
|
|
||||||
from novelwriter.enum import (
|
from novelwriter.enum import (
|
||||||
nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape
|
nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape,
|
||||||
|
nwTheme
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -453,6 +454,16 @@ class nwLabels:
|
|||||||
"blue": QT_TRANSLATE_NOOP("Constant", "Blue"),
|
"blue": QT_TRANSLATE_NOOP("Constant", "Blue"),
|
||||||
"purple": QT_TRANSLATE_NOOP("Constant", "Purple"),
|
"purple": QT_TRANSLATE_NOOP("Constant", "Purple"),
|
||||||
}
|
}
|
||||||
|
THEME_MODE_ICON: Final[dict[nwTheme, str]] = {
|
||||||
|
nwTheme.AUTO: "theme_auto",
|
||||||
|
nwTheme.LIGHT: "theme_light",
|
||||||
|
nwTheme.DARK: "theme_dark",
|
||||||
|
}
|
||||||
|
THEME_MODE_LABEL: Final[dict[nwTheme, str]] = {
|
||||||
|
nwTheme.AUTO: QT_TRANSLATE_NOOP("Constant", "System Theme"),
|
||||||
|
nwTheme.LIGHT: QT_TRANSLATE_NOOP("Constant", "Light Theme"),
|
||||||
|
nwTheme.DARK: QT_TRANSLATE_NOOP("Constant", "Dark Theme"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class nwHeadFmt:
|
class nwHeadFmt:
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ import logging
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal
|
from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal, pyqtSlot
|
||||||
from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget
|
from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from novelwriter import SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.common import qtLambda
|
from novelwriter.common import qtLambda
|
||||||
from novelwriter.enum import nwView
|
from novelwriter.constants import nwLabels, trConst
|
||||||
|
from novelwriter.enum import nwTheme, nwView
|
||||||
from novelwriter.extensions.eventfilters import StatusTipFilter
|
from novelwriter.extensions.eventfilters import StatusTipFilter
|
||||||
from novelwriter.extensions.modified import NIconToolButton
|
from novelwriter.extensions.modified import NIconToolButton
|
||||||
from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON
|
from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON
|
||||||
@@ -77,6 +78,10 @@ class GuiSideBar(QWidget):
|
|||||||
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
|
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
|
||||||
self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE))
|
self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE))
|
||||||
|
|
||||||
|
self.tbTheme = NIconToolButton(self, iSz)
|
||||||
|
self.tbTheme.setToolTip(self.tr("Switch Colour Theme"))
|
||||||
|
self.tbTheme.clicked.connect(self._cycleColurTheme)
|
||||||
|
|
||||||
self.tbBuild = NIconToolButton(self, iSz)
|
self.tbBuild = NIconToolButton(self, iSz)
|
||||||
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
|
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
|
||||||
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
|
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
|
||||||
@@ -109,6 +114,7 @@ class GuiSideBar(QWidget):
|
|||||||
self.outerBox.addWidget(self.tbOutline)
|
self.outerBox.addWidget(self.tbOutline)
|
||||||
self.outerBox.addWidget(self.tbBuild)
|
self.outerBox.addWidget(self.tbBuild)
|
||||||
self.outerBox.addStretch(1)
|
self.outerBox.addStretch(1)
|
||||||
|
self.outerBox.addWidget(self.tbTheme)
|
||||||
self.outerBox.addWidget(self.tbDetails)
|
self.outerBox.addWidget(self.tbDetails)
|
||||||
self.outerBox.addWidget(self.tbStats)
|
self.outerBox.addWidget(self.tbStats)
|
||||||
self.outerBox.addWidget(self.tbSettings)
|
self.outerBox.addWidget(self.tbSettings)
|
||||||
@@ -131,6 +137,7 @@ class GuiSideBar(QWidget):
|
|||||||
self.tbSearch.setStyleSheet(buttonStyle)
|
self.tbSearch.setStyleSheet(buttonStyle)
|
||||||
self.tbOutline.setStyleSheet(buttonStyle)
|
self.tbOutline.setStyleSheet(buttonStyle)
|
||||||
self.tbBuild.setStyleSheet(buttonStyle)
|
self.tbBuild.setStyleSheet(buttonStyle)
|
||||||
|
self.tbTheme.setStyleSheet(buttonStyle)
|
||||||
self.tbDetails.setStyleSheet(buttonStyle)
|
self.tbDetails.setStyleSheet(buttonStyle)
|
||||||
self.tbStats.setStyleSheet(buttonStyle)
|
self.tbStats.setStyleSheet(buttonStyle)
|
||||||
self.tbSettings.setStyleSheet(buttonStyle)
|
self.tbSettings.setStyleSheet(buttonStyle)
|
||||||
@@ -144,6 +151,36 @@ class GuiSideBar(QWidget):
|
|||||||
self.tbStats.setThemeIcon("sb_stats")
|
self.tbStats.setThemeIcon("sb_stats")
|
||||||
self.tbSettings.setThemeIcon("settings")
|
self.tbSettings.setThemeIcon("settings")
|
||||||
|
|
||||||
|
self._setThemeModeIcon()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Private Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _cycleColurTheme(self) -> None:
|
||||||
|
"""Go to nex colour theme."""
|
||||||
|
match CONFIG.themeMode:
|
||||||
|
case nwTheme.AUTO:
|
||||||
|
CONFIG.themeMode = nwTheme.LIGHT
|
||||||
|
case nwTheme.LIGHT:
|
||||||
|
CONFIG.themeMode = nwTheme.DARK
|
||||||
|
case nwTheme.DARK:
|
||||||
|
CONFIG.themeMode = nwTheme.AUTO
|
||||||
|
self.mainGui.checkThemeUpdate()
|
||||||
|
self._setThemeModeIcon()
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Internal Functions
|
||||||
|
##
|
||||||
|
|
||||||
|
def _setThemeModeIcon(self) -> None:
|
||||||
|
"""Set the theme button icon."""
|
||||||
|
self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode])
|
||||||
|
self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode]))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+17
-16
@@ -100,22 +100,22 @@ class GuiTheme:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", "_qColors",
|
"_availSyntax", "_availThemes", "_currentTheme", "_darkThemes", "_guiPalette",
|
||||||
"_styleSheets", "_svgColors", "_syntaxList", "_themeList", "baseButtonHeight",
|
"_lightThemes", "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "_themeList",
|
||||||
"baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText",
|
"baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText",
|
||||||
"fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration",
|
"fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration",
|
||||||
"getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon",
|
"getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon",
|
||||||
"guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText",
|
"guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText",
|
||||||
"iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth",
|
"iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth",
|
||||||
"themeMeta",
|
"themeMeta",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
||||||
self.iconCache = GuiIcons(self)
|
# Theme Objects
|
||||||
|
|
||||||
# GUI Theme
|
|
||||||
self.themeMeta = ThemeMeta()
|
self.themeMeta = ThemeMeta()
|
||||||
|
self.iconCache = GuiIcons(self)
|
||||||
|
self.syntaxTheme = SyntaxColors()
|
||||||
self.isDarkTheme = False
|
self.isDarkTheme = False
|
||||||
|
|
||||||
# Special Text Colours
|
# Special Text Colours
|
||||||
@@ -123,11 +123,8 @@ class GuiTheme:
|
|||||||
self.fadedText = QColor(0, 0, 0)
|
self.fadedText = QColor(0, 0, 0)
|
||||||
self.errorText = QColor(255, 0, 0)
|
self.errorText = QColor(255, 0, 0)
|
||||||
|
|
||||||
# Syntax Theme
|
|
||||||
self.syntaxMeta = ThemeMeta()
|
|
||||||
self.syntaxTheme = SyntaxColors()
|
|
||||||
|
|
||||||
# Load Themes
|
# Load Themes
|
||||||
|
self._currentTheme = ""
|
||||||
self._guiPalette = QPalette()
|
self._guiPalette = QPalette()
|
||||||
self._themeList: list[T_ThemeEntry] = []
|
self._themeList: list[T_ThemeEntry] = []
|
||||||
self._availThemes: dict[str, Path] = {}
|
self._availThemes: dict[str, Path] = {}
|
||||||
@@ -275,6 +272,10 @@ class GuiTheme:
|
|||||||
theme = DEF_GUI_LIGHT
|
theme = DEF_GUI_LIGHT
|
||||||
CONFIG.lightTheme = DEF_GUI_LIGHT
|
CONFIG.lightTheme = DEF_GUI_LIGHT
|
||||||
|
|
||||||
|
if theme == self._currentTheme:
|
||||||
|
logger.info("Theme '%s' is already loaded", theme)
|
||||||
|
return False
|
||||||
|
|
||||||
if not (file := self._availThemes.get(theme)):
|
if not (file := self._availThemes.get(theme)):
|
||||||
logger.error("Could not load GUI theme")
|
logger.error("Could not load GUI theme")
|
||||||
return False
|
return False
|
||||||
@@ -387,7 +388,6 @@ class GuiTheme:
|
|||||||
text = self._guiPalette.text().color()
|
text = self._guiPalette.text().color()
|
||||||
window = self._guiPalette.window().color()
|
window = self._guiPalette.window().color()
|
||||||
highlight = self._guiPalette.highlight().color()
|
highlight = self._guiPalette.highlight().color()
|
||||||
isDark = text.lightnessF() > window.lightnessF()
|
|
||||||
|
|
||||||
QtColActive = QPalette.ColorGroup.Active
|
QtColActive = QPalette.ColorGroup.Active
|
||||||
QtColInactive = QPalette.ColorGroup.Inactive
|
QtColInactive = QPalette.ColorGroup.Inactive
|
||||||
@@ -407,8 +407,8 @@ class GuiTheme:
|
|||||||
darkOff = dark.darker(150)
|
darkOff = dark.darker(150)
|
||||||
shadowOff = ref.darker(150)
|
shadowOff = ref.darker(150)
|
||||||
|
|
||||||
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
|
grey = QColor(120, 120, 120) if darkMode else QColor(140, 140, 140)
|
||||||
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
dimmed = QColor(130, 130, 130) if darkMode else QColor(190, 190, 190)
|
||||||
|
|
||||||
placeholder = QColor(text)
|
placeholder = QColor(text)
|
||||||
placeholder.setAlpha(128)
|
placeholder.setAlpha(128)
|
||||||
@@ -453,10 +453,11 @@ class GuiTheme:
|
|||||||
self.iconCache.loadTheme(CONFIG.iconTheme)
|
self.iconCache.loadTheme(CONFIG.iconTheme)
|
||||||
|
|
||||||
# Finalise
|
# Finalise
|
||||||
self.isDarkTheme = isDark
|
self.isDarkTheme = darkMode
|
||||||
QApplication.setPalette(self._guiPalette)
|
QApplication.setPalette(self._guiPalette)
|
||||||
self._buildStyleSheets(self._guiPalette)
|
self._buildStyleSheets(self._guiPalette)
|
||||||
|
|
||||||
|
self._currentTheme = theme
|
||||||
CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}")
|
CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
+38
-17
@@ -30,7 +30,7 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt, QTimer, pyqtSlot
|
from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSlot
|
||||||
from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut
|
from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox,
|
QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox,
|
||||||
@@ -902,10 +902,46 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
return not self.splitView.isVisible()
|
return not self.splitView.isVisible()
|
||||||
|
|
||||||
|
def checkThemeUpdate(self) -> None:
|
||||||
|
"""Load theme if mode changed."""
|
||||||
|
if SHARED.theme.loadTheme():
|
||||||
|
self.refreshThemeColors(syntax=True)
|
||||||
|
self.docEditor.initEditor()
|
||||||
|
self.docViewer.initViewer()
|
||||||
|
return
|
||||||
|
|
||||||
|
def refreshThemeColors(self, syntax: bool) -> None:
|
||||||
|
"""Refresh the GUI theme."""
|
||||||
|
SHARED.theme.loadTheme()
|
||||||
|
self.setPalette(QApplication.palette())
|
||||||
|
self.docEditor.updateTheme()
|
||||||
|
self.docViewer.updateTheme()
|
||||||
|
self.docViewerPanel.updateTheme()
|
||||||
|
self.sideBar.updateTheme()
|
||||||
|
self.projView.updateTheme()
|
||||||
|
self.novelView.updateTheme()
|
||||||
|
self.projSearch.updateTheme()
|
||||||
|
self.outlineView.updateTheme()
|
||||||
|
self.itemDetails.updateTheme()
|
||||||
|
self.mainStatus.updateTheme()
|
||||||
|
SHARED.project.tree.refreshAllItems()
|
||||||
|
|
||||||
|
if syntax:
|
||||||
|
self.docEditor.updateSyntaxColors()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Events
|
# Events
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def changeEvent(self, event: QEvent) -> None:
|
||||||
|
"""Capture application change events."""
|
||||||
|
if int(event.type()) == 210:
|
||||||
|
# ThemeChange
|
||||||
|
self.checkThemeUpdate()
|
||||||
|
return
|
||||||
|
|
||||||
def closeEvent(self, event: QCloseEvent) -> None:
|
def closeEvent(self, event: QCloseEvent) -> None:
|
||||||
"""Capture the closing event of the GUI and call the close
|
"""Capture the closing event of the GUI and call the close
|
||||||
function to handle all the close process steps.
|
function to handle all the close process steps.
|
||||||
@@ -1054,22 +1090,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.novelView.refreshCurrentTree()
|
self.novelView.refreshCurrentTree()
|
||||||
|
|
||||||
if theme:
|
if theme:
|
||||||
SHARED.theme.loadTheme()
|
self.refreshThemeColors(syntax=syntax)
|
||||||
self.setPalette(QApplication.palette())
|
|
||||||
self.docEditor.updateTheme()
|
|
||||||
self.docViewer.updateTheme()
|
|
||||||
self.docViewerPanel.updateTheme()
|
|
||||||
self.sideBar.updateTheme()
|
|
||||||
self.projView.updateTheme()
|
|
||||||
self.novelView.updateTheme()
|
|
||||||
self.projSearch.updateTheme()
|
|
||||||
self.outlineView.updateTheme()
|
|
||||||
self.itemDetails.updateTheme()
|
|
||||||
self.mainStatus.updateTheme()
|
|
||||||
SHARED.project.tree.refreshAllItems()
|
|
||||||
|
|
||||||
if syntax:
|
|
||||||
self.docEditor.updateSyntaxColors()
|
|
||||||
|
|
||||||
self.docEditor.initEditor()
|
self.docEditor.initEditor()
|
||||||
self.docViewer.initViewer()
|
self.docViewer.initViewer()
|
||||||
|
|||||||
Reference in New Issue
Block a user