Add a new placeholder icon and clean up project search styling (#1781)

This commit is contained in:
Veronica Berglyd Olsen
2024-03-27 13:46:31 +01:00
committed by GitHub
9 changed files with 114 additions and 81 deletions
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m0 0v4l8 8-8 8v4h4l8-8 8 8h4v-4l-8-8 8-8v-4h-4l-8 8-8-8h-4z" fill="#f00" stroke-width=".70711"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

+6 -1
View File
@@ -36,7 +36,7 @@ from configparser import ConfigParser
from urllib.parse import urljoin from urllib.parse import urljoin
from urllib.request import pathname2url from urllib.request import pathname2url
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QColor, QDesktopServices
from PyQt5.QtCore import QCoreApplication, QUrl from PyQt5.QtCore import QCoreApplication, QUrl
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -382,6 +382,11 @@ def numberToRoman(value: int, toLower: bool = False) -> str:
return roman.lower() if toLower else roman return roman.lower() if toLower else roman
def cssCol(col: QColor, alpha: int | None = None) -> str:
"""Convert a QColor object to an rgba entry to use in CSS."""
return f"rgba({col.red()}, {col.green()}, {col.blue()}, {alpha or col.alpha()})"
## ##
# Encoder Functions # Encoder Functions
## ##
+19 -28
View File
@@ -41,6 +41,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol
from novelwriter.constants import nwHeaders, nwUnicode from novelwriter.constants import nwHeaders, nwUnicode
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
@@ -467,35 +468,25 @@ class GuiDocViewer(QTextBrowser):
"""Generate an appropriate style sheet for the document viewer, """Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme. based on the current syntax highlighter theme.
""" """
colText = SHARED.theme.colText colText = cssCol(SHARED.theme.colText)
colHead = SHARED.theme.colHead colHead = cssCol(SHARED.theme.colHead)
colVals = SHARED.theme.colVal colVals = cssCol(SHARED.theme.colVal)
colMark = SHARED.theme.colMark colMark = cssCol(SHARED.theme.colMark)
colKeys = SHARED.theme.colKey colKeys = cssCol(SHARED.theme.colKey)
colHide = SHARED.theme.colHidden colOpts = cssCol(SHARED.theme.colOpt)
colMods = SHARED.theme.colMod colHide = cssCol(SHARED.theme.colHidden)
colOpts = SHARED.theme.colOpt colMods = cssCol(SHARED.theme.colMod)
styleSheet = ( self.document().setDefaultStyleSheet(
"body {{color: rgb({rT}, {gT}, {bT});}}\n" f"body {{color: {colText};}}\n"
"h1, h2, h3, h4 {{color: rgb({rH}, {gH}, {bH});}}\n" f"h1, h2, h3, h4 {{color: {colHead};}}\n"
"a {{color: rgb({rA}, {gA}, {bA});}}\n" f"a {{color: {colVals};}}\n"
"mark {{background-color: rgba({rE}, {gE}, {bE}, {aE});}}\n" f"mark {{background-color: {colMark};}}\n"
".tags {{color: rgb({rK}, {gK}, {bK});}}\n" f".tags {{color: {colKeys};}}\n"
".optional {{color: rgb({rO}, {gO}, {bO});}}\n" f".optional {{color: {colOpts};}}\n"
".comment {{color: rgb({rC}, {gC}, {bC});}}\n" f".comment {{color: {colHide});}}\n"
".synopsis {{color: rgb({rM}, {gM}, {bM});}}\n" f".synopsis {{color: {colMods};}}\n"
".title {{text-align: center;}}\n" ".title {text-align: center;}\n"
).format(
rT=colText.red(), gT=colText.green(), bT=colText.blue(),
rH=colHead.red(), gH=colHead.green(), bH=colHead.blue(),
rA=colVals.red(), gA=colVals.green(), bA=colVals.blue(),
rE=colMark.red(), gE=colMark.green(), bE=colMark.blue(), aE=colMark.alpha(),
rK=colKeys.red(), gK=colKeys.green(), bK=colKeys.blue(),
rC=colHide.red(), gC=colHide.green(), bC=colHide.blue(),
rM=colMods.red(), gM=colMods.green(), bM=colMods.blue(),
rO=colOpts.red(), gO=colOpts.green(), bO=colOpts.blue(),
) )
self.document().setDefaultStyleSheet(styleSheet)
return return
+1 -1
View File
@@ -565,7 +565,7 @@ class GuiProjectTree(QTreeWidget):
# Auto Scroll # Auto Scroll
self._scrollMargin = SHARED.theme.baseIconSize self._scrollMargin = SHARED.theme.baseIconSize
self._scrollDirection = 0 self._scrollDirection = 0
self._scrollTimer = QTimer() self._scrollTimer = QTimer(self)
self._scrollTimer.timeout.connect(self._doAutoScroll) self._scrollTimer.timeout.connect(self._doAutoScroll)
self._scrollTimer.setInterval(250) self._scrollTimer.setInterval(250)
+30 -9
View File
@@ -28,14 +28,14 @@ import logging
from time import time from time import time
from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCursor, QKeyEvent, QPalette from PyQt5.QtGui import QCursor, QKeyEvent
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget, QFrame, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget, qApp QTreeWidgetItem, QVBoxLayout, QWidget, qApp
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt from novelwriter.common import checkInt, cssCol
from novelwriter.core.coretools import DocSearch from novelwriter.core.coretools import DocSearch
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
@@ -63,6 +63,7 @@ class GuiProjectSearch(QWidget):
iPx = SHARED.theme.baseIconSize iPx = SHARED.theme.baseIconSize
mPx = CONFIG.pxInt(2) mPx = CONFIG.pxInt(2)
tPx = CONFIG.pxInt(4)
self._time = time() self._time = time()
self._search = DocSearch() self._search = DocSearch()
@@ -71,7 +72,7 @@ class GuiProjectSearch(QWidget):
# Header # Header
self.viewLabel = QLabel(self.tr("Project Search")) self.viewLabel = QLabel(self.tr("Project Search"))
self.viewLabel.setFont(SHARED.theme.guiFontB) self.viewLabel.setFont(SHARED.theme.guiFontB)
self.viewLabel.setContentsMargins(mPx, mPx, 0, mPx) self.viewLabel.setContentsMargins(mPx, tPx, 0, mPx)
# Options # Options
self.searchOpt = QToolBar(self) self.searchOpt = QToolBar(self)
@@ -96,7 +97,7 @@ class GuiProjectSearch(QWidget):
# Search Box # Search Box
self.searchText = QLineEdit(self) self.searchText = QLineEdit(self)
self.searchText.setPlaceholderText(self.tr("Search text ...")) self.searchText.setPlaceholderText(self.tr("Search"))
self.searchText.setClearButtonEnabled(True) self.searchText.setClearButtonEnabled(True)
self.searchAction = self.searchText.addAction( self.searchAction = self.searchText.addAction(
@@ -110,6 +111,9 @@ class GuiProjectSearch(QWidget):
self.searchResult.setColumnCount(2) self.searchResult.setColumnCount(2)
self.searchResult.setIconSize(QSize(iPx, iPx)) self.searchResult.setIconSize(QSize(iPx, iPx))
self.searchResult.setIndentation(iPx) self.searchResult.setIndentation(iPx)
self.searchResult.setFrameStyle(QFrame.Shape.NoFrame)
self.searchResult.setUniformRowHeights(True)
self.searchResult.setAllColumnsShowFocus(True)
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked) self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected) self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
@@ -121,11 +125,16 @@ class GuiProjectSearch(QWidget):
# Assemble # Assemble
self.headerBox = QHBoxLayout() self.headerBox = QHBoxLayout()
self.headerBox.addWidget(self.viewLabel, 1) self.headerBox.addWidget(self.viewLabel, 1)
self.headerBox.addWidget(self.searchOpt, 0) self.headerBox.addWidget(self.searchOpt, 0, Qt.AlignmentFlag.AlignVCenter)
self.headerBox.setContentsMargins(0, 0, 0, 0) self.headerBox.setContentsMargins(0, 0, 0, 0)
self.headerBox.setSpacing(0)
self.headerWidget = QWidget(self)
self.headerWidget.setLayout(self.headerBox)
self.headerWidget.setContentsMargins(0, 0, 0, 0)
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.headerBox, 0) self.outerBox.addWidget(self.headerWidget, 0)
self.outerBox.addWidget(self.searchText, 0) self.outerBox.addWidget(self.searchText, 0)
self.outerBox.addWidget(self.searchResult, 1) self.outerBox.addWidget(self.searchResult, 1)
self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setContentsMargins(0, 0, 0, 0)
@@ -144,9 +153,21 @@ class GuiProjectSearch(QWidget):
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
bPx = CONFIG.pxInt(1)
mPx = CONFIG.pxInt(2)
qPalette = self.palette() qPalette = self.palette()
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base()) colBase = cssCol(qPalette.base().color())
self.setPalette(qPalette) colFocus = cssCol(qPalette.highlight().color())
self.headerWidget.setStyleSheet(f"background: {colBase};")
self.headerWidget.setAutoFillBackground(True)
self.setStyleSheet(
"QToolBar {padding: 0; background: none;} "
f"QLineEdit {{border: {bPx}px solid {colBase}; padding: {mPx}px;}} "
f"QLineEdit:focus {{border: {bPx}px solid {colFocus};}} "
)
self.searchAction.setIcon(SHARED.theme.getIcon("search")) self.searchAction.setIcon(SHARED.theme.getIcon("search"))
self.toggleCase.setIcon(SHARED.theme.getIcon("search_case")) self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
+20 -17
View File
@@ -38,7 +38,7 @@ from PyQt5.QtGui import (
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import NWConfigParser, minmax from novelwriter.common import NWConfigParser, cssCol, minmax
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -440,24 +440,24 @@ class GuiTheme:
# Flat Tab Widget and Tab Bar: # Flat Tab Widget and Tab Bar:
self._styleSheets[STYLES_FLAT_TABS] = ( self._styleSheets[STYLES_FLAT_TABS] = (
"QTabWidget::pane {{border: 0;}} " "QTabWidget::pane {border: 0;} "
"QTabWidget QTabBar::tab {{border: 0; padding: {0}px {1}px;}} " f"QTabWidget QTabBar::tab {{border: 0; padding: {bPx}px {dPx}px;}} "
"QTabWidget QTabBar::tab:selected {{color: rgb({2}, {3}, {4});}} " f"QTabWidget QTabBar::tab:selected {{color: {cssCol(hCol)};}} "
).format(bPx, dPx, hCol.red(), hCol.green(), hCol.blue()) )
# Minimal Tool Button # Minimal Tool Button
self._styleSheets[STYLES_MIN_TOOLBUTTON] = ( self._styleSheets[STYLES_MIN_TOOLBUTTON] = (
"QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} " f"QToolButton {{padding: {aPx}px; margin: 0; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} " f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
"QToolButton::menu-indicator {{image: none;}} " "QToolButton::menu-indicator {image: none;} "
).format(aPx, tCol.red(), tCol.green(), tCol.blue()) )
# Big Tool Button # Big Tool Button
self._styleSheets[STYLES_BIG_TOOLBUTTON] = ( self._styleSheets[STYLES_BIG_TOOLBUTTON] = (
"QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} " f"QToolButton {{padding: {cPx}px; margin: 0; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} " f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
"QToolButton::menu-indicator {{image: none;}} " "QToolButton::menu-indicator {image: none;} "
).format(cPx, tCol.red(), tCol.green(), tCol.blue()) )
return return
@@ -544,6 +544,9 @@ class GuiIcons:
self._confName = "icons.conf" self._confName = "icons.conf"
self._iconPath = CONFIG.assetPath("icons") self._iconPath = CONFIG.assetPath("icons")
# None Icon
self._noIcon = QIcon(str(self._iconPath / "none.svg"))
# Icon Theme Meta # Icon Theme Meta
self.themeName = "" self.themeName = ""
self.themeDescription = "" self.themeDescription = ""
@@ -680,7 +683,7 @@ class GuiIcons:
icon.addPixmap(pOne, QIcon.Mode.Normal, QIcon.State.On) icon.addPixmap(pOne, QIcon.Mode.Normal, QIcon.State.On)
icon.addPixmap(pTwo, QIcon.Mode.Normal, QIcon.State.Off) icon.addPixmap(pTwo, QIcon.Mode.Normal, QIcon.State.Off)
return icon return icon
return QIcon() return self._noIcon
def getPixmap(self, name: str, size: tuple[int, int]) -> QPixmap: def getPixmap(self, name: str, size: tuple[int, int]) -> QPixmap:
"""Return an icon from the icon buffer as a QPixmap. If it """Return an icon from the icon buffer as a QPixmap. If it
@@ -712,7 +715,7 @@ class GuiIcons:
elif tLayout == nwItemLayout.NOTE: elif tLayout == nwItemLayout.NOTE:
iconName = "proj_note" iconName = "proj_note"
if iconName is None: if iconName is None:
return QIcon() return self._noIcon
return self.getIcon(iconName) return self.getIcon(iconName)
@@ -753,7 +756,7 @@ class GuiIcons:
""" """
if name not in self.ICON_KEYS: if name not in self.ICON_KEYS:
logger.error("Requested unknown icon name '%s'", name) logger.error("Requested unknown icon name '%s'", name)
return QIcon() return self._noIcon
# If we just want the app icons, return right away # If we just want the app icons, return right away
if name == "novelwriter": if name == "novelwriter":
@@ -769,7 +772,7 @@ class GuiIcons:
# If we didn't find one, give up and return an empty icon # If we didn't find one, give up and return an empty icon
logger.warning("Did not load an icon for '%s'", name) logger.warning("Did not load an icon for '%s'", name)
return QIcon() return self._noIcon
# END Class GuiIcons # END Class GuiIcons
+13 -13
View File
@@ -42,7 +42,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.common import formatInt, makeFileNameSafe from novelwriter.common import cssCol, formatInt, makeFileNameSafe
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.coretools import ProjectBuilder from novelwriter.core.coretools import ProjectBuilder
from novelwriter.extensions.configlayout import NWrappedWidgetBox from novelwriter.extensions.configlayout import NWrappedWidgetBox
@@ -52,7 +52,7 @@ from novelwriter.extensions.versioninfo import VersionInfoWidget
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PANEL_ALPHA = 0.7 PANEL_ALPHA = 178
class GuiWelcome(QDialog): class GuiWelcome(QDialog):
@@ -306,12 +306,12 @@ class _OpenProjectPage(QWidget):
self._selectFirstItem() self._selectFirstItem()
baseCol = self.palette().base().color() mPx = CONFIG.pxInt(4)
self.setStyleSheet(( baseCol = cssCol(self.palette().base().color(), PANEL_ALPHA)
"QListView {{border: none; background: rgba({r},{g},{b},{a});}} " self.setStyleSheet(
"QLineEdit {{border: none; background: rgba({r},{g},{b},{a}); padding: {m}px;}} " f"QListView {{border: none; background: {baseCol};}} "
).format(r=baseCol.red(), g=baseCol.green(), b=baseCol.blue(), f"QLineEdit {{border: none; background: {baseCol}; padding: {mPx}px;}} "
a=PANEL_ALPHA, m=CONFIG.pxInt(4))) )
return return
@@ -518,11 +518,11 @@ class _NewProjectPage(QWidget):
# Styles # Styles
# ====== # ======
baseCol = self.palette().base().color() baseCol = cssCol(self.palette().base().color(), PANEL_ALPHA)
self.setStyleSheet(( self.setStyleSheet(
"QScrollArea {{border: none; background: rgba({r},{g},{b},{a});}} " f"QScrollArea {{border: none; background: {baseCol};}} "
"_NewProjectForm {{border: none; background: rgba({r},{g},{b},{a});}} " f"_NewProjectForm {{border: none; background: {baseCol};}} "
).format(r=baseCol.red(), g=baseCol.green(), b=baseCol.blue(), a=PANEL_ALPHA)) )
return return
+15 -6
View File
@@ -29,16 +29,16 @@ from xml.etree import ElementTree as ET
from tools import writeFile from tools import writeFile
from mocked import causeOSError from mocked import causeOSError
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QColor, QDesktopServices
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from novelwriter.common import ( from novelwriter.common import (
checkBool, checkFloat, checkInt, checkIntTuple, checkPath, checkString, checkBool, checkFloat, checkInt, checkIntTuple, checkPath, checkString,
checkStringNone, checkUuid, formatFileFilter, formatInt, formatTime, checkStringNone, checkUuid, cssCol, formatFileFilter, formatInt,
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, formatTime, formatTimeStamp, formatVersion, fuzzyTime, getFileSize,
isItemClass, isItemLayout, isItemType, isTitleTag, jsonEncode, hexToInt, isHandle, isItemClass, isItemLayout, isItemType, isTitleTag,
makeFileNameSafe, minmax, numberToRoman, NWConfigParser, openExternalPath, jsonEncode, makeFileNameSafe, minmax, numberToRoman, NWConfigParser,
readTextFile, simplified, transferCase, xmlIndent, yesNo openExternalPath, readTextFile, simplified, transferCase, xmlIndent, yesNo
) )
@@ -494,6 +494,15 @@ def testBaseCommon_numberToRoman():
# END Test testBaseCommon_numberToRoman # END Test testBaseCommon_numberToRoman
@pytest.mark.base
def testBaseCommon_cssCol():
"""Test the cssCol function."""
assert cssCol(QColor(0, 0, 0, 0)) == "rgba(0, 0, 0, 0)"
assert cssCol(QColor(10, 20, 30, 40)) == "rgba(10, 20, 30, 40)"
# END Test testBaseCommon_cssCol
@pytest.mark.base @pytest.mark.base
def testBaseCommon_jsonEncode(): def testBaseCommon_jsonEncode():
"""Test the jsonEncode function.""" """Test the jsonEncode function."""
+6 -6
View File
@@ -332,7 +332,7 @@ def testGuiTheme_LoadIcons(qtbot):
# Load an unknown icon # Load an unknown icon
qIcon = iconCache.getIcon("stuff") qIcon = iconCache.getIcon("stuff")
assert isinstance(qIcon, QIcon) assert isinstance(qIcon, QIcon)
assert qIcon.isNull() is True assert qIcon == iconCache._noIcon
# Load an icon, it is likely already cached # Load an icon, it is likely already cached
qIcon = iconCache.getIcon("add") qIcon = iconCache.getIcon("add")
@@ -349,17 +349,17 @@ def testGuiTheme_LoadIcons(qtbot):
# Load app icon # Load app icon
qIcon = iconCache.getIcon("novelwriter") qIcon = iconCache.getIcon("novelwriter")
assert isinstance(qIcon, QIcon) assert isinstance(qIcon, QIcon)
assert qIcon.isNull() is False assert qIcon != iconCache._noIcon
# Load mime icon # Load mime icon
qIcon = iconCache.getIcon("proj_nwx") qIcon = iconCache.getIcon("proj_nwx")
assert isinstance(qIcon, QIcon) assert isinstance(qIcon, QIcon)
assert qIcon.isNull() is False assert qIcon != iconCache._noIcon
# Toggle icon # Toggle icon
qIcon = iconCache.getToggleIcon("bullet", (24, 24)) qIcon = iconCache.getToggleIcon("bullet", (24, 24))
assert isinstance(qIcon, QIcon) assert isinstance(qIcon, QIcon)
assert qIcon.isNull() is False assert qIcon != iconCache._noIcon
pOn = qIcon.pixmap(24, 24, QIcon.Mode.Normal, QIcon.State.On) pOn = qIcon.pixmap(24, 24, QIcon.Mode.Normal, QIcon.State.On)
pOff = qIcon.pixmap(24, 24, QIcon.Mode.Normal, QIcon.State.Off) pOff = qIcon.pixmap(24, 24, QIcon.Mode.Normal, QIcon.State.Off)
assert pOn != pOff assert pOn != pOff
@@ -367,7 +367,7 @@ def testGuiTheme_LoadIcons(qtbot):
# Unknown toggle icon # Unknown toggle icon
qIcon = iconCache.getToggleIcon("stuff", (24, 24)) qIcon = iconCache.getToggleIcon("stuff", (24, 24))
assert isinstance(qIcon, QIcon) assert isinstance(qIcon, QIcon)
assert qIcon.isNull() is True assert qIcon == iconCache._noIcon
# Load Item Icons # Load Item Icons
# =============== # ===============
@@ -420,7 +420,7 @@ def testGuiTheme_LoadIcons(qtbot):
# No Type -> Null # No Type -> Null
assert iconCache.getItemIcon( assert iconCache.getItemIcon(
nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0" nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0"
).isNull() is True ) == iconCache._noIcon
# qtbot.stop() # qtbot.stop()