Commit Graph

4 Commits

Author SHA1 Message Date
Ken Sanislo 3df2a30f30 Add RGB lighting persistence and software LED power management for G515
Software-managed LED persistence and power management for devices that
expose RGBEffects (0x8071) — primarily G515 LIGHTSPEED TKL, but the same
infrastructure works on any 0x8071 device that supports SW takeover.

Core mechanism: RGBControl toggle drives a Set SWControl(mode=3, flags)
handshake. While SW control is held, the host owns the LED pipeline: zone
effects, per-key paint, idle/sleep transitions, and the NvConfig boot/exit
animations. On release, the firmware resumes its onboard profile.

Major pieces:

- rgb_power.py — new module hosting the software RGB power manager:
  ACTIVE / DIMMING / IDLE / SLEEPING state machine driven by firmware
  onUserActivity events, smooth 5-second dim ramp (zone or per-key), idle
  Static color snap, software sleep timer, wake handler that re-pushes
  saved state. Includes the cleanup hook that runs on device close (and,
  optionally, fires the cap 0x0040 shutdown trigger).

- RGBControl (settings_templates) — switch-style render via BooleanValidator
  (true_value=3 / false_value=0) plus a full _claim_sw_control /
  _release_sw_control pair: profile-management mode, SetSWControl, per-key
  flag reset, manager start, cleanup registration, and a post-claim
  repaint pass so the device immediately reflects Solaar's saved zone +
  per-key state.

- RGBEffectSetting — zone-effect Setting subclass for 0x8071. Handles
  per-key/zone coexistence: per-key paint dominates only when zone is
  Static and the user has explicitly opted in via the lock icon; under
  animations or before opt-in, the zone wire push is the visible layer.

- RGBIdleEffect / RGBIdleTimeout / RGBSleepTimeout — Solaar-managed idle
  behavior. Choice list: "No change" → Dim → Static (snap to color) →
  device-specific animations. Static idle substitutes the idle color for
  unset per-key cells via effective_zone_base_color's state-aware lookup.

- RgbStartupAnimation / RgbShutdownAnimation — toggle-and-color rows for
  RGBEffects NvConfig caps 0x0001 and 0x0040, exposed only on devices
  that answer the probe. Shutdown trigger fires SetRgbPowerMode(0) at
  cleanup time so the firmware plays the configured animation on exit.

- PerKeyLighting — per-key painter improvements: explicit-opt-in
  dominance over zone effects, BUSY retry, FrameEnd suppression on
  per-cell failure, single-shot prep sequence (SetEffectByIndex into the
  out-of-range slot) on mice with firmware effect cards.

- device_quirks.py — small per-model quirks table keyed by device.modelId
  (stable across USB/BLE/wireless). Currently used to mark RGBEffects
  NvConfig color slots as inert on devices where the firmware accepts
  but ignores the bytes (G502 X PLUS startup colors).

- config_panel.py — HeteroKeyControl gains a TOGGLE field kind that
  renders as a Gtk.Switch (used by the boot-effect rows). Visual gate
  greys out RGB settings whose prerequisites aren't met: rgb_zone_* /
  rgb_idle_* / rgb_sleep_timeout require LED Control = Solaar; per-key
  additionally requires every zone effect to be Static. Visual-only —
  doesn't touch the persister's user lock-icon state.

- tests/test_rgb_power.py — coverage for the power manager state
  machine, dim ramp, idle effects, wake path, and per-key/zone
  coexistence.

Closes pwr-Solaar/Solaar#3149.
2026-05-13 19:03:44 -04:00
Ken Sanislo b594c7292b PerKey dialog: one window per device, keyed by firmware unit-id
The editor was a process-wide singleton: opening it on a different
device replaced the content in the existing window. A user with two
perkey-capable devices (e.g. a G915 keyboard and a G502 mouse) had to
context-switch between them, losing any in-progress edits on the
device they switched away from.

Replace the singleton with a `_dialogs` dict in dialog.py keyed by a
stable per-device identifier. control.py builds the key from
`device.unitId` first — read from the device firmware via the
DeviceInformation feature, the same regardless of whether the device
is currently on a receiver or plugged directly via USB — so the same
physical device on different transports shares one dialog instead of
opening two windows. Falls back to `serial`, `hid_serial`, `codename`,
and finally `id(sink)` for the pathological case where nothing else
identifies the device.

If the dialog is already open for a given device and `present()` is
called with the same sink instance, the window is just raised — no
flicker, in-progress interaction state preserved. A different sink
under the same key (transport change for the same physical device)
rebuilds the window content under the existing dialog slot, so the
window position is preserved across transport switches.

Closing a window via the WM tears down only that dialog and pops it
from the registry; other open editors stay up. As a side cleanup the
unused `inset` ScrolledWindow shadow already added in editor.py and
the per-device sizing logic in dialog.py remain in place.
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