diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 2a10fd92..44bf3590 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -552,6 +552,8 @@ class NWProject: def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: NWProject") + self._data.itemStatus.refreshIcons() self._data.itemImport.refreshIcons() diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 71cfca38..d36ca9ba 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -158,7 +158,7 @@ class GuiPreferences(NDialog): self.mainForm.addGroupLabel(title, section) # Display Language - self.guiLocale = NComboBox(self) + self.guiLocale = NComboBox(self, scrollable=True) self.guiLocale.setMinimumWidth(200) for lang, name in CONFIG.listLanguages(CONFIG.LANG_NW): self.guiLocale.addItem(name, lang) @@ -170,9 +170,9 @@ class GuiPreferences(NDialog): ) # Colour Theme - self.lightTheme = NComboBox(self) + self.lightTheme = NComboBox(self, scrollable=True) self.lightTheme.setMinimumWidth(200) - self.darkTheme = NComboBox(self) + self.darkTheme = NComboBox(self, scrollable=True) self.darkTheme.setMinimumWidth(200) for key, theme in SHARED.theme.colourThemes.items(): if theme.dark: @@ -193,7 +193,7 @@ class GuiPreferences(NDialog): ) # Icon Theme - self.iconTheme = NComboBox(self) + self.iconTheme = NComboBox(self, scrollable=True) self.iconTheme.setMinimumWidth(200) for key, theme in SHARED.theme.iconCache.iconThemes.items(): self.iconTheme.addItem(theme.name, key) @@ -511,7 +511,7 @@ class GuiPreferences(NDialog): self.mainForm.addGroupLabel(title, section) # Spell Checking - self.spellLanguage = NComboBox(self) + self.spellLanguage = NComboBox(self, scrollable=True) self.spellLanguage.setMinimumWidth(200) if CONFIG.hasEnchant: diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index dcb7f8de..31af45a5 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -257,7 +257,7 @@ class _SettingsPage(NScrollableForm): # Project Language projLang = data.language or CONFIG.guiLocale - self.projLang = NComboBox(self) + self.projLang = NComboBox(self, scrollable=True) self.projLang.setMinimumWidth(200) for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): self.projLang.addItem(language, tag) @@ -269,7 +269,7 @@ class _SettingsPage(NScrollableForm): ) # Spell Check Language - self.spellLang = NComboBox(self) + self.spellLang = NComboBox(self, scrollable=True) self.spellLang.setMinimumWidth(200) self.spellLang.addItem(self.tr("Default"), "None") if CONFIG.hasEnchant: diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 14800a5c..2c11ad8e 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -27,7 +27,7 @@ along with this program. If not, see . """ # noqa from __future__ import annotations -from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap +from PyQt6.QtGui import QColor, QFont, QPalette from PyQt6.QtWidgets import ( QAbstractButton, QFrame, QHBoxLayout, QLabel, QLayout, QScrollArea, QVBoxLayout, QWidget @@ -170,7 +170,7 @@ class NScrollableForm(QScrollArea): def addRow( self, label: str | None, - widget: QWidget | list[QWidget | QPixmap | int], + widget: QWidget | list[QWidget | int], helpText: str = "", unit: str | None = None, button: QWidget | None = None, @@ -187,10 +187,6 @@ class NScrollableForm(QScrollArea): for item in widget: if isinstance(item, QWidget): wBox.addWidget(item) - elif isinstance(item, QPixmap): - icon = QLabel(self) - icon.setPixmap(item) - wBox.addWidget(icon) elif isinstance(item, int): wBox.addSpacing(item) qWidget = QWidget(self) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index ee1be5e7..054665ce 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -125,14 +125,16 @@ class NComboBox(QComboBox): window of many widgets. """ - def __init__(self, parent: QWidget | None = None, maxItems: int = 15) -> None: + def __init__( + self, parent: QWidget | None = None, maxItems: int = 15, scrollable: bool = False + ) -> None: super().__init__(parent=parent) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.setMaxVisibleItems(maxItems) - - # The style sheet disables Fusion style pop-up mode on some platforms - # and allows for scrolling of long lists of items - self.setStyleSheet("QComboBox {combobox-popup: 0;}") + if scrollable: + # The style sheet disables Fusion style pop-up mode on some + # platforms and allows for scrolling of long lists of items + self.setStyleSheet("QComboBox {combobox-popup: 0;}") def wheelEvent(self, event: QWheelEvent) -> None: """Only capture the mouse wheel if the widget has focus.""" @@ -214,10 +216,10 @@ class NPushButton(QPushButton): self._color = color self.setText(text) self.setIconSize(iconSize) - self.refreshIcon() + self.updateIcon() - def refreshIcon(self) -> None: - """Reload the theme icon.""" + def updateIcon(self) -> None: + """Update the theme icon.""" if self._icon: self.setIcon(SHARED.theme.getIcon(self._icon, self._color)) @@ -239,9 +241,9 @@ class NIconToolButton(QToolButton): if icon: self.setThemeIcon(icon, color) - def setThemeIcon(self, iconKey: str, color: str | None = None) -> None: + def setThemeIcon(self, icon: str, color: str | None = None) -> None: """Set an icon from the current theme.""" - self.setIcon(SHARED.theme.getIcon(iconKey, color)) + self.setIcon(SHARED.theme.getIcon(icon, color)) class NIconToggleButton(QToolButton): @@ -260,10 +262,10 @@ class NIconToggleButton(QToolButton): if icon: self.setThemeIcon(icon) - def setThemeIcon(self, iconKey: str) -> None: + def setThemeIcon(self, icon: str) -> None: """Set an icon from the current theme.""" - iconSize = self.iconSize() - self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height()))) + size = self.iconSize() + self.setIcon(SHARED.theme.getToggleIcon(icon, (size.width(), size.height()))) class NClickableLabel(QLabel): diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index fd332495..04c284ed 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -72,6 +72,9 @@ class NPagedSideBar(QToolBar): def setLabelColor(self, color: QColor) -> None: """Set the text color for the labels.""" self._labelCol = color + for widget in self.children(): + if isinstance(widget, _NPagedToolLabel): + widget.setTextColor(color) def addLabel(self, text: str) -> None: """Add a new label to the toolbar.""" @@ -188,6 +191,10 @@ class _NPagedToolLabel(QLabel): self._textCol = textColor or self.palette().text().color() + def setTextColor(self, textColor: QColor | None = None) -> None: + """Set a new text colour.""" + self._textCol = textColor or self.palette().text().color() + def paintEvent(self, event: QPaintEvent) -> None: """Overload the paint event to draw a simple, left aligned text label that matches the button style. diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index cc0d43a9..983d6294 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -34,7 +34,7 @@ from novelwriter.types import QtNoPen, QtPaintAntiAlias, QtSizeFixed class NSwitch(QAbstractButton): """Custom: Toggle Switch.""" - __slots__ = ("_cOff", "_cOn", "_offset", "_rH", "_rR", "_xH", "_xR", "_xW") + __slots__ = ("_offset", "_rH", "_rR", "_xH", "_xR", "_xW") def __init__(self, parent: QWidget, height: int = 0) -> None: super().__init__(parent=parent) @@ -45,13 +45,11 @@ class NSwitch(QAbstractButton): self._rH = self._xH - 4 self._rR = self._xR - 2 - self._cOn = SHARED.theme.accentCol - self._cOff = self.palette().alternateBase() - self.setCheckable(True) self.setSizePolicy(QtSizeFixed, QtSizeFixed) self.setFixedWidth(self._xW) self.setFixedHeight(self._xH) + self.setUpdatesEnabled(True) self._offset = self._xR self.clicked.connect(self._onClick) @@ -96,7 +94,7 @@ class NSwitch(QAbstractButton): painter.setOpacity(1.0 if self.isEnabled() else 0.5) painter.setPen(palette.highlight().color() if self.hasFocus() else palette.mid().color()) - painter.setBrush(self._cOn if self.isChecked() else self._cOff) + painter.setBrush(SHARED.theme.accentCol if self.isChecked() else palette.alternateBase()) painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR) painter.setPen(QtNoPen) @@ -110,6 +108,10 @@ class NSwitch(QAbstractButton): self.setCursor(Qt.CursorShape.PointingHandCursor) super().enterEvent(event) + ## + # Internal Functions + ## + @pyqtSlot(bool) def _onClick(self, checked: bool) -> None: """Animate the toggle action.""" diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 1496743f..d7a96765 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -292,6 +292,8 @@ class GuiDocEditor(QPlainTextEdit): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiDocEditor") + self.docSearch.updateTheme() self.docHeader.updateTheme() self.docFooter.updateTheme() @@ -2478,8 +2480,9 @@ class GuiDocToolBar(QWidget): def updateTheme(self) -> None: """Initialise GUI elements that depend on specific settings.""" - syntax = SHARED.theme.syntaxTheme + logger.debug("Theme Update: GuiDocToolBar") + syntax = SHARED.theme.syntaxTheme palette = self.palette() palette.setColor(QPalette.ColorRole.Window, syntax.back) palette.setColor(QPalette.ColorRole.WindowText, syntax.text) @@ -2590,7 +2593,7 @@ class GuiDocEditSearch(QFrame): # Buttons # ======= - self.showReplace = NIconToggleButton(self, iSz, "unfold") + self.showReplace = NIconToggleButton(self, iSz) self.showReplace.toggled.connect(self._doToggleReplace) self.searchButton = NIconToolButton(self, iSz) @@ -2716,8 +2719,9 @@ class GuiDocEditSearch(QFrame): def updateTheme(self) -> None: """Update theme elements.""" - palette = QApplication.palette() + logger.debug("Theme Update: GuiDocEditSearch") + palette = QApplication.palette() self.setPalette(palette) self.searchBox.setPalette(palette) self.replaceBox.setPalette(palette) @@ -2732,6 +2736,7 @@ class GuiDocEditSearch(QFrame): self.cancelSearch.setIcon(SHARED.theme.getIcon("search_cancel")) self.searchButton.setThemeIcon("search", "green") self.replaceButton.setThemeIcon("search_replace", "green") + self.showReplace.setThemeIcon("unfold") # Set stylesheets self.searchOpt.setStyleSheet("QToolBar {padding: 0;}") @@ -2960,6 +2965,8 @@ class GuiDocEditHeader(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiDocEditHeader") + self.tbButton.setThemeIcon("fmt_toolbar", "blue") self.outlineButton.setThemeIcon("list", "blue") self.searchButton.setThemeIcon("search", "blue") @@ -3149,6 +3156,8 @@ class GuiDocEditFooter(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiDocEditFooter") + iPx = round(0.9*SHARED.theme.baseIconHeight) self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx))) self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx))) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index fc7bef81..5cd44e27 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -141,6 +141,7 @@ class GuiDocViewer(QTextBrowser): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiDocViewer") self.docHeader.updateTheme() self.docFooter.updateTheme() @@ -724,6 +725,8 @@ class GuiDocViewHeader(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiDocViewHeader") + self.outlineButton.setThemeIcon("list", "blue") self.backButton.setThemeIcon("chevron_left", "blue") self.forwardButton.setThemeIcon("chevron_right", "blue") @@ -910,7 +913,8 @@ class GuiDocViewFooter(QWidget): def updateTheme(self) -> None: """Update theme elements.""" - # Icons + logger.debug("Theme Update: GuiDocViewFooter") + fPx = int(0.9*SHARED.theme.fontPixelSize) bulletIcon = SHARED.theme.getToggleIcon("bullet", (fPx, fPx), "blue") diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index 8ac9816a..9f3fd209 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -106,6 +106,8 @@ class GuiDocViewerPanel(QWidget): def updateTheme(self, updateTabs: bool = True) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiDocViewerPanel") + self.optsButton.setThemeIcon("more_vertical") self.optsButton.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)) self.mainTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS)) @@ -270,6 +272,8 @@ class _ViewPanelBackRefs(QTreeWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: _ViewPanelBackRefs") + self._editIcon = SHARED.theme.getIcon("edit", "green") self._viewIcon = SHARED.theme.getIcon("view", "blue") for i in range(self.topLevelItemCount()): @@ -400,6 +404,8 @@ class _ViewPanelKeyWords(QTreeWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: _ViewPanelKeyWords") + self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[self._class], "root") self._editIcon = SHARED.theme.getIcon("edit", "green") self._viewIcon = SHARED.theme.getIcon("view", "blue") diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index d087f742..697d8b8c 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -216,6 +216,7 @@ class GuiItemDetails(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiItemDetails") self.updateViewBox(self._handle) def updateViewBox(self, tHandle: str | None) -> None: diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index a5521ef8..feb5e703 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -89,6 +89,7 @@ class GuiNovelView(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiNovelView") self.novelBar.updateTheme() def initSettings(self) -> None: @@ -244,12 +245,12 @@ class GuiNovelToolBar(QWidget): def updateTheme(self) -> None: """Update theme elements.""" - # Icons + logger.debug("Theme Update: GuiNovelToolBar") + self.tbNovel.setThemeIcon("cls_novel", "red") self.tbRefresh.setThemeIcon("refresh", "green") self.tbMore.setThemeIcon("more_vertical") - # StyleSheets buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) self.tbNovel.setStyleSheet(buttonStyle) self.tbRefresh.setStyleSheet(buttonStyle) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 4a76712f..3a97ec35 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -103,6 +103,8 @@ class GuiOutlineView(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiOutlineView") + self.outlineBar.updateTheme() self.outlineTree.updateTheme() self.outlineTree.refreshTree( @@ -258,6 +260,8 @@ class GuiOutlineToolBar(QToolBar): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiOutlineToolBar") + self.setStyleSheet("QToolBar {border: 0px;}") self.novelValue.refreshNovelList() self.aRefresh.setIcon(SHARED.theme.getIcon("refresh", "green")) @@ -454,6 +458,8 @@ class GuiOutlineTree(QTreeWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiOutlineTree") + iType = nwItemType.FILE iClass = nwItemClass.NO_CLASS iLayout = nwItemLayout.DOCUMENT diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index ab79cfb6..758b15cd 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -138,6 +138,7 @@ class GuiProjectView(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiProjectView") self.projBar.updateTheme() def initSettings(self) -> None: @@ -346,6 +347,8 @@ class GuiProjectToolBar(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiProjectToolBar") + buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) self.tbQuick.setStyleSheet(buttonStyle) self.tbMoveU.setStyleSheet(buttonStyle) diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py index 5313d676..e49770b3 100644 --- a/novelwriter/gui/search.py +++ b/novelwriter/gui/search.py @@ -158,6 +158,8 @@ class GuiProjectSearch(QWidget): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiProjectSearch") + palette = QApplication.palette() colBase = palette.base().color().name(QtHexArgb) colFocus = palette.highlight().color().name(QtHexArgb) diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 6440fc15..8e6acfd3 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -129,8 +129,9 @@ class GuiSideBar(QWidget): def updateTheme(self) -> None: """Initialise GUI elements that depend on specific settings.""" - buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) + logger.debug("Theme Update: GuiSideBar") + buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) self.tbProject.setStyleSheet(buttonStyle) self.tbNovel.setStyleSheet(buttonStyle) self.tbSearch.setStyleSheet(buttonStyle) diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 0c8288b5..3578e3e0 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -133,6 +133,8 @@ class GuiMainStatus(QStatusBar): def updateTheme(self) -> None: """Update theme elements.""" + logger.debug("Theme Update: GuiMainStatus") + iPx = SHARED.theme.baseIconHeight self.langIcon.setPixmap(SHARED.theme.getPixmap("language", (iPx, iPx))) self.statsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx))) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index acf6114b..20e86ad8 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -910,6 +910,9 @@ class GuiMain(QMainWindow): self.mainStatus.updateTheme() SHARED.project.tree.refreshAllItems() + if dialog := SHARED.findTopLevelWidget(GuiManuscript): + dialog.updateTheme() + if syntax: self.docEditor.updateSyntaxColors() diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 79a93787..e3c5679b 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -101,30 +101,20 @@ class GuiManuscript(NToolDialog): # Build Controls # ============== - qPalette = self.palette() - qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base()) - self.setPalette(qPalette) - - buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) - - self.tbAdd = NIconToolButton(self, iSz, "add", "green") + self.tbAdd = NIconToolButton(self, iSz) self.tbAdd.setToolTip(self.tr("Add New Build")) - self.tbAdd.setStyleSheet(buttonStyle) self.tbAdd.clicked.connect(self._createNewBuild) - self.tbDel = NIconToolButton(self, iSz, "remove", "red") + self.tbDel = NIconToolButton(self, iSz) self.tbDel.setToolTip(self.tr("Delete Selected Build")) - self.tbDel.setStyleSheet(buttonStyle) self.tbDel.clicked.connect(self._deleteSelectedBuild) - self.tbCopy = NIconToolButton(self, iSz, "copy", "blue") + self.tbCopy = NIconToolButton(self, iSz) self.tbCopy.setToolTip(self.tr("Duplicate Selected Build")) - self.tbCopy.setStyleSheet(buttonStyle) self.tbCopy.clicked.connect(self._copySelectedBuild) - self.tbEdit = NIconToolButton(self, iSz, "edit", "green") + self.tbEdit = NIconToolButton(self, iSz) self.tbEdit.setToolTip(self.tr("Edit Selected Build")) - self.tbEdit.setStyleSheet(buttonStyle) self.tbEdit.clicked.connect(self._editSelectedBuild) self.lblBuilds = QLabel("{0}".format(self.tr("Builds")), self) @@ -159,7 +149,6 @@ class GuiManuscript(NToolDialog): self.detailsTabs = QTabWidget(self) self.detailsTabs.addTab(self.buildDetails, self.tr("Details")) self.detailsTabs.addTab(self.buildOutline, self.tr("Outline")) - self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS)) self.buildSplit = QSplitter(Qt.Orientation.Vertical, self) self.buildSplit.addWidget(self.buildList) @@ -247,6 +236,8 @@ class GuiManuscript(NToolDialog): self.setLayout(self.outerBox) self.setSizeGripEnabled(True) + self.updateTheme(init=True) + # Signals self.buildOutline.outlineEntryClicked.connect(self.docPreview.navigateTo) @@ -270,6 +261,37 @@ class GuiManuscript(NToolDialog): self.buildList.setCurrentItem(self._buildMap[selected]) QTimer.singleShot(200, self._generatePreview) + def updateTheme(self, *, init: bool = False) -> None: + """Update theme elements.""" + logger.debug("Theme Update: GuiManuscript, init=%s", init) + + if not init: + self.btnPreview.updateIcon() + self.btnPrint.updateIcon() + self.btnBuild.updateIcon() + self.btnClose.updateIcon() + + self.tbAdd.setThemeIcon("add", "green") + self.tbDel.setThemeIcon("remove", "red") + self.tbCopy.setThemeIcon("copy", "blue") + self.tbEdit.setThemeIcon("edit", "green") + + buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) + self.tbAdd.setStyleSheet(buttonStyle) + self.tbDel.setStyleSheet(buttonStyle) + self.tbCopy.setStyleSheet(buttonStyle) + self.tbEdit.setStyleSheet(buttonStyle) + + self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS)) + + self.buildDetails.updateTheme() + self.buildOutline.updateTheme() + self.docPreview.updateTheme() + + for obj in SHARED.mainGui.children(): + if isinstance(obj, GuiBuildSettings): + obj.updateTheme() + ## # Events ## @@ -490,6 +512,7 @@ class _DetailsWidget(QWidget): super().__init__(parent=parent) self._initExpanded = True + self._build = None # Tree Widget self.listView = QTreeWidget(self) @@ -612,9 +635,17 @@ class _DetailsWidget(QWidget): sub.setIcon(1, on if build.getBool(key) else off) item.addChild(sub) + self._build = build + # Restore expanded state self.setExpandedState(expanded) + def updateTheme(self) -> None: + """Update theme elements.""" + if self._build: + logger.debug("Theme Update: _DetailsWidget") + self.updateInfo(self._build) + class _OutlineWidget(QWidget): @@ -639,9 +670,9 @@ class _OutlineWidget(QWidget): self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) - def updateOutline(self, data: dict[str, str]) -> None: + def updateOutline(self, data: dict[str, str], *, force: bool = False) -> None: """Update the outline.""" - if isinstance(data, dict) and data != self._outline: + if isinstance(data, dict) and (data != self._outline or force): self.listView.clear() tFont = self.font() @@ -679,6 +710,11 @@ class _OutlineWidget(QWidget): self.listView.setIndentation(SHARED.theme.baseIconHeight if indent else 4) self._outline = data + def updateTheme(self) -> None: + """Update theme elements.""" + logger.debug("Theme Update: _OutlineWidget") + self.updateOutline(self._outline, force=True) + ## # Private Slots ## @@ -721,17 +757,12 @@ class _PreviewWidget(QTextBrowser): self.anchorClicked.connect(self._linkClicked) # Document Age - aPalette = self.palette() - aPalette.setColor(QPalette.ColorRole.Window, aPalette.toolTipBase().color()) - aPalette.setColor(QPalette.ColorRole.WindowText, aPalette.toolTipText().color()) - aFont = self.font() aFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) self.ageLabel = QLabel("", self) self.ageLabel.setIndent(0) self.ageLabel.setFont(aFont) - self.ageLabel.setPalette(aPalette) self.ageLabel.setAutoFillBackground(True) self.ageLabel.setAlignment(QtAlignCenter) self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize)) @@ -750,6 +781,7 @@ class _PreviewWidget(QTextBrowser): self._updateDocMargins() self._updateBuildAge() + self.updateTheme() self.setTextFont(CONFIG.textFont) # Age Timer @@ -815,6 +847,15 @@ class _PreviewWidget(QTextBrowser): QApplication.processEvents() QTimer.singleShot(300, self._postUpdate) + def updateTheme(self) -> None: + """Update theme elements.""" + logger.debug("Theme Update: _PreviewWidget") + + palette = QApplication.palette() + palette.setColor(QPalette.ColorRole.Window, palette.toolTipBase().color()) + palette.setColor(QPalette.ColorRole.WindowText, palette.toolTipText().color()) + self.ageLabel.setPalette(palette) + ## # Events ## @@ -907,7 +948,7 @@ class _StatsWidget(QWidget): self.minWidget = QWidget(self) self.maxWidget = QWidget(self) - self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold") + self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize) self.toggleButton.toggled.connect(self._toggleView) self._buildBottomPanel() @@ -922,6 +963,7 @@ class _StatsWidget(QWidget): self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) + self.updateTheme() self._toggleView(False) @@ -946,6 +988,11 @@ class _StatsWidget(QWidget): self.maxHeadWordChars.setText(f"{data.get(nwStats.WCHARS_TITLE, 0):n}") self.maxTextWordChars.setText(f"{data.get(nwStats.WCHARS_TEXT, 0):n}") + def updateTheme(self) -> None: + """Update theme elements.""" + logger.debug("Theme Update: _StatsWidget") + self.toggleButton.setThemeIcon("unfold") + ## # Private Slots ## diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 407eff5e..dd5bc50c 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -154,6 +154,7 @@ class GuiBuildSettings(NToolDialog): self.outerBox.setSpacing(12) self.setLayout(self.outerBox) + self.updateTheme(init=True) # Set Default Tab self.sidebar.setSelected(self.OPT_FILTERS) @@ -170,6 +171,22 @@ class GuiBuildSettings(NToolDialog): self.optTabHeadings.loadContent() self.optTabFormatting.loadContent() + def updateTheme(self, *, init: bool = False) -> None: + """Update theme elements.""" + logger.debug("Theme Update: GuiBuildSettings, init=%s", init) + + if not init: + self.btnApply.updateIcon() + self.btnSave.updateIcon() + self.btnClose.updateIcon() + + self.optTabSelect.updateTheme() + self.optTabHeadings.updateTheme() + self.optTabFormatting.updateTheme() + + self.titleLabel.setTextColors(color=SHARED.theme.helpText) + self.sidebar.setLabelColor(SHARED.theme.helpText) + ## # Properties ## @@ -326,15 +343,13 @@ class _FilterTab(NFixedPage): self.includedButton = NIconToolButton(self, iSz) self.includedButton.setToolTip(self.tr("Always included")) - self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED]) self.includedButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_INCLUDED)) self.excludedButton = NIconToolButton(self, iSz) self.excludedButton.setToolTip(self.tr("Always excluded")) - self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED]) self.excludedButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_EXCLUDED)) - self.resetButton = NIconToolButton(self, iSz, "revert", "green") + self.resetButton = NIconToolButton(self, iSz) self.resetButton.setToolTip(self.tr("Reset to default")) self.resetButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_FILTERED)) @@ -376,6 +391,7 @@ class _FilterTab(NFixedPage): pOptions.getInt("GuiBuildSettings", "filterWidth", 300), ]) + self.updateTheme(init=True) self.setCentralWidget(self.mainSplit) def loadContent(self) -> None: @@ -389,6 +405,20 @@ class _FilterTab(NFixedPage): m, n = (sizes[0], sizes[1]) if len(sizes) >= 2 else (0, 0) return m, n + def updateTheme(self, *, init: bool = False) -> None: + """Update theme elements.""" + logger.debug("Theme Update: _FilterTab, init=%s", init) + + if not init: + self._statusFlags[self.F_FILTERED] = SHARED.theme.getIcon("filter", "orange") + self._statusFlags[self.F_INCLUDED] = SHARED.theme.getIcon("pin", "blue") + self._statusFlags[self.F_EXCLUDED] = SHARED.theme.getIcon("exclude", "red") + self.loadContent() + + self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED]) + self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED]) + self.resetButton.setThemeIcon("revert", "green") + ## # Slots ## @@ -555,7 +585,7 @@ class _HeadingsTab(NScrollablePage): self.lblPart = QLabel(self._build.getLabel("headings.fmtPart"), self) self.fmtPart = QLineEdit("", self) self.fmtPart.setReadOnly(True) - self.btnPart = NIconToolButton(self, iSz, "edit", "green") + self.btnPart = NIconToolButton(self, iSz) self.btnPart.clicked.connect(qtLambda(self._editHeading, self.EDIT_TITLE)) self.swtPart = NSwitch(self, height=iPx) self.hdePart = QLabel(trHide, self) @@ -572,7 +602,7 @@ class _HeadingsTab(NScrollablePage): self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter"), self) self.fmtChapter = QLineEdit("", self) self.fmtChapter.setReadOnly(True) - self.btnChapter = NIconToolButton(self, iSz, "edit", "green") + self.btnChapter = NIconToolButton(self, iSz) self.btnChapter.clicked.connect(qtLambda(self._editHeading, self.EDIT_CHAPTER)) self.swtChapter = NSwitch(self, height=iPx) self.hdeChapter = QLabel(trHide, self) @@ -589,7 +619,7 @@ class _HeadingsTab(NScrollablePage): self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered"), self) self.fmtUnnumbered = QLineEdit("", self) self.fmtUnnumbered.setReadOnly(True) - self.btnUnnumbered = NIconToolButton(self, iSz, "edit", "green") + self.btnUnnumbered = NIconToolButton(self, iSz) self.btnUnnumbered.clicked.connect(qtLambda(self._editHeading, self.EDIT_UNNUM)) self.swtUnnumbered = NSwitch(self, height=iPx) self.hdeUnnumbered = QLabel(trHide, self) @@ -606,7 +636,7 @@ class _HeadingsTab(NScrollablePage): self.lblScene = QLabel(self._build.getLabel("headings.fmtScene"), self) self.fmtScene = QLineEdit("", self) self.fmtScene.setReadOnly(True) - self.btnScene = NIconToolButton(self, iSz, "edit", "green") + self.btnScene = NIconToolButton(self, iSz) self.btnScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_SCENE)) self.swtScene = NSwitch(self, height=iPx) self.hdeScene = QLabel(trHide, self) @@ -623,7 +653,7 @@ class _HeadingsTab(NScrollablePage): self.lblAScene = QLabel(self._build.getLabel("headings.fmtAltScene"), self) self.fmtAScene = QLineEdit("", self) self.fmtAScene.setReadOnly(True) - self.btnAScene = NIconToolButton(self, iSz, "edit", "green") + self.btnAScene = NIconToolButton(self, iSz) self.btnAScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_HSCENE)) self.swtAScene = NSwitch(self, height=iPx) self.hdeAScene = QLabel(trHide, self) @@ -640,7 +670,7 @@ class _HeadingsTab(NScrollablePage): self.lblSection = QLabel(self._build.getLabel("headings.fmtSection"), self) self.fmtSection = QLineEdit("", self) self.fmtSection.setReadOnly(True) - self.btnSection = NIconToolButton(self, iSz, "edit", "green") + self.btnSection = NIconToolButton(self, iSz) self.btnSection.clicked.connect(qtLambda(self._editHeading, self.EDIT_SECTION)) self.swtSection = NSwitch(self, height=iPx) self.hdeSection = QLabel(trHide, self) @@ -774,8 +804,23 @@ class _HeadingsTab(NScrollablePage): self.outerBox.addLayout(self.layoutMatrix) self.outerBox.addStretch(1) + self.updateTheme() self.setCentralLayout(self.outerBox) + def updateTheme(self) -> None: + """Update theme elements.""" + logger.debug("Theme Update: _HeadingsTab") + + self.btnPart.setThemeIcon("edit", "green") + self.btnChapter.setThemeIcon("edit", "green") + self.btnUnnumbered.setThemeIcon("edit", "green") + self.btnScene.setThemeIcon("edit", "green") + self.btnAScene.setThemeIcon("edit", "green") + self.btnSection.setThemeIcon("edit", "green") + + self.formSyntax.initHighlighter() + self.formSyntax.rehighlight() + def loadContent(self) -> None: """Populate the widgets.""" def fmtBreak(text: str) -> str: @@ -907,10 +952,14 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): def __init__(self, document: QTextDocument | None) -> None: super().__init__(document) - syntax = SHARED.theme.syntaxTheme self._fmtSymbol = QTextCharFormat() - self._fmtSymbol.setForeground(syntax.head) self._fmtFormat = QTextCharFormat() + self.initHighlighter() + + def initHighlighter(self) -> None: + """Update theme elements.""" + syntax = SHARED.theme.syntaxTheme + self._fmtSymbol.setForeground(syntax.head) self._fmtFormat.setForeground(syntax.emph) def highlightBlock(self, text: str) -> None: @@ -936,6 +985,7 @@ class _FormattingTab(NScrollableForm): self.setHelpTextStyle(SHARED.theme.helpText) self.buildForm() + self.updateTheme() def buildForm(self) -> None: """Build the formatting form.""" @@ -979,7 +1029,7 @@ class _FormattingTab(NScrollableForm): lambda keyword=keyword: self._updateIgnoredKeywords(keyword) ) - self.ignoredKeywordsButton = NIconToolButton(self, iSz, "add", "green") + self.ignoredKeywordsButton = NIconToolButton(self, iSz) self.ignoredKeywordsButton.setMenu(self.mnKeywords) self.addRow( self._build.getLabel("text.ignoredKeywords"), self.ignoredKeywords, @@ -1001,7 +1051,7 @@ class _FormattingTab(NScrollableForm): # Text Font self.textFont = QLineEdit(self) self.textFont.setReadOnly(True) - self.btnTextFont = NIconToolButton(self, iSz, "font") + self.btnTextFont = NIconToolButton(self, iSz) self.btnTextFont.clicked.connect(self._selectFont) self.addRow( self._build.getLabel("format.textFont"), self.textFont, @@ -1060,12 +1110,12 @@ class _FormattingTab(NScrollableForm): self._sidebar.addButton(title, section) self.addGroupLabel(title, section) - pixT = SHARED.theme.getPixmap("margin_top", (iPx, iPx)) - pixB = SHARED.theme.getPixmap("margin_bottom", (iPx, iPx)) - pixL = SHARED.theme.getPixmap("margin_left", (iPx, iPx)) - pixR = SHARED.theme.getPixmap("margin_right", (iPx, iPx)) - pixH = SHARED.theme.getPixmap("fit_height", (iPx, iPx)) - pixW = SHARED.theme.getPixmap("fit_width", (iPx, iPx)) + self.pixT = QLabel(self) + self.pixB = QLabel(self) + self.pixL = QLabel(self) + self.pixR = QLabel(self) + self.pixH = QLabel(self) + self.pixW = QLabel(self) # Title self.titleMarginT = NDoubleSpinBox(self) @@ -1076,7 +1126,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.titleMargin"), - [pixT, self.titleMarginT, 6, pixB, self.titleMarginB], + [self.pixT, self.titleMarginT, 6, self.pixB, self.titleMarginB], unit="em", ) @@ -1089,7 +1139,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.h1Margin"), - [pixT, self.h1MarginT, 6, pixB, self.h1MarginB], + [self.pixT, self.h1MarginT, 6, self.pixB, self.h1MarginB], unit="em", ) @@ -1102,7 +1152,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.h2Margin"), - [pixT, self.h2MarginT, 6, pixB, self.h2MarginB], + [self.pixT, self.h2MarginT, 6, self.pixB, self.h2MarginB], unit="em", ) @@ -1115,7 +1165,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.h3Margin"), - [pixT, self.h3MarginT, 6, pixB, self.h3MarginB], + [self.pixT, self.h3MarginT, 6, self.pixB, self.h3MarginB], unit="em", ) @@ -1128,7 +1178,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.h4Margin"), - [pixT, self.h4MarginT, 6, pixB, self.h4MarginB], + [self.pixT, self.h4MarginT, 6, self.pixB, self.h4MarginB], unit="em", ) @@ -1141,7 +1191,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.textMargin"), - [pixT, self.textMarginT, 6, pixB, self.textMarginB], + [self.pixT, self.textMarginT, 6, self.pixB, self.textMarginB], unit="em", ) @@ -1154,7 +1204,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.sepMargin"), - [pixT, self.sepMarginT, 6, pixB, self.sepMarginB], + [self.pixT, self.sepMarginT, 6, self.pixB, self.sepMarginB], unit="em", ) @@ -1188,7 +1238,7 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.pageSize"), - [self.pageSize, 6, pixW, self.pageWidth, 6, pixH, self.pageHeight], + [self.pageSize, 6, self.pixW, self.pageWidth, 6, self.pixH, self.pageHeight], ) # Page Margins @@ -1206,11 +1256,11 @@ class _FormattingTab(NScrollableForm): self.addRow( self._build.getLabel("format.pageMargins"), - [pixT, self.topMargin, 6, pixB, self.bottomMargin], + [self.pixT, self.topMargin, 6, self.pixB, self.bottomMargin], ) self.addRow( "", - [pixL, self.leftMargin, 6, pixR, self.rightMargin], + [self.pixL, self.leftMargin, 6, self.pixR, self.rightMargin], ) # Open Document @@ -1224,7 +1274,7 @@ class _FormattingTab(NScrollableForm): # Header self.odtPageHeader = QLineEdit(self) self.odtPageHeader.setMinimumWidth(200) - self.btnPageHeader = NIconToolButton(self, iSz, "revert", "green") + self.btnPageHeader = NIconToolButton(self, iSz) self.btnPageHeader.clicked.connect(self._resetPageHeader) self.addRow( self._build.getLabel("doc.pageHeader"), self.odtPageHeader, @@ -1264,6 +1314,22 @@ class _FormattingTab(NScrollableForm): # Finalise self.finalise() + def updateTheme(self) -> None: + """Update theme elements.""" + logger.debug("Theme Update: _FormattingTab") + + self.ignoredKeywordsButton.setThemeIcon("add", "green") + self.btnTextFont.setThemeIcon("font") + self.btnPageHeader.setThemeIcon("revert", "green") + + iPx = SHARED.theme.baseIconHeight + self.pixT.setPixmap(SHARED.theme.getPixmap("margin_top", (iPx, iPx))) + self.pixB.setPixmap(SHARED.theme.getPixmap("margin_bottom", (iPx, iPx))) + self.pixL.setPixmap(SHARED.theme.getPixmap("margin_left", (iPx, iPx))) + self.pixR.setPixmap(SHARED.theme.getPixmap("margin_right", (iPx, iPx))) + self.pixH.setPixmap(SHARED.theme.getPixmap("fit_height", (iPx, iPx))) + self.pixW.setPixmap(SHARED.theme.getPixmap("fit_width", (iPx, iPx))) + def loadContent(self) -> None: """Populate the widgets.""" # Text Content diff --git a/tests/test_ext/test_ext_modified.py b/tests/test_ext/test_ext_modified.py index 7e65de1a..fa66a528 100644 --- a/tests/test_ext/test_ext_modified.py +++ b/tests/test_ext/test_ext_modified.py @@ -22,12 +22,13 @@ from __future__ import annotations import pytest -from PyQt6.QtCore import QEvent, QPoint, QPointF, Qt +from PyQt6.QtCore import QEvent, QPoint, QPointF, QSize, Qt from PyQt6.QtGui import QKeyEvent, QMouseEvent, QStandardItem, QStandardItemModel, QWheelEvent from PyQt6.QtWidgets import QWidget from novelwriter.extensions.modified import ( - NClickableLabel, NComboBox, NDialog, NDoubleSpinBox, NSpinBox, NTreeView + NClickableLabel, NComboBox, NDialog, NDoubleSpinBox, NIconToggleButton, + NIconToolButton, NSpinBox, NTreeView ) from novelwriter.types import QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected @@ -168,7 +169,7 @@ def testExtModified_NDoubleSpinBox(qtbot, monkeypatch): @pytest.mark.gui -def testExtModified_NClickableLabel(qtbot, monkeypatch): +def testExtModified_NClickableLabel(qtbot): """Test the NClickableLabel class.""" widget = NClickableLabel() dialog = SimpleDialog(widget) @@ -181,3 +182,23 @@ def testExtModified_NClickableLabel(qtbot, monkeypatch): with qtbot.waitSignal(widget.mouseClicked): widget.mousePressEvent(event) + + +@pytest.mark.gui +def testExtModified_ToolButtons(qtbot): + """Test the NIconToolButton and NIconToggleButton classes.""" + dialog = SimpleDialog(None) + + size = QSize(16, 16) + button1 = NIconToolButton(dialog, size, "add", "green") + button2 = NIconToggleButton(dialog, size, "bullet") + + assert button1.iconSize() == size + assert button2.iconSize() == size + + assert button1.icon().isNull() is False + assert button2.icon().isNull() is False + + dialog.addWidget(button1) + dialog.addWidget(button2) + dialog.show() diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index 0d74dbff..3d8b86cb 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -72,6 +72,9 @@ def testToolManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd): manus.btnPreview.click() assert manus.docPreview.toPlainText().strip() == allText + # Trigger a theme update, which is only a visual refresh, but it shouldn't crash + manus.updateTheme() + nwGUI.closeProject() # This should auto-close the manuscript tool # qtbot.stop() @@ -148,6 +151,9 @@ def testToolManuscript_Builds(qtbot, nwGUI, projPath): assert new is not None assert new.name == "Test Build 2" + # Trigger a theme update, which should propagate to settings + nwGUI.refreshThemeColors() + # Close the dialog should also close the child dialogs manus.btnClose.click() if isinstance(bSettings, GuiBuildSettings):