Skip to content

Commit b8a41d9

Browse files
authored
Merge pull request #6 from healkeiser/dev
v11.4.0: Icons are now reacting to the hover/selection color
2 parents 172da1d + 53a70ea commit b8a41d9

File tree

5 files changed

+217
-25
lines changed

5 files changed

+217
-25
lines changed

fxgui/examples.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
QLabel,
7777
QTreeWidget,
7878
QTreeWidgetItem,
79+
QListWidget,
80+
QListWidgetItem,
7981
QSpinBox,
8082
QTabWidget,
8183
QGroupBox,
@@ -259,6 +261,88 @@ def _create_delegates_tab() -> QWidget:
259261
header.setWordWrap(True)
260262
layout.addWidget(header)
261263

264+
# Icon Color Test Section - Simple list and tree to test icon_on_accent colors
265+
icon_test_group = QGroupBox("Icon Color on Selection/Hover Test")
266+
icon_test_layout = QHBoxLayout(icon_test_group)
267+
268+
# Simple list widget
269+
test_list = QListWidget()
270+
test_list_delegate = fxwidgets.FXThumbnailDelegate()
271+
test_list_delegate.show_thumbnail = False
272+
test_list_delegate.show_status_dot = False
273+
test_list_delegate.show_status_label = False
274+
test_list.setItemDelegate(test_list_delegate)
275+
276+
list_items_data = [
277+
("Documents", "folder"),
278+
("Images", "image"),
279+
("Settings", "settings"),
280+
("Search", "search"),
281+
("Home", "home"),
282+
]
283+
test_list_items = []
284+
for name, icon_name in list_items_data:
285+
item = QListWidgetItem(name)
286+
item.setIcon(get_icon(icon_name))
287+
item.setData(Qt.UserRole + 50, icon_name) # Store icon name
288+
test_list.addItem(item)
289+
test_list_items.append(item)
290+
291+
icon_test_layout.addWidget(test_list)
292+
293+
# Simple tree widget
294+
test_tree = QTreeWidget()
295+
test_tree.setHeaderLabels(["Name", "Type"])
296+
test_tree_delegate = fxwidgets.FXThumbnailDelegate()
297+
test_tree_delegate.show_thumbnail = False
298+
test_tree_delegate.show_status_dot = False
299+
test_tree_delegate.show_status_label = False
300+
test_tree.setItemDelegate(test_tree_delegate)
301+
302+
tree_items_data = [
303+
("Project Files", "folder", "Folder", [
304+
("main.py", "code", "Python"),
305+
("utils.py", "code", "Python"),
306+
("config.yaml", "settings", "YAML"),
307+
]),
308+
("Assets", "image", "Folder", [
309+
("logo.png", "image", "PNG"),
310+
("icon.svg", "image", "SVG"),
311+
]),
312+
]
313+
test_tree_items = []
314+
for parent_name, parent_icon, parent_type, children in tree_items_data:
315+
parent_item = QTreeWidgetItem(test_tree, [parent_name, parent_type])
316+
parent_item.setIcon(0, get_icon(parent_icon))
317+
parent_item.setData(0, Qt.UserRole + 50, parent_icon)
318+
test_tree_items.append(parent_item)
319+
for child_name, child_icon, child_type in children:
320+
child_item = QTreeWidgetItem(parent_item, [child_name, child_type])
321+
child_item.setIcon(0, get_icon(child_icon))
322+
child_item.setData(0, Qt.UserRole + 50, child_icon)
323+
test_tree_items.append(child_item)
324+
325+
test_tree.expandAll()
326+
icon_test_layout.addWidget(test_tree)
327+
328+
# Theme-aware icon update for test widgets
329+
def update_icon_test_colors(_theme_name: str = None):
330+
for item in test_list_items:
331+
icon_name = item.data(Qt.UserRole + 50)
332+
if icon_name:
333+
item.setIcon(get_icon(icon_name))
334+
for item in test_tree_items:
335+
icon_name = item.data(0, Qt.UserRole + 50)
336+
if icon_name:
337+
item.setIcon(0, get_icon(icon_name))
338+
test_list.viewport().update()
339+
test_tree.viewport().update()
340+
341+
update_icon_test_colors()
342+
fxstyle.theme_manager.theme_changed.connect(update_icon_test_colors)
343+
344+
layout.addWidget(icon_test_group)
345+
262346
# Code example with syntax-highlighted code block
263347
code_group = QGroupBox("Theme-Aware Custom Colors Pattern")
264348
code_layout = QVBoxLayout(code_group)

