From cf9a88bcaf5ba90fb159355a999ac89c0dd7d879 Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Mon, 11 May 2026 00:22:39 -0700 Subject: [PATCH] PerKeyEditor: replace tool button labels with icons The Brush / Rect / Fill tool buttons in the per-key color editor now show icons instead of text labels, with the original labels retained as tooltips and accessible names. Icons are recolored at load time to match the active GTK theme's text foreground (light grey on dark themes, dark on light), by reading the button's style context and substituting currentColor in the SVG before loading the pixbuf. GTK's stock symbolic loader is bypassed because it only recolors fill palette stand-ins and ignores stroke="currentColor", which is what the source SVGs use. Adds MIT-licensed Tabler icons under share/solaar/icons/, see THIRD_PARTY.md. --- lib/solaar/ui/perkey/editor.py | 73 ++++++++++++++++++- share/solaar/icons/THIRD_PARTY.md | 41 +++++++++++ .../icons/solaar-tool-brush-symbolic.svg | 22 ++++++ .../icons/solaar-tool-bucket-symbolic.svg | 21 ++++++ .../icons/solaar-tool-rect-symbolic.svg | 21 ++++++ 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 share/solaar/icons/THIRD_PARTY.md create mode 100644 share/solaar/icons/solaar-tool-brush-symbolic.svg create mode 100644 share/solaar/icons/solaar-tool-bucket-symbolic.svg create mode 100644 share/solaar/icons/solaar-tool-rect-symbolic.svg diff --git a/lib/solaar/ui/perkey/editor.py b/lib/solaar/ui/perkey/editor.py index 9b0bab47..71a2e7c1 100644 --- a/lib/solaar/ui/perkey/editor.py +++ b/lib/solaar/ui/perkey/editor.py @@ -25,10 +25,13 @@ from __future__ import annotations import logging from enum import Enum +from pathlib import Path import gi gi.require_version("Gtk", "3.0") +from gi.repository import GdkPixbuf # NOQA: E402 +from gi.repository import Gio # NOQA: E402 from gi.repository import Gtk # NOQA: E402 from solaar.i18n import _ # NOQA: E402 @@ -57,6 +60,64 @@ _TOOL_LABELS = { _TOOL_TOOLTIPS = { "gradient": _("Drag to fade from previous color to active color"), } +_TOOL_ICON_NAMES = { + "single": "solaar-tool-brush-symbolic", + "rect": "solaar-tool-rect-symbolic", + "bucket": "solaar-tool-bucket-symbolic", +} +_TOOL_ICON_PIXEL_SIZE = 22 + +_icon_search_path_added = False + + +def _ensure_tool_icon_path() -> None: + """Register share/solaar/icons with the default GtkIconTheme so our + custom symbolic tool icons resolve by name. Idempotent.""" + global _icon_search_path_added + if _icon_search_path_added: + return + theme = Gtk.IconTheme.get_default() + existing = set(theme.get_search_path() or []) + # Source-tree path: lib/solaar/ui/perkey/editor.py -> parents[4] = repo root + candidates = [ + Path(__file__).resolve().parents[4] / "share" / "solaar" / "icons", + ] + for c in candidates: + if c.is_dir() and str(c) not in existing: + theme.append_search_path(str(c)) + _icon_search_path_added = True + + +def _tool_icon_image(icon_name: str, style_widget: Gtk.Widget) -> Gtk.Image | None: + """Load a Solaar tool icon and recolor it to match the given widget's + text foreground color, so the icons follow the active GTK theme + (light / dark / custom). Returns None if the icon can't be loaded. + + GTK's stock symbolic loader (`load_symbolic_for_context`) only recolors + specific palette stand-ins (e.g. fill="#bebebe"); it ignores + `stroke="currentColor"`. We bypass it and substitute currentColor + ourselves so any SVG using the currentColor convention works. + """ + _ensure_tool_icon_path() + theme = Gtk.IconTheme.get_default() + icon_info = theme.lookup_icon(icon_name, _TOOL_ICON_PIXEL_SIZE, Gtk.IconLookupFlags.FORCE_SIZE) + if icon_info is None: + return None + path = icon_info.get_filename() + if not path: + return None + fg = style_widget.get_style_context().get_color(Gtk.StateFlags.NORMAL) + color = f"#{int(fg.red * 255):02x}{int(fg.green * 255):02x}{int(fg.blue * 255):02x}" + try: + with open(path, "r", encoding="utf-8") as f: + svg = f.read() + svg = svg.replace("currentColor", color) + stream = Gio.MemoryInputStream.new_from_data(svg.encode("utf-8")) + pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(stream, _TOOL_ICON_PIXEL_SIZE, _TOOL_ICON_PIXEL_SIZE, True) + return Gtk.Image.new_from_pixbuf(pixbuf) + except Exception as e: + logger.debug("recolor failed for %s: %s", icon_name, e) + return None class PerKeyEditor(Gtk.Box): @@ -81,9 +142,17 @@ class PerKeyEditor(Gtk.Box): btn.set_tooltip_text(_TOOL_TOOLTIPS["gradient"]) else: label, tip = _TOOL_LABELS.get(name, (name, "")) - btn = Gtk.RadioButton.new_with_label_from_widget(first, label) + icon_name = _TOOL_ICON_NAMES.get(name) + btn = Gtk.RadioButton.new_from_widget(first) btn.set_mode(False) # render as toggle button rather than radio - btn.set_tooltip_text(tip) + image = _tool_icon_image(icon_name, btn) if icon_name else None + if image is not None: + btn.add(image) + btn.set_tooltip_text(tip or label) + btn.get_accessible().set_name(label) + else: + btn.set_label(label) + btn.set_tooltip_text(tip) btn.connect(GtkSignal.TOGGLED.value, self._on_tool_toggled, name) if first is None: first = btn diff --git a/share/solaar/icons/THIRD_PARTY.md b/share/solaar/icons/THIRD_PARTY.md new file mode 100644 index 00000000..9326f212 --- /dev/null +++ b/share/solaar/icons/THIRD_PARTY.md @@ -0,0 +1,41 @@ +# Third-party assets in this directory + +Solaar itself is licensed under GPL-2.0 (see `LICENSE.txt` at the repository +root). Some icons in this directory are sourced from third-party projects +under permissive licenses compatible with GPL-2.0. Their original license +notices are preserved as comments inside each SVG file. + +## Tabler Icons + +The following SVGs are derived from [Tabler Icons](https://tabler.io/icons), +copyright (c) 2020-2024 Paweł Kuna, distributed under the MIT License: + +- `solaar-tool-brush-symbolic.svg` — from `brush` +- `solaar-tool-rect-symbolic.svg` — from `paint` +- `solaar-tool-bucket-symbolic.svg` — from `bucket-droplet` + +### MIT License (Tabler Icons) + +``` +MIT License + +Copyright (c) 2020-2024 Paweł Kuna + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/share/solaar/icons/solaar-tool-brush-symbolic.svg b/share/solaar/icons/solaar-tool-brush-symbolic.svg new file mode 100644 index 00000000..e37803fc --- /dev/null +++ b/share/solaar/icons/solaar-tool-brush-symbolic.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/share/solaar/icons/solaar-tool-bucket-symbolic.svg b/share/solaar/icons/solaar-tool-bucket-symbolic.svg new file mode 100644 index 00000000..e73c5a83 --- /dev/null +++ b/share/solaar/icons/solaar-tool-bucket-symbolic.svg @@ -0,0 +1,21 @@ + + + + + + diff --git a/share/solaar/icons/solaar-tool-rect-symbolic.svg b/share/solaar/icons/solaar-tool-rect-symbolic.svg new file mode 100644 index 00000000..844ca447 --- /dev/null +++ b/share/solaar/icons/solaar-tool-rect-symbolic.svg @@ -0,0 +1,21 @@ + + + + + +