Skip to content

feat: support icon color - followup#277

Open
brisvag wants to merge 25 commits into
pyapp-kit:mainfrom
brisvag:icon-color
Open

feat: support icon color - followup#277
brisvag wants to merge 25 commits into
pyapp-kit:mainfrom
brisvag:icon-color

Conversation

@brisvag
Copy link
Copy Markdown

@brisvag brisvag commented Apr 3, 2026

Trying to pick up #130!

I fixed conflicts and brought it up to date. @tlambert03 was there something specific with that PR that was incomplete or you wanted to finish?

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 88.52459% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.43%. Comparing base (82f70b3) to head (39d2e1c).

Files with missing lines Patch % Lines
src/app_model/_app.py 58.33% 5 Missing ⚠️
src/app_model/types/_icon.py 83.33% 2 Missing ⚠️

❌ Your patch check has failed because the patch coverage (88.52%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #277      +/-   ##
==========================================
- Coverage   99.78%   99.43%   -0.36%     
==========================================
  Files          31       31              
  Lines        1879     1931      +52     
==========================================
+ Hits         1875     1920      +45     
- Misses          4       11       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tlambert03
Copy link
Copy Markdown
Member

@brisvag, I think the main reason that PR stalled is because of the problem with theme changes. as mentioned in the first comment:

note that svg-colored icons don't currently change if the parent style sheet changes. This is just a generally harder thing to solve (and one of the few benefits of font-icons). However, we could, as magicgui does, attach QPallete change events here and auto-change icon colors in a future PR.

so, currently here:

  app = Application("myapp")
  app.theme_mode = "dark"  # or let it auto-detect

  Action(
      id="my.action",
      title="Do Thing",
      icon={"dark": "mdi:some-icon", "color_dark": "#FFFFFF", "color_light": "#000000"},
      ...
  )

The icon color is resolved once — at the moment the QAction or QMenuSubMenu is constructed. The resulting QIcon is a static pixmap/SVG baked with whatever color was chosen at creation time. If the user later, changes app.theme_mode from "dark" to "light", or wwitches their OS/Qt theme (causing the QPalette to change), nothing re-runs to_qicon(). The icons stay the old color. There's no signal, no QPalette change event listener, no theme_mode change signal — nothing triggers a re-render.

so, it feels like a partially implemented thing that will pretty quickly have a bug report or feature request

@brisvag
Copy link
Copy Markdown
Author

brisvag commented Apr 20, 2026

Does something like this make sense?

One thing I noticed when testing this out with demo/model_app.py is that the "disabled" color seems to be hardcoded. Not sure if this is happening qt the qt level or if it's something that can/should be controlled by superqt when creating the icon.

@tlambert03
Copy link
Copy Markdown
Member

Does something like this make sense?

if you try it out and it works for you, it seems fine to me.

that the "disabled" color seems to be hardcoded. Not sure if this is happening qt the qt level or if it's something that can/should be controlled by superqt when creating the icon.

i need a bit more context: what did you observe and how did it differ from what you expected?

Comment thread src/app_model/backends/qt/_qaction.py Outdated
@brisvag
Copy link
Copy Markdown
Author

brisvag commented Apr 22, 2026

Ok so I pushed some changes that should make this work as intended. I also updated temporarily the demo so you test it out, with 2 new buttons: the first changes "system theme" by changing the qpalette. The second changes the model's theme between light and dark manually.

When you swap theme, you'll see that while the icon theme color overrides (blue and red) apply correctly to the two new buttons, they don't affect the scissors icon as long as it's disabled: that grey-out colopr is hardcoded somewhere else. IMO it should be a desaturated version of the provided color?

@brisvag
Copy link
Copy Markdown
Author

brisvag commented May 13, 2026

@tlambert03 ping in case this slipped through the cracks!

@tlambert03
Copy link
Copy Markdown
Member

I'll have a look today... but can you make sure that things like pre-commit/pyright are green? That will definitely prevent merge

@brisvag
Copy link
Copy Markdown
Author

brisvag commented May 18, 2026

I was mainly asking for feedback on my last comment above, before I lock in in terms of implementation and solve the remaining tests/issues. Some of the changes I made (mainly the example) are temporary and it's just to make it easier for you to see the issue :)

@tlambert03
Copy link
Copy Markdown
Member

got it thanks! lemme give you a better review soon

Copy link
Copy Markdown
Member

@tlambert03 tlambert03 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly good to me! A couple comments inline. Main issues are the demo code (useful, but currently incomplete in a confusing way)... and the per-action eventFilter causing a potential performance problem

Comment thread demo/model_app.py
Comment on lines +280 to +303
types.Action(
id="switch_theme",
icon={
"dark": "fa6-solid:circle-half-stroke",
"color_dark": "#ff0000",
"color_light": "#0000ff",
},
title="Switch dark/light theme",
status_tip="Switch between dark and light theme.",
menus=[{"id": MenuId.HELP}],
callback=MainWindow.switch_theme,
),
types.Action(
id="switch_theme_model",
icon={
"dark": "fa6-solid:circle-half-stroke",
"color_dark": "#ff0000",
"color_light": "#0000ff",
},
title="Switch dark/light theme",
status_tip="Switch between dark and light theme.",
menus=[{"id": MenuId.HELP}],
callback=MainWindow.switch_theme_model,
),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these two actions are very useful in the demo... but they seem to be copy/paste incomplete. They have the same icon/tooltip/title, etc... but do different things?
please also add docstrings to the two methods switch_theme[_model], so it's actually clear what the methods are supposed to be demoing.

Comment thread demo/model_app.py
Comment thread demo/model_app.py
Comment thread demo/model_app.py
Comment thread src/app_model/backends/qt/_qaction.py
Comment thread src/app_model/backends/qt/_qaction.py
)
if submenu.icon:
self.setIcon(to_qicon(submenu.icon))
self.setIcon(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to react to theme_mode_changed like the QCommandAction did?

Comment on lines +76 to +77
color_dark: str | None
color_light: str | None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these may have been the names I used when I took a first stab at implementing this... but I'm realizing now it's a footgun. Do you think that color_dark sounds like "the color of the icon when it should be dark" (e.g. for the light theme) ... rather than "the color of the icon when the theme is dark". If so, maybe let's make it dark_theme_color and light_theme_color? (more verbose, but less confusing)

self.setIcon(to_qicon(command_rule.icon))
self.setIconVisibleInMenu(command_rule.icon_visible_in_menu)
self._update_icon_theme()
QApplication.instance().installEventFilter(self)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for an app with hundreds of actions, I fear the costs of this per-action eventFilter: every QCommandRuleAction installs its own event filter on the global QApplication. With N actions, every Qt event delivered to QApplication (mouse moves, etc.) traverses N filters... And events are fired all the time.
Can we consider one shared event filter at the Application level that triggers app.theme_mode_changed instead?



LIGHT_COLOR = "#BCB4B4"
DARK_COLOR = "#6B6565"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just pointing out (for myself). This is an observable change for all users, even if they aren't touching the theme stuff. The old default icon color was black, and will now be gray. Probably not a big deal, but could perhaps be mentioned in the main PR comment for visibility

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants