Draw action glyphs in the theme foreground colour (and recolour live)#780
Conversation
|
While this fixes the issue, it does so in a way that breaks the second the theme itself changes. The core issue is that the Python code has no idea of the foreground/background colors used by the UI. The original code used a value that worked but then I forgot to do the step that is required to make it work reliably, i.e. expose the required information to Python. I'll try to put together the code that provides the required information to python such that this can be fixed in a way that holds up should the main theme colors change. Getting data once on startup is easy, the tricky bit atm is to ensure it's maintained properly since one of your next changes is going to remove to need to reload for light/dark switch, thus this code needs to deal with that too. |
|
You should be able to use JoystickGremlin/gremlin/ui/util.py Line 458 in 5ab8493 |
3344157 to
ec85087
Compare
|
Thanks for #782 — reworked this to use Testing it live surfaced the maintenance issue you predicted. Refreshing the cached colours on For the live switch itself: after refreshing the colours and clearing the cached glyph images, I emit a small Tab-label legibility from the same dark-mode pass is a separate small PR. |
| @staticmethod | ||
| def _glyph_color() -> QtGui.QColor: | ||
| """Returns the pen color used to draw action glyphs. | ||
|
|
||
| Uses the theme's foreground color so glyphs stay legible regardless of | ||
| the active theme, rather than assuming a fixed black-on-light layout. | ||
|
|
||
| Returns: | ||
| The current theme's foreground color | ||
| """ | ||
| return ColorInformation().foreground |
There was a problem hiding this comment.
This is an unnecessary indirection that makes it actually harder to understand what the intent is. Just using ColorInformation().foreground in the _render function is good enough. Also the description is a bit lengthy in the sense that it describes the "origin" for the existence. That's fine for a MR etc. but not very useful for a function's docstring (in many cases at least).
| def invalidate_cache(self) -> None: | ||
| """Discards all cached images. | ||
|
|
||
| Rendered glyphs bake in the theme's foreground color, so cached images | ||
| become stale when the theme changes (e.g. a live dark-mode toggle). | ||
| Clearing the cache forces them to be re-rendered with the new color. | ||
| """ | ||
| with self._lock: | ||
| self._cache.clear() |
There was a problem hiding this comment.
If we only clear the cache in one place, just adding the code there is sufficient and not need to add a function that does exactly what the name is and the function calls are. Similar comment for the docstring as the _glyph_color method.
| # Refresh on foregroundChanged rather than isDarkThemeChanged: the dark | ||
| # flag flips before the colour bindings settle, so reading the colours in | ||
| # the isDarkThemeChanged handler captures the outgoing theme's values. | ||
| # foregroundChanged fires once the new colour is in place. | ||
| self.color_information_object.foregroundChanged.connect( | ||
| self._on_theme_colors_changed | ||
| ) |
There was a problem hiding this comment.
As there is no guarantee that the order stays the same and is thus brittle it would be better to trigger on any of the color information to change and coalesce the almost instantaneous signals to be handled only once.
Claude thinks this should work (and claims it tried the code but you know ... :-) )
from PySide6.QtCore import QTimer
_single_shot_timer = QTimer()
_single_shot_timer.setSingleShot(True
_single_shot_timer.setInterval(0)
_single_shot_timer.timeout.connect(some_function)
for sig in (probe.foregroundChanged, probe.backgroundChanged, probe.accentChanged):
sig.connect(_single_shot_timer.start)
The reason this should work is that setInterval(0) would trigger once all events in the queue right now are done, i.e. it fires once all things queued up have been processed, which means all colors should be up to date and we're not relying on any ordering from Qt code.
| self.action_image_provider = \ | ||
| gremlin.ui.action_image_generator.ActionSummaryImageProvider() | ||
| self.engine.addImageProvider("action_summary", action_image_provider) | ||
| self.engine.addImageProvider("action_summary", self.action_image_provider) |
There was a problem hiding this comment.
What is that change fixing? The engine will take ownership of the provider, which should prevent Python's GC from deleting the object and so far this hasn't caused problems.
Sets the provider to use for images requested via the image: url scheme, with host providerId. The QQmlEngine takes ownership of provider.
See: https://doc.qt.io/qt-6/qqmlengine.html#addImageProvider
| // Bumped on a theme change to force the action-summary image to re-request | ||
| // so its glyphs are redrawn in the new theme colour. | ||
| property int _themeRevision: 0 |
There was a problem hiding this comment.
Why is this needed when the cache of the image provider is cleared? After clearing the cache the image ought to be regenerated. If that's not the case then something else is wrong and all this does is mask that.
|
Adopted the QTimer approach — a zero-interval single-shot restarted on Also from the review:
|
ec85087 to
221f625
Compare
|
|
||
| function onThemeColorsChanged() { | ||
| _control._themeRevision += 1 | ||
| } |
There was a problem hiding this comment.
If this works then not because it should. That signal would be the one from the signal side which this connection is not making connections with. So This should be at least a Connections against that but it also would increment the revision repeatedly, once for however many InputButtons happen to kind of exist which isn't desired. Also, as mentioned above this shouldn't be needed as it tries to solve something the cache clearing already should take care of.
| # QML may append a "?<token>" suffix to force a re-request when the | ||
| # theme changes; it is not part of the action string. | ||
| image_id = image_id.split("?", 1)[0] |
There was a problem hiding this comment.
As mentioned down below this should not be required to be able to force a refresh.
|
|
||
| # Emitted once the theme's colours have been applied and any colour-derived | ||
| # caches refreshed, so UI elements that bake in those colours can redraw. | ||
| themeColorsChanged = QtCore.Signal() |
There was a problem hiding this comment.
If we add a new signal instead of reusing something like reloadUi which may be the better on in this case. Then this should also drive everything. That means that the image provider connects the cache clearing to this. Qt has the signal/slot mechanism to avoid a lot of annoying calling of methods to get something done in response to something happening.
|
Overall the changes make sense just what may look like a lot of changes based around of how Qt does things (or allows you to do) and then follow on things of what "should" happen but may not which in my view would point to something being broken that needs fixing rather than adding a workaround. |
221f625 to
20d2bbc
Compare
|
A couple of these are on an earlier revision of the diff, so quick status first: the cache clear is already gone — I dropped On why clearing the provider cache isn't enough by itself: it doesn't make the on-screen On the per-button increment — agreed, not good. I've moved it to a single shared counter ( If there's a provider-reload path you'd prefer that avoids the URL token, I'm glad to switch — I just couldn't find one that re-fetches without a source change. |
| # rather than depend on one of them being last, restart a zero-interval | ||
| # single-shot timer on any of them. It fires once the event queue has | ||
| # drained, by which point every colour has settled. | ||
| self._theme_refresh_timer = QtCore.QTimer() |
There was a problem hiding this comment.
I'd go with something like this. Worth explaining what the behavior overall is but I usually don't explain what API calls do as that gets very verbose very quickly.
"Coalesce the color change signals into a single update call that triggers only once the current event queue is empty."
|
I looked around a bit and tried some things, there is the option of using If we go with the "have to mangle image string" approach I don't think this should live in signals. Those are really just stateless things that can be used to propagate things around that otherwise are too painful or global. I don't really want to add state etc. to them. Especially for this case I think the |
The action-glyph painter hard-coded black, which is invisible against a dark theme. Draw with the theme's foreground colour via ColorInformation (WhiteMagic#782) so the glyphs stay legible under any theme. Two supporting changes keep this correct when the theme changes at runtime: - Refresh the cached theme colours when the ColorInformation values change. A zero-interval single-shot timer, restarted on any of the colour change signals, coalesces them and fires once the event queue has drained, so every colour has settled and there is no reliance on Qt's emission order. - After refreshing, bump a themeRevision counter on UIState. The action- summary image id includes it, so a theme change alters the URL and QML re-requests the image (a provider cache clear alone would not: an on-screen Image is only re-fetched when its source changes or its delegate is rebuilt). The re-requested id misses the cache and renders in the new colour. UIState is the engine's state-tracking context property, so the signal helper stays stateless.
20d2bbc to
1a14488
Compare
|
Makes sense — Re Also trimmed that comment down to your wording. |
|
Thanks, looks good to me with all those changes. |
Reworked per your note on the original #778 split. Action glyphs now draw with the theme's foreground colour from
ColorInformation(#782) instead of a hard-coded colour, and recolour live when the theme changes.Includes a fix to how #782's colours are consumed: refreshing on
foregroundChangedrather thanisDarkThemeChanged(the dark flag flips before the colour bindings settle). Details in a comment below.