Skip to content

Commit 53dc9f2

Browse files
authored
Fix QTreeView item hover shift and excessive row padding (#1218)
* Fix QTreeView item hover shift and excessive row padding Replace mixed margin/padding stylesheet with consistent padding across all item states, and suppress Qt's focus rectangle with outline: 0. Fixes #1217. * Use palette(highlight) borders for hover/selection instead of overriding status colours Replace static dimgrey/grey background on hover and selected states with top/bottom borders using the system palette highlight colour. This keeps the alert status colour (CRITICAL, WARNING, etc.) visible at all times and works correctly in both dark and light themes. Fixes #1219. * Fix unreadable hover/selection on Windows dark mode Explicitly set background-color and color using palette(highlight) and palette(highlighted-text) on hover and selected states. Without this, Windows' native style engine paints its own dark selection background on top of the model's status colour, making text unreadable. * Reverse-video hover/selection using each row's own status colours Replace palette(highlight) with a custom delegate that swaps the row's foreground and background colours on hover and selection, preserving the alert status context (CRITICAL red, WARNING yellow, etc.) while still providing clear visual feedback. Works across all themes and platforms. * Use reverse-video delegate for status-aware hover and selection Replace palette(highlight) CSS with a QStyledItemDelegate that swaps each row's foreground and background colours on hover and selection. A trivial CSS :hover rule is retained solely to opt Qt into per-item hover tracking. The delegate calls super().paint() first, then overdraws background and text to guarantee the result is never overwritten by native OS styles (QWindowsVistaStyle, QMacStyle) which ignore option.backgroundBrush. Includes PyQt5/PyQt6 compatibility guards for QPalette.ColorRole and Qt.AlignmentFlag enum access. Tested on KDE Neon, Ubuntu 26.04 (GNOME), and Windows 11. Fixes #1219. * Fix delegate text shift on macOS by removing manual text drawing Replace the post-paint overdraw approach (fillRect + drawText) with colour swapping in initStyleOption instead. By stripping hover/selected state flags there, super().paint() draws the item as a plain item, letting each platform's native style handle text layout with its own margins. Eliminates the leftward text shift on macOS caused by the hardcoded adjusted(4, 0, -4, 0) text rect. --------- Co-authored-by: Dino Korah <691011+codemedic@users.noreply.github.com>
1 parent cfc7216 commit 53dc9f2

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

Nagstamon/qui/qt.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
from PyQt5.QtWidgets import QAbstractItemView, \
143143
QAction, \
144144
QApplication, \
145+
QStyledItemDelegate, \
145146
QColorDialog, \
146147
QComboBox, \
147148
QDialog, \
@@ -283,6 +284,7 @@ def get_sort_order_value(sort_order):
283284
from PyQt6.QtWebEngineWidgets import QWebEngineView as WebEngineView
284285
from PyQt6.QtWidgets import (QAbstractItemView,
285286
QApplication,
287+
QStyledItemDelegate,
286288
QColorDialog,
287289
QComboBox,
288290
QDialog,

Nagstamon/qui/widgets/treeview.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,84 @@
3939
from Nagstamon.qui.qt import (get_sort_order_value,
4040
QAbstractItemView,
4141
QAction,
42+
QBrush,
4243
QColor,
4344
QHeaderView,
4445
QKeySequence,
4546
QMenu,
4647
QObject,
48+
QPalette,
4749
QSignalMapper,
4850
QSizePolicy,
51+
QStyle,
52+
QStyledItemDelegate,
4953
Qt,
5054
QThread,
5155
QTimer,
5256
QTreeView,
5357
Signal,
5458
Slot)
59+
60+
try:
61+
_STATE_HOVER = QStyle.StateFlag.State_MouseOver
62+
_STATE_SELECTED = QStyle.StateFlag.State_Selected
63+
except AttributeError:
64+
_STATE_HOVER = QStyle.State_MouseOver
65+
_STATE_SELECTED = QStyle.State_Selected
5566
from Nagstamon.qui.widgets.app import app
5667
from Nagstamon.qui.widgets.menu import MenuAtCursor
5768
from Nagstamon.qui.widgets.model import Model
5869
from Nagstamon.servers import SERVER_TYPES, servers
5970

6071

72+
class StatusAwareDelegate(QStyledItemDelegate):
73+
"""
74+
Renders hover and selection as reverse video using each row's own
75+
status colours, so the alert context is always visible.
76+
77+
Colours are swapped in initStyleOption and the hover/selected state flags
78+
are stripped there too, so super().paint() draws the item as a plain
79+
(non-selected) item. Native OS styles (Windows, macOS) respect
80+
backgroundBrush and palette.Text for normal items, so no manual
81+
text drawing or post-paint overdraw is needed.
82+
"""
83+
84+
# QPalette colour role access differs between PyQt5 (<5.15) and PyQt6
85+
try:
86+
_ROLE_TEXT = QPalette.ColorRole.Text
87+
_ROLE_WINDOW_TEXT = QPalette.ColorRole.WindowText
88+
except AttributeError:
89+
_ROLE_TEXT = QPalette.Text
90+
_ROLE_WINDOW_TEXT = QPalette.WindowText
91+
92+
def initStyleOption(self, option, index):
93+
super().initStyleOption(option, index)
94+
95+
is_hovered = bool(option.state & _STATE_HOVER)
96+
is_selected = bool(option.state & _STATE_SELECTED)
97+
98+
if not (is_hovered or is_selected):
99+
return
100+
101+
# BackgroundRole and ForegroundRole both return QColor (qbrushes naming is misleading)
102+
bg_color = index.data(Qt.ItemDataRole.BackgroundRole)
103+
fg_color = index.data(Qt.ItemDataRole.ForegroundRole)
104+
105+
if bg_color is None or fg_color is None:
106+
return
107+
108+
# Reverse video: swap foreground and background
109+
option.backgroundBrush = QBrush(fg_color)
110+
option.palette.setColor(self._ROLE_TEXT, bg_color)
111+
option.palette.setColor(self._ROLE_WINDOW_TEXT, bg_color)
112+
113+
# Strip hover/selected so the style engine treats this as a plain item
114+
# and uses our backgroundBrush and Text palette colour rather than
115+
# the system selection highlight
116+
option.state &= ~_STATE_SELECTED
117+
option.state &= ~_STATE_HOVER
118+
119+
61120
class TreeView(QTreeView):
62121
"""
63122
attempt to get a less resource-hungry table/tree
@@ -142,17 +201,16 @@ def __init__(self, columncount, rowcount, sort_column, sort_order, server, paren
142201
self.header().sortIndicatorChanged.connect(self.sort_columns)
143202

144203
# set overall margin and hover colors - to be refined
145-
self.setStyleSheet('''QTreeView::item {margin: 5px;}
146-
QTreeView::item:hover {margin: 0px;
147-
padding: 5px;
148-
color: white;
149-
background-color: dimgrey;}
150-
QTreeView::item:selected {margin: 0px;
151-
padding: 5px;
152-
color: white;
153-
background-color: grey;}
204+
# The :hover and :selected rules here contain no visual changes — their sole purpose
205+
# is to make Qt enable per-item hover tracking so State_MouseOver is set in the delegate.
206+
self.setStyleSheet('''QTreeView {outline: 0;}
207+
QTreeView::item {padding: 3px;}
208+
QTreeView::item:hover {padding: 3px;}
209+
QTreeView::item:selected {padding: 3px;}
154210
''')
155211

212+
self.setItemDelegate(StatusAwareDelegate(self))
213+
156214
# set application font
157215
self.set_font()
158216

0 commit comments

Comments
 (0)