Commit Graph

6 Commits

Author SHA1 Message Date
Ken Sanislo 3dc121e8b3 PerKey gradient swatch: align gradient endpoints to visible corners
Cairo linear-gradient endpoints were placed at the rounded rect's
geometric corners (3,3) and (21,21), but the *visible* corner pixel
of a rounded rect with radius 2 is the outermost point of the arc,
inset along the diagonal by r * (1 - 1/sqrt(2)) ≈ 0.586 units.

That meant t=0 and t=1 of the gradient landed in the cut-off corner
regions, and the rendered corners sampled at t≈0.033 / 0.967 — about
3.3% in from each endpoint, ~8 RGB units short of the true previous /
active colors (visible on saturated pairs like pure red → pure blue:
the "pure red" corner rendered as ~rgb(194, 10, 0)).

Shift the gradient endpoints inward by the arc inset so t=0 maps to
the visible TL corner pixel and t=1 to the visible BR pixel. The
gradient now spans the visually rendered area exactly; saturated
endpoints render flush.
2026-05-12 12:51:36 -04:00
Ken Sanislo a8e006c948 PerKey gradient swatch: Tabler "square" outline around the gradient
The gradient swatch on the gradient-tool button drew a flat
rectangular fill with a 1px translucent-black border. Visually it
sat as an odd-one-out next to the rounded-square Tabler outline
icons on the rest of the toolbar — a sharp-cornered patch flanked
by rounded-corner icons.