fxgui/fxstyle.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ def _safe_apply_theme_styles(self) -> None:
374374
"get_feedback_colors",
375375
"get_theme_colors",
376376
"get_icon_color",
377+
"get_icon_on_accent_primary",
378+
"get_icon_on_accent_secondary",
377379
# Theme functions
378380
"get_available_themes",
379381
"get_theme",
@@ -606,9 +608,11 @@ def get_theme_colors() -> dict:
606608
- ``slider_thumb``: Slider handle color
607609
- ``slider_thumb_hover``: Slider handle hover/pressed
608610
609-
**Icon**:
611+
**Icon Colors**:
610612
611613
- ``icon``: Tint color for monochrome icons
614+
- ``icon_on_accent_primary``: Icon color on accent_primary backgrounds (optional)
615+
- ``icon_on_accent_secondary``: Icon color on accent_secondary backgrounds (optional)
612616
613617
Returns:
614618
Dictionary containing theme-specific colors.
@@ -655,6 +659,58 @@ def get_icon_color() -> str:
655659
return theme_colors.get("icon", "#b4b4b4")
656660

657661

662+
def get_icon_on_accent_primary() -> str:
663+
"""Get the icon color for accent_primary backgrounds.
664+
665+
This color should be used for icons displayed on selected items or other
666+
elements that use the accent_primary color as their background.
667+
668+
If not explicitly defined in the theme, falls back to text_on_accent_primary,
669+
which is auto-computed based on the accent_primary color's luminance.
670+
671+
Returns:
672+
The icon color as a hex string for use on accent_primary backgrounds.
673+
674+
Examples:
675+
>>> color = fxstyle.get_icon_on_accent_primary()
676+
>>> print(color) # "#ffffff" for dark theme with blue accent
677+
"""
678+
theme_colors = get_theme_colors()
679+
# Fallback chain: icon_on_accent_primary -> text_on_accent_primary -> computed
680+
if "icon_on_accent_primary" in theme_colors:
681+
return theme_colors["icon_on_accent_primary"]
682+
if "text_on_accent_primary" in theme_colors:
683+
return theme_colors["text_on_accent_primary"]
684+
accent = theme_colors.get("accent_primary", "#2196F3")
685+
return get_contrast_text_color(accent)
686+
687+
688+
def get_icon_on_accent_secondary() -> str:
689+
"""Get the icon color for accent_secondary backgrounds.
690+
691+
This color should be used for icons displayed on hovered items or other
692+
elements that use the accent_secondary color as their background.
693+
694+
If not explicitly defined in the theme, falls back to text_on_accent_secondary,
695+
which is auto-computed based on the accent_secondary color's luminance.
696+
697+
Returns:
698+
The icon color as a hex string for use on accent_secondary backgrounds.
699+
700+
Examples:
701+
>>> color = fxstyle.get_icon_on_accent_secondary()
702+
>>> print(color) # "#ffffff" for dark theme with blue accent
703+
"""
704+
theme_colors = get_theme_colors()
705+
# Fallback chain: icon_on_accent_secondary -> text_on_accent_secondary -> computed
706+
if "icon_on_accent_secondary" in theme_colors:
707+
return theme_colors["icon_on_accent_secondary"]
708+
if "text_on_accent_secondary" in theme_colors:
709+
return theme_colors["text_on_accent_secondary"]
710+
accent = theme_colors.get("accent_secondary", "#1976D2")
711+
return get_contrast_text_color(accent)
712+
713+
658714
###### Color Utility Functions
659715

660716

@@ -1133,6 +1189,15 @@ def load_stylesheet(
11331189
"text_on_accent_secondary", get_contrast_text_color(accent_secondary)
11341190
)
11351191

