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.
This commit is contained in:
Ken Sanislo 2026-05-11 00:22:39 -07:00 committed by Peter F. Patel-Schneider
parent 9b627410b6
commit cf9a88bcaf
5 changed files with 176 additions and 2 deletions

View File

@ -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

View File

@ -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.
```

View File

@ -0,0 +1,22 @@
<!--
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: 2020-2024 Paweł Kuna
Source: Tabler Icons (https://tabler.io/icons) - "brush"
License: MIT (https://github.com/tabler/tabler-icons/blob/main/LICENSE)
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 21v-4a4 4 0 1 1 4 4h-4" />
<path d="M21 3a16 16 0 0 0 -12.8 10.2" />
<path d="M21 3a16 16 0 0 1 -10.2 12.8" />
<path d="M10.6 9a9 9 0 0 1 4.4 4.4" />
</svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@ -0,0 +1,21 @@
<!--
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: 2020-2024 Paweł Kuna
Source: Tabler Icons (https://tabler.io/icons) - "bucket-droplet"
License: MIT (https://github.com/tabler/tabler-icons/blob/main/LICENSE)
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 16l1.465 1.638a2 2 0 1 1 -3.015 .099l1.55 -1.737" />
<path d="M13.737 9.737c2.299 -2.3 3.23 -5.095 2.081 -6.245c-1.15 -1.15 -3.945 -.217 -6.244 2.082c-2.3 2.299 -3.231 5.095 -2.082 6.244c1.15 1.15 3.946 .218 6.245 -2.081" />
<path d="M7.492 11.818c.362 .362 .768 .676 1.208 .934l6.895 4.047c1.078 .557 2.255 -.075 3.692 -1.512c1.437 -1.437 2.07 -2.614 1.512 -3.692c-.372 -.718 -1.72 -3.017 -4.047 -6.895a6.015 6.015 0 0 0 -.934 -1.208" />
</svg>

After

Width:  |  Height:  |  Size: 887 B

View File

@ -0,0 +1,21 @@
<!--
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: 2020-2024 Paweł Kuna
Source: Tabler Icons (https://tabler.io/icons) - "paint"
License: MIT (https://github.com/tabler/tabler-icons/blob/main/LICENSE)
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 5a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2l0 -2" />
<path d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" />
<path d="M10 16a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1l0 -4" />
</svg>

After

Width:  |  Height:  |  Size: 668 B