Unify search clear button styling

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-09-08 15:59:30 +08:00
parent 525b357c55
commit 9b9d93fee1
5 changed files with 127 additions and 4 deletions
+2
View File
@@ -278,6 +278,7 @@ class AppStyleSheet:
caretUpIcon = f'{iconPrefix}/caret-up-fill.svg'
caretRightIcon = f'{iconPrefix}/caret-right-fill.svg'
checkIcon = ':/Icons/bootstrap/white/check.svg'
clearIcon = f'{iconPrefix}/x.svg'
return composeStyleSheet(
palette,
@@ -286,4 +287,5 @@ class AppStyleSheet:
caretUpIcon,
caretRightIcon,
checkIcon,
clearIcon,
)
+24 -1
View File
@@ -25,7 +25,7 @@ from Furious.Qt.AppStyleSheet import *
from Furious.Qt.DynamicTheme import *
from Furious.Qt.DynamicTranslate import gettext as _
from Furious.Qt.QtGui import *
from Furious.Qt.Signals import connectWeakly
from Furious.Qt.Signals import connectWeakly, singleShotWeakly
from PySide6 import QtCore
from PySide6.QtGui import *
@@ -625,8 +625,31 @@ class AppQLineEdit(Mixins.QTranslatable, QLineEdit):
def __init__(self, *args, **kwargs):
"""Initialize the AppQLineEdit."""
self._clearIconRefreshPending = False
super().__init__(*args, **kwargs)
def changeEvent(self, event):
"""Refresh the cached native icon after Qt finishes applying its style."""
super().changeEvent(event)
if (
event.type() == QtCore.QEvent.Type.StyleChange
and self.isClearButtonEnabled()
and not self._clearIconRefreshPending
):
self._clearIconRefreshPending = True
singleShotWeakly(0, self, '_refreshClearIcon')
def _refreshClearIcon(self):
"""Recreate the native action outside Qt's child-polishing traversal."""
self._clearIconRefreshPending = False
if self.isClearButtonEnabled():
self.setClearButtonEnabled(False)
self.setClearButtonEnabled(True)
def retranslate(self):
"""Refresh translated text for the app q line edit."""
self.setPlaceholderText(_(self.placeholderText()))
+28
View File
@@ -26,6 +26,7 @@ def controlStyleSheet(
caretUpIcon,
caretRightIcon,
checkIcon,
clearIcon,
):
"""Return interactive control styling."""
return dedent(f"""
@@ -361,6 +362,33 @@ def controlStyleSheet(
selection-color: {palette['selection_text']};
}}
QLineEdit {{
lineedit-clear-button-icon: url("{clearIcon}");
}}
/* Embedded actions keep Qt's geometry, without toolbar padding. */
QLineEdit QToolButton {{
min-width: 0;
min-height: 0;
padding: 0;
margin: 0;
border: none;
border-radius: 4px;
background-color: transparent;
}}
QLineEdit QToolButton:hover {{
background-color: {palette['hover']};
}}
QLineEdit QToolButton:pressed {{
background-color: {palette['pressed']};
}}
QLineEdit QToolButton:disabled {{
background-color: transparent;
}}
QLineEdit:hover,
QTextEdit:hover,
QPlainTextEdit:hover,
+2
View File
@@ -29,6 +29,7 @@ def composeStyleSheet(
caretUpIcon,
caretRightIcon,
checkIcon,
clearIcon,
):
"""Return the complete stylesheet without exposing component fragments."""
return '\n\n'.join(
@@ -40,6 +41,7 @@ def composeStyleSheet(
caretUpIcon,
caretRightIcon,
checkIcon,
clearIcon,
),
dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon),
)
+71 -3
View File
@@ -19,15 +19,15 @@
from __future__ import annotations
from Furious.Qt import AppStyleSheet
from Furious.Qt import AppQLineEdit, AppStyleSheet
from Furious.Widget.NavigationView import NavigationView
from PySide6 import QtCore
from PySide6.QtGui import QColor, QPalette
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QPushButton, QStyleOptionButton, QWidget
from PySide6.QtWidgets import QPushButton, QStyleOptionButton, QToolButton, QWidget
from tests.support import application, collectAtBoundary, processQtEvents
from tests.support import application, collectAtBoundary, processQtEvents, waitFor
import unittest
@@ -84,6 +84,74 @@ class StyleSheetStateRenderingTest(unittest.TestCase):
return color
def testClearButtonKeepsNativeBehaviorAndGeometryAcrossThemes(self):
"""Keep embedded buttons centered, theme-correct and bounded after restyling."""
app = application()
originalStyleSheet = app.styleSheet()
edit = AppQLineEdit()
edit.setClearButtonEnabled(True)
edit.resize(340, 36)
edit.show()
edit.activateWindow()
edit.setFocus()
try:
for theme in (AppStyleSheet.Light, AppStyleSheet.Dark) * 3:
for direction in (QtCore.Qt.LeftToRight, QtCore.Qt.RightToLeft):
with self.subTest(theme=theme, direction=direction):
edit.setLayoutDirection(direction)
edit.setText('example search')
edit.setSelection(0, 7)
app.setStyleSheet(AppStyleSheet.forTheme(theme))
processQtEvents()
self.assertEqual(edit.selectedText(), 'example')
buttons = edit.findChildren(QToolButton)
self.assertEqual(len(buttons), 1)
button = buttons[0]
self.assertTrue(edit.rect().contains(button.geometry()))
self.assertLessEqual(
abs(
button.geometry().center().y()
- edit.rect().center().y()
),
1,
)
self.assertFalse(button.icon().isNull())
pixmap = button.icon().pixmap(16, 16).toImage()
colors = [
pixmap.pixelColor(x, y)
for x in range(pixmap.width())
for y in range(pixmap.height())
if pixmap.pixelColor(x, y).alpha() > 128
]
self.assertTrue(colors)
self.assertTrue(
all(
(color.lightness() > 128)
== (theme == AppStyleSheet.Dark)
for color in colors
)
)
QTest.mouseClick(button, QtCore.Qt.LeftButton)
self.assertEqual(edit.text(), '')
self.assertTrue(edit.hasFocus())
self.assertTrue(waitFor(lambda: not button.isVisible()))
edit.setText('read only')
edit.setReadOnly(True)
app.setStyleSheet(AppStyleSheet.forTheme(AppStyleSheet.Light))
processQtEvents()
self.assertEqual(edit.text(), 'read only')
self.assertFalse(edit.findChild(QToolButton).isEnabled())
app.setStyleSheet(AppStyleSheet.forTheme(AppStyleSheet.Dark))
edit.setClearButtonEnabled(False)
processQtEvents()
self.assertFalse(edit.isClearButtonEnabled())
self.assertFalse(edit.findChildren(QToolButton))
finally:
edit.close()
edit.deleteLater()
processQtEvents()
app.setStyleSheet(originalStyleSheet)
def testFlatAndLinkButtonsRetainFocusedOutlineDuringHover(self):
"""Do not let flat presentation clear the keyboard-focus frame."""
app = application()