From 1a14488ccb8ff136f69e4611f0a8f56311a7ed5e Mon Sep 17 00:00:00 2001 From: Subliminal <151383070+SubliminalsTV@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:47:57 -0500 Subject: [PATCH] Draw action glyphs in the theme foreground colour and recolour them live The action-glyph painter hard-coded black, which is invisible against a dark theme. Draw with the theme's foreground colour via ColorInformation (#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. --- gremlin/ui/action_image_generator.py | 9 +++++++-- gremlin/ui/backend.py | 16 ++++++++++++++++ joystick_gremlin.py | 23 +++++++++++++++++++---- qml/InputButton.qml | 1 + 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/gremlin/ui/action_image_generator.py b/gremlin/ui/action_image_generator.py index a575926eb..fdec8d5c9 100644 --- a/gremlin/ui/action_image_generator.py +++ b/gremlin/ui/action_image_generator.py @@ -14,6 +14,8 @@ QtQuick, ) +from gremlin.ui.util import ColorInformation + class ActionSummaryImageProvider(QtQuick.QQuickImageProvider): @@ -76,7 +78,10 @@ def requestImage( self._cache.move_to_end(image_id) return self._cache[image_id] - image = self._render(image_id) + # A "?" suffix, bumped on a theme change, keeps the id (and thus + # the cache entry) distinct per theme; it is not part of the action + # string, so strip it before rendering. + image = self._render(image_id.split("?", 1)[0]) with self._lock: if len(self._cache) >= self._max_cache_size: @@ -101,7 +106,7 @@ def _render(self, action_string: str) -> QtGui.QImage: try: painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing) - painter.setPen(QtGui.QColor(0, 0, 0)) + painter.setPen(ColorInformation().foreground) x_offset = 0 for token in tokens: diff --git a/gremlin/ui/backend.py b/gremlin/ui/backend.py index c17877522..0d9cd1cc1 100644 --- a/gremlin/ui/backend.py +++ b/gremlin/ui/backend.py @@ -73,6 +73,7 @@ class UIState(QtCore.QObject): inputChanged = Signal() modeChanged = Signal() tabChanged = Signal() + themeRevisionChanged = Signal() selectIndex = Signal(int) def __init__(self, parent: ta.OQO=None) -> None: @@ -82,6 +83,7 @@ def __init__(self, parent: ta.OQO=None) -> None: self._current_input = {} self._current_mode = "Default" self._current_tab = "physical" + self._theme_revision = 0 event_handler.EventListener().device_change_event.connect( self._device_change @@ -135,6 +137,11 @@ def setCurrentTab(self, tab: str) -> None: self._current_tab = tab self.tabChanged.emit() + @Slot() + def bumpThemeRevision(self) -> None: + self._theme_revision += 1 + self.themeRevisionChanged.emit() + @Property(str, notify=deviceChanged) def currentDevice(self) -> str: return str(self._current_device).upper() @@ -161,6 +168,15 @@ def currentMode(self) -> str: def currentTab(self) -> str: return self._current_tab + @Property(int, notify=themeRevisionChanged) + def themeRevision(self) -> int: + """Counter bumped whenever the theme colours change. + + Image sources that bake in a theme colour append it so a theme change + alters the URL and the image is re-requested in the new colour. + """ + return self._theme_revision + def __str__(self) -> str: cur_input = self._current_input.get( self._current_device, diff --git a/joystick_gremlin.py b/joystick_gremlin.py index 63a554637..d2bf45500 100644 --- a/joystick_gremlin.py +++ b/joystick_gremlin.py @@ -357,15 +357,30 @@ def __init__(self, argv: List[str]) -> None: "Failed to find color information object in QML." ) gremlin.ui.util.ColorInformation().update_colors(self.color_information_object) - self.color_information_object.isDarkThemeChanged.connect( - lambda: gremlin.ui.util.ColorInformation().update_colors( - self.color_information_object) - ) + # Coalesce the colour change signals into a single update call that + # triggers only once the current event queue is empty. + self._theme_refresh_timer = QtCore.QTimer() + self._theme_refresh_timer.setSingleShot(True) + self._theme_refresh_timer.setInterval(0) + self._theme_refresh_timer.timeout.connect(self._on_theme_colors_changed) + for changed in ( + self.color_information_object.foregroundChanged, + self.color_information_object.backgroundChanged, + self.color_information_object.accentChanged, + ): + changed.connect(self._theme_refresh_timer.start) # Run UI. self.syslog.info("Gremlin UI launching") self.aboutToQuit.connect(shutdown_cleanup) + def _on_theme_colors_changed(self) -> None: + """Refreshes the cached theme colours and asks QML to redraw.""" + gremlin.ui.util.ColorInformation().update_colors( + self.color_information_object + ) + self.backend.ui_state.bumpThemeRevision() + def process_cmd_args(self, args: argparse.Namespace) -> None: # Load the profile specified by the user on the command line, otherwise # attempt to load the previously loaded profile diff --git a/qml/InputButton.qml b/qml/InputButton.qml index 02dd46932..c19f4301d 100644 --- a/qml/InputButton.qml +++ b/qml/InputButton.qml @@ -153,6 +153,7 @@ Button { sourceComponent: Image { source: "image://action_summary/" + actionSequenceDescriptor + + "?r=" + uiState.themeRevision asynchronous: false cache: false clip: true