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