diff --git a/Furious/Qt/StyleSheets/Controls.py b/Furious/Qt/StyleSheets/Controls.py index 17fdfbc..6a7fafe 100644 --- a/Furious/Qt/StyleSheets/Controls.py +++ b/Furious/Qt/StyleSheets/Controls.py @@ -473,6 +473,7 @@ def controlStyleSheet( QComboBox QAbstractItemView {{ padding: 4px; + background-clip: border; border: 1px solid {palette['border_strong']}; border-radius: 6px; background-color: {palette['overlay']}; diff --git a/Furious/Qt/StyleSheets/DataViews.py b/Furious/Qt/StyleSheets/DataViews.py index fce639c..0b90937 100644 --- a/Furious/Qt/StyleSheets/DataViews.py +++ b/Furious/Qt/StyleSheets/DataViews.py @@ -28,7 +28,10 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon): QTreeView, QListView {{ border: 1px solid {palette['border']}; - border-radius: 8px; + border-radius: 6px; + /* Keep rectangular viewports and headers inside the rounded frame. */ + padding: 2px; + background-clip: content; background-color: {palette['panel']}; alternate-background-color: {palette['panel_alt']}; gridline-color: {palette['border']}; @@ -37,6 +40,12 @@ def dataViewStyleSheet(palette, progressBarStyleSheet, checkIcon): selection-color: {palette['selection_text']}; }} + QTableView, + QTableWidget {{ + padding: 0; + background-clip: border; + }} + QTableView::item, QTableWidget::item, QTreeView::item, diff --git a/tests/README.md b/tests/README.md index 520c91e..3fc9866 100644 --- a/tests/README.md +++ b/tests/README.md @@ -55,7 +55,7 @@ strategy in an individual test. | Bounded service work, update validation, plugin UI, and worker lifetime | `test_service_runtime.py` | | Frozenlib state helpers and mocked platform-operation boundaries | `test_frozenlib.py` | | Settings sandbox, navigation overlay behavior, public exports, and scale/theme isolation | `test_isolation_and_navigation.py`, `test_public_api.py`, `test_layout_matrix.py` | -| Shared Fluent visual states, rounded menu/combo popup transparency and interaction, and native line-edit clear-button alignment and theme changes | `test_stylesheet_states.py` | +| Shared Fluent visual states, zero table padding and intact rounded list corners, rounded menu/combo popup transparency and interaction, and native line-edit clear-button alignment and theme changes | `test_stylesheet_states.py` | | Theme cross-fade activation, interruption, multi-window cleanup, and animation policy | `test_theme_transition.py` | | AppQMainWindow lifecycle, subclass policies, geometry restoration, and migration | `test_main_window_geometry.py` | | AppQDialog first-presentation geometry, native show paths, centering, and async lifetime | `test_dialog_geometry.py` | diff --git a/tests/test_stylesheet_states.py b/tests/test_stylesheet_states.py index bb1b9cd..280bea2 100644 --- a/tests/test_stylesheet_states.py +++ b/tests/test_stylesheet_states.py @@ -19,11 +19,24 @@ from __future__ import annotations -from Furious.Qt import AppQComboBox, AppQLineEdit, AppQMenu, AppStyleSheet +from Furious.Qt import ( + AppQComboBox, + AppQLineEdit, + AppQListView, + AppQMenu, + AppQTableView, + AppStyleSheet, +) from Furious.Widget.NavigationView import NavigationView from PySide6 import QtCore -from PySide6.QtGui import QColor, QPalette +from PySide6.QtGui import ( + QColor, + QImage, + QPalette, + QStandardItem, + QStandardItemModel, +) from PySide6.QtTest import QTest from PySide6.QtWidgets import ( QGraphicsDropShadowEffect, @@ -91,6 +104,114 @@ class StyleSheetStateRenderingTest(unittest.TestCase): return color + def assertItemViewInsetsAndListCorners(self, view, theme): + """Check zero table padding and intact padded list corners.""" + if isinstance(view, AppQTableView): + self.assertEqual(view.contentsRect(), view.rect().adjusted(1, 1, -1, -1)) + return + + actual = view.grab().toImage() + frame = QImage(actual.size(), QImage.Format.Format_ARGB32_Premultiplied) + frame.setDevicePixelRatio(actual.devicePixelRatio()) + frame.fill(QtCore.Qt.transparent) + view.render(frame, renderFlags=QWidget.RenderFlag.DrawWindowBackground) + panel = QColor(AppStyleSheet.Palettes[theme]['panel']) + paddingPoint = QtCore.QPoint(view.width() // 2, 2) + host = view.parentWidget() + hostImage = host.grab().toImage() + hostPoint = view.mapTo(host, paddingPoint) + self.assertEqual( + hostImage.pixelColor( + round(hostPoint.x() * hostImage.devicePixelRatio()), + round(hostPoint.y() * hostImage.devicePixelRatio()), + ), + QColor(AppStyleSheet.Palettes[theme]['window']), + ) + cornerSize = round(8 * actual.devicePixelRatio()) + + for left in (0, actual.width() - cornerSize): + for top in (0, actual.height() - cornerSize): + checked = 0 + + for x in range(left, left + cornerSize): + for y in range(top, top + cornerSize): + expected = frame.pixelColor(x, y) + + if expected.alpha() and expected != panel: + self.assertEqual( + actual.pixelColor(x, y), + expected, + f"frame pixel {(x, y)}", + ) + checked += 1 + + self.assertGreater(checked, 0) + + def testItemViewInsetsAndListCornersAcrossThemes(self): + """Keep table insets and list corners stable while resizing and scrolling.""" + app = application() + originalStyleSheet = app.styleSheet() + + try: + for viewType in (AppQTableView, AppQListView): + host = QWidget() + view = viewType(host) + view.move(10, 10) + model = QStandardItemModel(view) + + for row in range(20): + model.appendRow([QStandardItem(f'Item {row}') for _ in range(5)]) + + view.setModel(model) + view.setAlternatingRowColors(True) + + try: + for theme in (AppStyleSheet.Light, AppStyleSheet.Dark): + app.setStyleSheet(AppStyleSheet.forTheme(theme)) + + for direction in (QtCore.Qt.LeftToRight, QtCore.Qt.RightToLeft): + view.setLayoutDirection(direction) + + for size in ( + QtCore.QSize(360, 180), + QtCore.QSize(620, 340), + ): + with self.subTest( + view=viewType, + theme=theme, + direction=direction, + size=size, + ): + view.resize(size) + host.resize(size + QtCore.QSize(20, 20)) + host.show() + view.setCurrentIndex(model.index(0, 0)) + view.scrollToTop() + processQtEvents() + self.assertItemViewInsetsAndListCorners(view, theme) + view.setCurrentIndex(model.index(19, 4)) + view.scrollToBottom() + view.horizontalScrollBar().setValue( + view.horizontalScrollBar().maximum() + ) + processQtEvents() + self.assertItemViewInsetsAndListCorners(view, theme) + + model.clear() + processQtEvents() + self.assertItemViewInsetsAndListCorners(view, AppStyleSheet.Dark) + viewportImage = view.viewport().grab().toImage() + self.assertEqual( + viewportImage.pixelColor(viewportImage.rect().center()), + QColor(AppStyleSheet.Palettes[AppStyleSheet.Dark]['panel']), + ) + finally: + host.close() + host.deleteLater() + processQtEvents() + finally: + app.setStyleSheet(originalStyleSheet) + def assertRoundedPopup(self, popup): """Require clear outer corners and an opaque painted surface.""" image = popup.grab().toImage() @@ -184,6 +305,15 @@ class StyleSheetStateRenderingTest(unittest.TestCase): processQtEvents() self.assertIs(combo.view().window(), popup) self.assertRoundedPopup(popup) + # Dropdown padding must remain opaque over the page below. + popupImage = combo.view().grab().toImage() + self.assertEqual( + popupImage.pixelColor( + popupImage.width() // 2, + round(2 * popupImage.devicePixelRatio()), + ).alpha(), + 255, + ) self.assertFalse(shiboken6.isValid(shadow)) QTest.keyClick(combo.view(), QtCore.Qt.Key_Down) QTest.keyClick(combo.view(), QtCore.Qt.Key_Return)