Render the gradient inside the same path Tabler "square" uses
(rounded rect from (3,3) to (21,21), corner radius 2, stroke 2 in
a 24x24 viewBox; cairo scale into the swatch's pixel size) and
stroke the outline in the GTK theme's foreground color. The
swatch now reads as one of the icon family — same outline style,
same line weight, same theme-following color — with the gradient
filling the inside.

Connect style-updated to queue_draw so the outline color tracks
runtime theme switches alongside the icon buttons.
2026-05-12 12:51:36 -04:00
Ken Sanislo dfa1cb7ca5 PerKey icons: read theme fg from style-updated, not Settings notify
Previously the editor and palette listened to Gtk.Settings
notify::gtk-theme-name (and notify::gtk-application-prefer-dark-theme)
to re-render their themed icons on theme switch. Two problems:

1. The initial icon load happened during widget construction, before
   the buttons were attached to the toolbar — so the style context
   resolved to a default (often white) foreground rather than the
   actual theme text color. Icons showed up white until the first
   theme-change event.

2. Settings notify fires *before* GTK's CSS engine re-resolves styles
   for the new theme. Reading the style context's foreground from
   that handler returned the previous theme's color, so toggling
   light <-> dark left both states settling on the same shade.

Move both responsibilities into a new attach_themed_icon helper in
_icons.py: it does the initial load, connects to the *button's own*
style-updated signal, and rebuilds the icon on each emission. That
signal fires *after* CSS resolution (both on first realize and on
runtime theme switches), so the foreground we read is always the
current one.

A per-button color-key guard skips the rebuild when the resolved
foreground hasn't changed, so unrelated style-updated emissions
(hover, focus, active) don't trigger needless re-renders.

The handler is connected to the button itself, so GTK cleans it up
when the button is destroyed; both editor.py and palette.py drop
their bespoke Gtk.Settings handler bookkeeping.
2026-05-12 12:51:36 -04:00
Ken Sanislo 661a186429 PerKey palette: replace hashed unset swatch with palette-off icon
The "Unset" toggle button next to the color picker used a custom
HashSwatch drawing area that mirrored the canvas's diagonal-hash
pattern for unset cells. The pattern was visually noisy at button
size and tied the button's appearance to the current zone base
color (the swatch had to be told the base color via
set_zone_base_color so it could redraw).

Use the Tabler "palette-off" symbolic icon instead. It reads as
"clear / no paint" at a glance, is independent of the zone base
color, and matches the icon style of the tool buttons on the other
end of the toolbar.

Extract the icon loader (themed_icon_image, ensure_icon_path) from
editor.py into a new private module _icons.py so palette.py can
reuse it; the new palette tracks its own Gtk.Settings notify signals
to re-render the icon on theme switches, and disconnects them via a
new Palette.shutdown() called from the editor's shutdown.

Adds MIT-licensed Tabler palette-off icon under share/solaar/icons/,
see THIRD_PARTY.md.
2026-05-12 12:51:36 -04:00
Ken Sanislo 1952e9ce98 Add regional keyboard layouts (ISO_QWERTY, QWERTZ, AZERTY, JIS) and fix copyright
Read HID++ feature 0x4540 KeyboardLayout to detect the device's country
code, then route the per-key painter to a matching regional layout.

Changes:

- lib/logitech_receiver/hidpp20.py: new get_keyboard_layout() returning the
  HID Usage Table country code from feature 0x4540's first response byte.
- lib/logitech_receiver/device.py: lazy device.keyboard_layout property,
  guarded by feature presence so devices without 0x4540 don't pay a query
  cost on access.
- lib/solaar/ui/perkey/control.py: thread the country code into the editor
  hint dict.
- lib/solaar/ui/perkey/layouts/_keyboard_base.py (new): factor out the
  function row, nav cluster, and numpad block as shared building blocks.
  Two main-block variants (ANSI with row 2 col 13 backslash, ISO without)
  cover all five regions. build_layout() applies per-zone label overrides
  on top of either main block.
- lib/solaar/ui/perkey/layouts/keyboard_ansi.py: refactored to use the
  builder; same LAYOUT_FULL/LAYOUT_TKL exports.
- lib/solaar/ui/perkey/layouts/keyboard_iso_qwerty.py (new): UK English
  ISO. Same shape as DE/FR/JIS but no label overrides.
- lib/solaar/ui/perkey/layouts/keyboard_iso_qwertz.py (new): DE/Swiss --
  Y/Z swap, Ü/Ö/Ä/ß placement.
- lib/solaar/ui/perkey/layouts/keyboard_iso_azerty.py (new): FR -- A↔Q,
  W↔Z, French digit-row symbols, M repositioning.
- lib/solaar/ui/perkey/layouts/keyboard_jis.py (new): JP -- @ / [ / :
  bracket-row relabels.
- lib/solaar/ui/perkey/layouts/__init__.py: country-code-aware matchers,
  five families × two sizes (full/TKL). Defaults to ANSI when 0x4540 is
  unsupported or returns an unknown code.

POUND, ISO_BACKSLASH, and the L-shape Enter top half (zone 46) are
intentionally omitted from the ISO layouts -- same coverage as OpenRGB.
ABNT2 (Brazilian) deferred until a confirmed Logitech BR RGB device shows
up; adding it later is one new layout file plus a country-code entry.

Also fix copyright headers on all new lib/solaar/ui/perkey/ files: the
files were created in 2026, not 2024 as the headers said.
2026-05-10 17:52:55 -04:00
Ken Sanislo d8422d78d1 Add per-key RGB color painter and replace MAP_CHOICE color validator
Replace the per-key dropdown UI (MapChoiceControl) with a Cairo-rendered
keyboard canvas where users can paint colors directly onto keys.

Editor (lib/solaar/ui/perkey/):
  - Cairo DrawingArea renders cells from a Layout dataclass; bound cells
    take their painted color, unset cells show a diagonal hash whose base
    color matches the device's rgb_zone_* setting.
  - Tools: brush, drag-rectangle, flood-fill (4-adjacent, Paint-style),
    and a directional gradient (line A->B projected across the matrix
    with cells past the endpoints clamped to the endpoint colors).
  - GradientSwatch is the single source of truth for the gradient's two
    colors; the canvas reads from it on each gradient stroke.
  - Palette: GTK ColorButton plus an unset toggle that paints the
    "no change" sentinel (-1).
  - PerKeyEditorDialog auto-sizes from the canvas's size_request, so a
    104-key keyboard opens wide and a 8-LED mouse opens compact.
  - Editor consumes only a narrow PerKeyColorSink protocol; never imports
    from lib/logitech_receiver, preserving the FE/BE seam.
  - Per-device palette state (active + previous color) persists via the
    existing persister under a _palette: prefixed key.

Layouts:
  - ANSI 104-key full-size and TKL keyboard layouts.
  - G502 X family mouse layout (zones 1-8 -> labels A-H).
  - Generic registry: register_layout(feature, matcher, layout). A
    _name_contains() helper builds case-insensitive substring matchers
    against device codename / name. Unknown devices fall back to a flat
    strip of all reported zones.

Validator (open value space):
  - New Range dataclass and MapRangeValidator extending Validator
    directly (kind=MAP_CHOICE for dispatch compatibility). Replaces the
    ChoicesMapValidator on PerKeyLighting -- the named-color universe
    (COLORSPLUS) was rejecting any picker color outside its ~20 entries.
    Other MAP_CHOICE settings are untouched.

Integration:
  - Setting base gains an editor_class string attribute. config_panel's
    _create_sbox resolves it via importlib before the kind dispatch, so
    PerKeyLighting routes to the new editor without a new Kind value.
  - CLI gains a hex/dec parser for open-value MAP_CHOICE settings:
      solaar config <dev> per-key-lighting A 0xFF00FF
  - Diversion rule editor skips Range-valued MAP_CHOICE settings'
    value-selector instead of crashing on the open value space.
  - pycairo declared in install_requires; transitively present on most
    systems but now explicit for pip-from-source installs.

Tests in test_setting_templates.py updated for the new validator.
2026-05-10 17:52:55 -04:00