Solaar/lib/solaar/ui/perkey/layouts/_keyboard_base.py

237 lines
11 KiB
Python

## Copyright (C) 2026 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Shared building blocks for regional keyboard layouts.
Each region (ANSI, ISO_QWERTY, ISO_QWERTZ, ISO_AZERTY, JIS) shares the function
row, nav-cluster, and numpad blocks; only the main alpha block differs (ANSI
includes the row 2 col 13 backslash, ISO doesn't). Regional label overrides on
top of either main block produce the final layout.
Cell positions and groupings adapted from OpenRGB's KeyboardLayoutManager.
Zone IDs are firmware values reported by Logitech HID++ feature 0x8081
(PER_KEY_LIGHTING_V2).
"""
from __future__ import annotations
from ..layout import Cell
from ..layout import Layout
# --- Function row: ESC + F1..F12 (shared across all regions).
FN_ROW: tuple[Cell, ...] = (
Cell(zone_id=38, row=0, col=0, group="fn_row", label="Esc"),
Cell(zone_id=55, row=0, col=2, group="fn_row", label="F1"),
Cell(zone_id=56, row=0, col=3, group="fn_row", label="F2"),
Cell(zone_id=57, row=0, col=4, group="fn_row", label="F3"),
Cell(zone_id=58, row=0, col=5, group="fn_row", label="F4"),
Cell(zone_id=59, row=0, col=6, group="fn_row", label="F5"),
Cell(zone_id=60, row=0, col=7, group="fn_row", label="F6"),
Cell(zone_id=61, row=0, col=8, group="fn_row", label="F7"),
Cell(zone_id=62, row=0, col=9, group="fn_row", label="F8"),
Cell(zone_id=63, row=0, col=10, group="fn_row", label="F9"),
Cell(zone_id=64, row=0, col=11, group="fn_row", label="F10"),
Cell(zone_id=65, row=0, col=12, group="fn_row", label="F11"),
Cell(zone_id=66, row=0, col=13, group="fn_row", label="F12"),
)
# --- Nav cluster + arrows (shared).
EXTRAS: tuple[Cell, ...] = (
Cell(zone_id=67, row=0, col=14, group="extras", label="PrtSc"),
Cell(zone_id=68, row=0, col=15, group="extras", label="ScrLk"),
Cell(zone_id=69, row=0, col=16, group="extras", label="Pause"),
Cell(zone_id=70, row=1, col=14, group="extras", label="Ins"),
Cell(zone_id=71, row=1, col=15, group="extras", label="Home"),
Cell(zone_id=72, row=1, col=16, group="extras", label="PgUp"),
Cell(zone_id=73, row=2, col=14, group="extras", label="Del"),
Cell(zone_id=74, row=2, col=15, group="extras", label="End"),
Cell(zone_id=75, row=2, col=16, group="extras", label="PgDn"),
Cell(zone_id=79, row=4, col=15, group="extras", label=""),
Cell(zone_id=77, row=5, col=14, group="extras", label=""),
Cell(zone_id=78, row=5, col=15, group="extras", label=""),
Cell(zone_id=76, row=5, col=16, group="extras", label=""),
)
# --- Numpad block (only on full-size keyboards).
NUMPAD: tuple[Cell, ...] = (
Cell(zone_id=80, row=1, col=17, group="numpad", label="Num"),
Cell(zone_id=81, row=1, col=18, group="numpad", label="/"),
Cell(zone_id=82, row=1, col=19, group="numpad", label="*"),
Cell(zone_id=83, row=1, col=20, group="numpad", label="-"),
Cell(zone_id=92, row=2, col=17, group="numpad", label="7"),
Cell(zone_id=93, row=2, col=18, group="numpad", label="8"),
Cell(zone_id=94, row=2, col=19, group="numpad", label="9"),
Cell(zone_id=84, row=2, col=20, height=2.0, group="numpad", label="+"),
Cell(zone_id=89, row=3, col=17, group="numpad", label="4"),
Cell(zone_id=90, row=3, col=18, group="numpad", label="5"),
Cell(zone_id=91, row=3, col=19, group="numpad", label="6"),
Cell(zone_id=86, row=4, col=17, group="numpad", label="1"),
Cell(zone_id=87, row=4, col=18, group="numpad", label="2"),
Cell(zone_id=88, row=4, col=19, group="numpad", label="3"),
Cell(zone_id=85, row=4, col=20, height=2.0, group="numpad", label="Enter"),
Cell(zone_id=95, row=5, col=17, width=2.0, group="numpad", label="0"),
Cell(zone_id=96, row=5, col=19, group="numpad", label="."),
)
# --- Main alpha block, ANSI (104-key). Includes row 2 col 13 backslash and
# omits POUND (row 3 col 12) + ISO_BACKSLASH (row 4 col 1).
MAIN_ANSI: tuple[Cell, ...] = (
# Row 1: backtick + numbers + minus/equals + backspace
Cell(zone_id=50, row=1, col=0, group="main", label="`"),
Cell(zone_id=27, row=1, col=1, group="main", label="1"),
Cell(zone_id=28, row=1, col=2, group="main", label="2"),
Cell(zone_id=29, row=1, col=3, group="main", label="3"),
Cell(zone_id=30, row=1, col=4, group="main", label="4"),
Cell(zone_id=31, row=1, col=5, group="main", label="5"),
Cell(zone_id=32, row=1, col=6, group="main", label="6"),
Cell(zone_id=33, row=1, col=7, group="main", label="7"),
Cell(zone_id=34, row=1, col=8, group="main", label="8"),
Cell(zone_id=35, row=1, col=9, group="main", label="9"),
Cell(zone_id=36, row=1, col=10, group="main", label="0"),
Cell(zone_id=42, row=1, col=11, group="main", label="-"),
Cell(zone_id=43, row=1, col=12, group="main", label="="),
Cell(zone_id=39, row=1, col=13, group="main", label="Bksp"),
# Row 2: tab + qwerty + brackets + backslash
Cell(zone_id=40, row=2, col=0, group="main", label="Tab"),
Cell(zone_id=17, row=2, col=1, group="main", label="Q"),
Cell(zone_id=23, row=2, col=2, group="main", label="W"),
Cell(zone_id=5, row=2, col=3, group="main", label="E"),
Cell(zone_id=18, row=2, col=4, group="main", label="R"),
Cell(zone_id=20, row=2, col=5, group="main", label="T"),
Cell(zone_id=25, row=2, col=6, group="main", label="Y"),
Cell(zone_id=21, row=2, col=7, group="main", label="U"),
Cell(zone_id=9, row=2, col=8, group="main", label="I"),
Cell(zone_id=15, row=2, col=9, group="main", label="O"),
Cell(zone_id=16, row=2, col=10, group="main", label="P"),
Cell(zone_id=44, row=2, col=11, group="main", label="["),
Cell(zone_id=45, row=2, col=12, group="main", label="]"),
Cell(zone_id=46, row=2, col=13, group="main", label="\\"),
# Row 3: caps + asdf-row + semi/quote + enter
Cell(zone_id=54, row=3, col=0, group="main", label="Caps"),
Cell(zone_id=1, row=3, col=1, group="main", label="A"),
Cell(zone_id=19, row=3, col=2, group="main", label="S"),
Cell(zone_id=4, row=3, col=3, group="main", label="D"),
Cell(zone_id=6, row=3, col=4, group="main", label="F"),
Cell(zone_id=7, row=3, col=5, group="main", label="G"),
Cell(zone_id=8, row=3, col=6, group="main", label="H"),
Cell(zone_id=10, row=3, col=7, group="main", label="J"),
Cell(zone_id=11, row=3, col=8, group="main", label="K"),
Cell(zone_id=12, row=3, col=9, group="main", label="L"),
Cell(zone_id=48, row=3, col=10, group="main", label=";"),
Cell(zone_id=49, row=3, col=11, group="main", label="'"),
Cell(zone_id=37, row=3, col=13, group="main", label="Enter"),
# Row 4: shift + zxcv-row + comma/period/slash + rshift
Cell(zone_id=105, row=4, col=0, group="main", label="Shift"),
Cell(zone_id=26, row=4, col=2, group="main", label="Z"),
Cell(zone_id=24, row=4, col=3, group="main", label="X"),
Cell(zone_id=3, row=4, col=4, group="main", label="C"),
Cell(zone_id=22, row=4, col=5, group="main", label="V"),
Cell(zone_id=2, row=4, col=6, group="main", label="B"),
Cell(zone_id=14, row=4, col=7, group="main", label="N"),
Cell(zone_id=13, row=4, col=8, group="main", label="M"),
Cell(zone_id=51, row=4, col=9, group="main", label=","),
Cell(zone_id=52, row=4, col=10, group="main", label="."),
Cell(zone_id=53, row=4, col=11, group="main", label="/"),
Cell(zone_id=109, row=4, col=13, group="main", label="Shift"),
# Row 5: bottom row. Space spans cols 3..9 visually.
Cell(zone_id=104, row=5, col=0, group="main", label="Ctrl"),
Cell(zone_id=107, row=5, col=1, group="main", label="Win"),
Cell(zone_id=106, row=5, col=2, group="main", label="Alt"),
Cell(zone_id=41, row=5, col=3, width=7.0, group="main", label="Space"),
Cell(zone_id=110, row=5, col=10, group="main", label="AltGr"),
Cell(zone_id=111, row=5, col=11, group="main", label="Win"),
Cell(zone_id=98, row=5, col=12, group="main", label="Menu"),
Cell(zone_id=108, row=5, col=13, group="main", label="Ctrl"),
)
# --- Main alpha block, ISO. Drops the row 2 col 13 backslash (zone 46 is the
# upper half of the L-shape Enter on ISO, addressed by zone 37) and adds
# the two ISO-only keys: POUND (zone 47) at row 3 col 12 between ' and
# Enter, and ISO_BACKSLASH (zone 97) at row 4 col 1 between Shift and Z.
# Regional layouts override the labels to match local keycaps (# / < on
# QWERTZ, # / \ on UK QWERTY, * / < on AZERTY).
_ISO_EXTRA_KEYS: tuple[Cell, ...] = (
Cell(zone_id=47, row=3, col=12, group="main", label="#"),
Cell(zone_id=97, row=4, col=1, group="main", label="\\"),
)
MAIN_ISO: tuple[Cell, ...] = tuple(c for c in MAIN_ANSI if not (c.row == 2 and c.col == 13)) + _ISO_EXTRA_KEYS
# --- Curated allowlist for unmapped device zones surfaced in the bottom strip.
# G-keys, logo, media, brightness — the canonical "extras" Logitech firmware
# actually addresses. Phantom zones (e.g. G515's 47, 97, 99-103, 254) drop.
EXTRAS_ALLOWLIST: frozenset[int] = frozenset(
{
153, # Brightness
155, # Play/Pause
156, # Mute
157, # Next
158, # Previous
180, # G1
181, # G2
182, # G3
183, # G4
184, # G5
210, # Logo
}
)
def _relabel(cells: tuple[Cell, ...], overrides: dict[int, str]) -> tuple[Cell, ...]:
"""Return a new tuple where any cell whose zone_id is in `overrides` has
its label replaced. Unaffected cells pass through unchanged.
"""
if not overrides:
return cells
return tuple(
Cell(
zone_id=c.zone_id,
row=c.row,
col=c.col,
width=c.width,
height=c.height,
group=c.group,
label=overrides[c.zone_id] if c.zone_id in overrides else c.label,
x=c.x,
y=c.y,
)
for c in cells
)
def build_layout(
main_cells: tuple[Cell, ...],
*,
include_numpad: bool,
label_overrides: dict[int, str] | None = None,
description: str = "",
) -> Layout:
"""Assemble a regional keyboard layout from a chosen main block + the
shared fn-row / extras / (optionally) numpad blocks. Apply per-zone
label overrides to every cell whose zone matches.
"""
cells = FN_ROW + main_cells + EXTRAS
if include_numpad:
cells = cells + NUMPAD
cells = _relabel(cells, label_overrides or {})
cols = 21 if include_numpad else 17
return Layout(
cells=cells,
rows=6,
cols=cols,
extra_zones=EXTRAS_ALLOWLIST,
description=description,
)