1192+
# Icon color for accent backgrounds: use theme value if defined,
1193+
# otherwise use the same value as text_on_accent (icons should match text)
1194+
icon_on_accent_primary = theme_data.get(
1195+
"icon_on_accent_primary", text_on_accent_primary
1196+
)
1197+
icon_on_accent_secondary = theme_data.get(
1198+
"icon_on_accent_secondary", text_on_accent_secondary
1199+
)
1200+
11361201
# Build replacement map for all placeholders
11371202
# Uses new semantic naming scheme
11381203
replace = {
@@ -1141,6 +1206,8 @@ def load_stylesheet(
11411206
"@accent_secondary": accent_secondary,
11421207
"@text_on_accent_primary": text_on_accent_primary,
11431208
"@text_on_accent_secondary": text_on_accent_secondary,
1209+
"@icon_on_accent_primary": icon_on_accent_primary,
1210+
"@icon_on_accent_secondary": icon_on_accent_secondary,
11441211
# Icon path
11451212
"~icons": str(_parent_directory / "icons" / icon_folder).replace(
11461213
os.sep, "/"

fxgui/fxwidgets/_delegates.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -681,10 +681,10 @@ def _draw_status_label(
681681
painter.setBrush(QBrush(label_color))
682682
painter.drawRoundedRect(label_rect, 2, 2)
683683

684-
# Calculate appropriate text color based on background
684+
# Calculate appropriate text color based on background luminance
685685
text_color = Qt.white if label_color.lightness() < 128 else Qt.black
686686

687-
# Draw the icon if provided
687+
# Draw the icon if provided (keep original colors for brand/DCC icons)
688688
content_x = label_x + label_padding
689689
if icon_size > 0:
690690
icon_y = label_y + (label_height - icon_size) // 2
@@ -1236,19 +1236,26 @@ def _draw_icon_and_text(
12361236
icon_margin = 6
12371237
text_x = option.rect.left() + icon_margin
12381238

1239+
# Determine icon and text colors based on selection state
1240+
# Note: Hover uses semi-transparent accent overlay, so icons stay normal
1241+
# Only selection uses full opaque accent background requiring contrast icons
1242+
if option.state & QStyle.State_Selected:
1243+
text_color = option.palette.highlightedText().color()
1244+
icon_color = QColor(fxstyle.get_icon_on_accent_primary())
1245+
else:
1246+
text_color = option.palette.text().color()
1247+
icon_color = QColor(fxstyle.get_icon_color())
1248+
12391249
if icon is not None and not icon.isNull():
12401250
icon_x = option.rect.left() + icon_margin
12411251
icon_y = option.rect.top() + (option.rect.height() - icon_size) // 2
12421252
icon_rect = QRect(icon_x, icon_y, icon_size, icon_size)
1243-
icon.paint(painter, icon_rect, Qt.AlignCenter)
1253+
# Colorize icon to match selection/hover state
1254+
pixmap = icon.pixmap(icon_size, icon_size)
1255+
colored_pixmap = fxicons.change_pixmap_color(pixmap, icon_color)
1256+
painter.drawPixmap(icon_rect, colored_pixmap)
12441257
text_x = icon_x + icon_size + icon_margin
12451258

1246-
# Set text color
1247-
if option.state & QStyle.State_Selected:
1248-
text_color = option.palette.highlightedText().color()
1249-
else:
1250-
text_color = option.palette.text().color()
1251-
12521259
title = index.data(Qt.DisplayRole) or ""
12531260

12541261
if description and description != "-":

fxgui/qss/style.qss

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,8 +1164,18 @@ QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
11641164

11651165

11661166
/* Item views inherit from QAbstractItemView above */
1167+
/* Use show-decoration-selected: 1 so the entire row gets consistent
1168+
highlighting. Branch uses @surface_sunken to match item background
1169+
(Qt ignores 'transparent' for branch selection/hover painting). */
11671170
QTreeView::branch {
1168-
background: transparent;
1171+
background: @surface_sunken;
1172+
}
1173+
1174+
/* Override branch background to stay neutral during selection/hover
1175+
since the delegate handles item painting, not the branch area */
1176+
QTreeView::branch:selected,
1177+
QTreeView::branch:!selected:hover {
1178+
background: @surface_sunken;
11691179
}
11701180

11711181
QTreeView::branch:has-siblings:!adjoins-item {
@@ -1206,7 +1216,7 @@ QTreeView::branch:open:has-children:has-siblings:hover {
12061216

12071217
QListView,
12081218
QTreeView {
1209-
show-decoration-selected: 0;
1219+
show-decoration-selected: 1;
12101220
}
12111221

12121222
QListView::item,

0 commit comments

Comments
 (0)