From 1952e9ce9869dfad73d4f4163b2c477decf633c1 Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Sun, 10 May 2026 02:31:39 -0700 Subject: [PATCH] Add regional keyboard layouts (ISO_QWERTY, QWERTZ, AZERTY, JIS) and fix copyright MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/logitech_receiver/device.py | 8 + lib/logitech_receiver/hidpp20.py | 12 + lib/solaar/ui/perkey/__init__.py | 2 +- lib/solaar/ui/perkey/binding.py | 2 +- lib/solaar/ui/perkey/canvas.py | 2 +- lib/solaar/ui/perkey/control.py | 3 +- lib/solaar/ui/perkey/dialog.py | 2 +- lib/solaar/ui/perkey/editor.py | 2 +- lib/solaar/ui/perkey/layout.py | 2 +- lib/solaar/ui/perkey/layouts/__init__.py | 75 +++++- .../ui/perkey/layouts/_keyboard_base.py | 230 ++++++++++++++++++ lib/solaar/ui/perkey/layouts/keyboard_ansi.py | 172 +------------ .../ui/perkey/layouts/keyboard_iso_azerty.py | 75 ++++++ .../ui/perkey/layouts/keyboard_iso_qwerty.py | 45 ++++ .../ui/perkey/layouts/keyboard_iso_qwertz.py | 57 +++++ lib/solaar/ui/perkey/layouts/keyboard_jis.py | 52 ++++ lib/solaar/ui/perkey/layouts/mouse_g502x.py | 2 +- lib/solaar/ui/perkey/palette.py | 2 +- lib/solaar/ui/perkey/protocol.py | 2 +- lib/solaar/ui/perkey/tools.py | 2 +- 20 files changed, 565 insertions(+), 184 deletions(-) create mode 100644 lib/solaar/ui/perkey/layouts/_keyboard_base.py create mode 100644 lib/solaar/ui/perkey/layouts/keyboard_iso_azerty.py create mode 100644 lib/solaar/ui/perkey/layouts/keyboard_iso_qwerty.py create mode 100644 lib/solaar/ui/perkey/layouts/keyboard_iso_qwertz.py create mode 100644 lib/solaar/ui/perkey/layouts/keyboard_jis.py diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index 2f23c5d1..ff3565b7 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -156,6 +156,7 @@ class Device: self._tid_map = None # map from transports to product identifiers self._persister = None # persister holds settings self._led_effects = self._firmware = self._keys = self._remap_keys = self._gestures = self._force_buttons = None + self._keyboard_layout = None # lazy: country code from HID++ 0x4540, None if unsupported self._profiles = self._backlight = self._settings = None self.registers = [] self.notification_flags = None @@ -373,6 +374,13 @@ class Device: self._polling_rate = rate if rate else self._polling_rate return self._polling_rate + @property + def keyboard_layout(self): + if self._keyboard_layout is None and self.online and self.protocol >= 2.0: + if SupportedFeature.KEYBOARD_LAYOUT_2 in self.features: + self._keyboard_layout = _hidpp20.get_keyboard_layout(self) + return self._keyboard_layout + @property def led_effects(self): if not self._led_effects and self.online and self.protocol >= 2.0: diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index f5813836..a377cd44 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -2031,6 +2031,18 @@ class Hidpp20: SupportedFeature._fallback = lambda x: f"unknown:{x:04X}" return result + def get_keyboard_layout(self, device: Device): + """Return the device's keyboard layout country code, or None. + + Country code semantics match the HID HUT keyboard country codes that + Logitech's KEYBOARD_LAYOUT_2 (0x4540) feature reports in the first byte. + Used by the per-key painter to pick the matching regional layout. + """ + result = device.feature_request(SupportedFeature.KEYBOARD_LAYOUT_2, 0x00) + if result: + return struct.unpack("!B", result[:1])[0] + return None + def config_change(self, device: Device, configuration, no_reply=False): return device.feature_request(SupportedFeature.CONFIG_CHANGE, 0x10, configuration, no_reply=no_reply) diff --git a/lib/solaar/ui/perkey/__init__.py b/lib/solaar/ui/perkey/__init__.py index 28a39a5c..720b3b57 100644 --- a/lib/solaar/ui/perkey/__init__.py +++ b/lib/solaar/ui/perkey/__init__.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/binding.py b/lib/solaar/ui/perkey/binding.py index e3892c83..79cd6d63 100644 --- a/lib/solaar/ui/perkey/binding.py +++ b/lib/solaar/ui/perkey/binding.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/canvas.py b/lib/solaar/ui/perkey/canvas.py index 74b14e78..101f21e6 100644 --- a/lib/solaar/ui/perkey/canvas.py +++ b/lib/solaar/ui/perkey/canvas.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/control.py b/lib/solaar/ui/perkey/control.py index a209ceec..9c3abc3c 100644 --- a/lib/solaar/ui/perkey/control.py +++ b/lib/solaar/ui/perkey/control.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 @@ -243,6 +243,7 @@ class PerKeyControl(Gtk.Box): "wpid": getattr(device, "wpid", None), "codename": getattr(device, "codename", None), "name": getattr(device, "name", None), + "keyboard_layout": getattr(device, "keyboard_layout", None), "zones": list(self._sink.zones), "zone_count": len(self._sink.zones), } diff --git a/lib/solaar/ui/perkey/dialog.py b/lib/solaar/ui/perkey/dialog.py index 3c8528b2..b58aaf47 100644 --- a/lib/solaar/ui/perkey/dialog.py +++ b/lib/solaar/ui/perkey/dialog.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/editor.py b/lib/solaar/ui/perkey/editor.py index 71de9444..9b0bab47 100644 --- a/lib/solaar/ui/perkey/editor.py +++ b/lib/solaar/ui/perkey/editor.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/layout.py b/lib/solaar/ui/perkey/layout.py index 86c21dd9..d2ad463b 100644 --- a/lib/solaar/ui/perkey/layout.py +++ b/lib/solaar/ui/perkey/layout.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/layouts/__init__.py b/lib/solaar/ui/perkey/layouts/__init__.py index 48149386..b2bde7e2 100644 --- a/lib/solaar/ui/perkey/layouts/__init__.py +++ b/lib/solaar/ui/perkey/layouts/__init__.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 @@ -27,6 +27,10 @@ from collections.abc import Callable from ..layout import Layout from . import keyboard_ansi +from . import keyboard_iso_azerty +from . import keyboard_iso_qwerty +from . import keyboard_iso_qwertz +from . import keyboard_jis from . import mouse_g502x # (feature_id, matcher, layout). Matcher receives a `hint` dict the editor @@ -65,23 +69,74 @@ def _name_contains(*needles: str) -> Callable[[dict], bool]: return match -# --- Keyboards: distinguish full-size from TKL by presence of a numpad zone. -# Counting zones is unreliable (G515 reports phantom zones 47, 97, 99-103, 254 -# that diverge from the keycap count). +# --- Keyboard region routing --- +# Country code → layout family. Codes from HID++ feature 0x4540 KeyboardLayout. +_KEYBOARD_FAMILY_BY_COUNTRY: dict[int, str] = { + 1: "ansi", + # ISO QWERTY (UK + ES/IT/PT/BE/Nordic — same shape, different keycap legends) + 2: "iso_qwerty", + 5: "iso_qwerty", + 8: "iso_qwerty", + 0x0B: "iso_qwerty", + 0x0D: "iso_qwerty", + 0x0E: "iso_qwerty", + 0x0F: "iso_qwerty", + 0x16: "iso_qwerty", + 0x1D: "iso_qwerty", + 0x21: "iso_qwerty", + 0x24: "iso_qwerty", + # ISO QWERTZ (DE/Swiss) + 3: "iso_qwertz", + 7: "iso_qwertz", + # ISO AZERTY (FR) + 4: "iso_azerty", + # JIS + 9: "jis", + 0x3E: "jis", +} + +_FAMILY_LAYOUTS = { + "ansi": (keyboard_ansi.LAYOUT_FULL, keyboard_ansi.LAYOUT_TKL), + "iso_qwerty": (keyboard_iso_qwerty.LAYOUT_FULL, keyboard_iso_qwerty.LAYOUT_TKL), + "iso_qwertz": (keyboard_iso_qwertz.LAYOUT_FULL, keyboard_iso_qwertz.LAYOUT_TKL), + "iso_azerty": (keyboard_iso_azerty.LAYOUT_FULL, keyboard_iso_azerty.LAYOUT_TKL), + "jis": (keyboard_jis.LAYOUT_FULL, keyboard_jis.LAYOUT_TKL), +} + + def _has_numpad(hint: dict) -> bool: + """Numpad presence is read from the device's reported zone bitmap rather + than counting zones — G515 reports phantom zones (47, 97, 99-103, 254) + that diverge from the keycap count. + """ zones = set(hint.get("zones", ())) return 80 in zones or 95 in zones -def _is_full_keyboard(hint: dict) -> bool: - return hint.get("kind") == "keyboard" and _has_numpad(hint) +def _keyboard_family(hint: dict) -> str: + """Pick a layout family from the device's HID++ keyboard layout country + code. Defaults to "ansi" when the code is missing or unknown. + """ + code = hint.get("keyboard_layout") + if code is None: + return "ansi" + return _KEYBOARD_FAMILY_BY_COUNTRY.get(int(code), "ansi") -def _is_tkl_keyboard(hint: dict) -> bool: - return hint.get("kind") == "keyboard" and not _has_numpad(hint) +def _keyboard_matcher(family: str, full_size: bool) -> Callable[[dict], bool]: + def match(hint: dict) -> bool: + if hint.get("kind") != "keyboard": + return False + if _has_numpad(hint) != full_size: + return False + return _keyboard_family(hint) == family + + return match # PER_KEY_LIGHTING_V2 = 0x8081 -register_layout(0x8081, _is_full_keyboard, keyboard_ansi.LAYOUT_FULL) -register_layout(0x8081, _is_tkl_keyboard, keyboard_ansi.LAYOUT_TKL) +for _family, (_full, _tkl) in _FAMILY_LAYOUTS.items(): + register_layout(0x8081, _keyboard_matcher(_family, full_size=True), _full) + register_layout(0x8081, _keyboard_matcher(_family, full_size=False), _tkl) + register_layout(0x8081, _name_contains("G502 X"), mouse_g502x.LAYOUT) diff --git a/lib/solaar/ui/perkey/layouts/_keyboard_base.py b/lib/solaar/ui/perkey/layouts/_keyboard_base.py new file mode 100644 index 00000000..2e38c47a --- /dev/null +++ b/lib/solaar/ui/perkey/layouts/_keyboard_base.py @@ -0,0 +1,230 @@ +## 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. Same as ANSI minus the row 2 col 13 backslash; +# on ISO that position is the top half of the L-shape Enter, addressed +# by zone 37 (the main Enter cell at row 3 col 13). Zone 46 is silently +# unaddressable on ISO layouts — same limitation as OpenRGB's UI. +MAIN_ISO: tuple[Cell, ...] = tuple(c for c in MAIN_ANSI if not (c.row == 2 and c.col == 13)) + +# --- 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, + ) diff --git a/lib/solaar/ui/perkey/layouts/keyboard_ansi.py b/lib/solaar/ui/perkey/layouts/keyboard_ansi.py index f24f30b6..8a0509f7 100644 --- a/lib/solaar/ui/perkey/layouts/keyboard_ansi.py +++ b/lib/solaar/ui/perkey/layouts/keyboard_ansi.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 @@ -20,177 +20,23 @@ Cell positions and groupings derived from OpenRGB's KeyboardLayoutManager (KeyboardLayoutManager.cpp), Copyright (C) Chris M (Dr_No), licensed under GPL-2.0-or-later. This file ports the static ANSI data only; the runtime opcode interpreter for regional overlays is intentionally not included. - -Zone IDs are the firmware values reported by Logitech HID++ feature 0x8081 -(PER_KEY_LIGHTING_V2). """ from __future__ import annotations -from ..layout import Cell from ..layout import Layout +from ._keyboard_base import MAIN_ANSI +from ._keyboard_base import build_layout -# Main alpha block (KLM keyboard_zone_main, ANSI variant). -# ANSI removes the ISO backslash (row 4 col 1) and POUND (row 3 col 12). -_MAIN: 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"), -) - -# Function row (KLM keyboard_zone_fn_row): ESC + F1..F12. -_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"), -) - -# Extras cluster (KLM keyboard_zone_extras): nav block + arrows. -_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 (KLM keyboard_zone_numpad). NumPad + and Enter span 2 rows tall. -_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="."), -) - - -# Curated allowlist for unmapped device zones surfaced in the bottom strip. -# Mirrors OpenRGB's `hidpp20_key_name_to_zone` extras: brightness, media, -# G1-G5, logo. Anything else (e.g. G515 phantoms 47, 97, 99-103, 254) is -# dropped by the binder. -_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 - } -) - - -LAYOUT_FULL: Layout = Layout( - cells=_FN_ROW + _MAIN + _EXTRAS + _NUMPAD, - rows=6, - cols=21, - extra_zones=_EXTRAS_ALLOWLIST, +LAYOUT_FULL: Layout = build_layout( + MAIN_ANSI, + include_numpad=True, description="ANSI QWERTY 104-key full-size", ) -LAYOUT_TKL: Layout = Layout( - cells=_FN_ROW + _MAIN + _EXTRAS, - rows=6, - cols=17, - extra_zones=_EXTRAS_ALLOWLIST, +LAYOUT_TKL: Layout = build_layout( + MAIN_ANSI, + include_numpad=False, description="ANSI QWERTY tenkeyless", ) diff --git a/lib/solaar/ui/perkey/layouts/keyboard_iso_azerty.py b/lib/solaar/ui/perkey/layouts/keyboard_iso_azerty.py new file mode 100644 index 00000000..7f23179b --- /dev/null +++ b/lib/solaar/ui/perkey/layouts/keyboard_iso_azerty.py @@ -0,0 +1,75 @@ +## 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. + +"""ISO AZERTY layout (FR). + +ISO shape plus French label overrides — A↔Q, W↔Z, M repositioned, French +digit-row symbols (& é " ' ( - è _ ç à ). Adapted from OpenRGB. +""" + +from __future__ import annotations + +from ..layout import Layout +from ._keyboard_base import MAIN_ISO +from ._keyboard_base import build_layout + +# zone_id → French label +_OVERRIDES: dict[int, str] = { + # Row 1 (digit row → French symbols) + 50: "²", # backtick → super-2 + 27: "&", # 1 + 28: "é", # 2 + 29: '"', # 3 + 30: "'", # 4 + 31: "(", # 5 + 32: "-", # 6 + 33: "è", # 7 + 34: "_", # 8 + 35: "ç", # 9 + 36: "à", # 0 + 42: ")", # minus → close-paren + # Row 2 — Q/A and W/Z swaps, brackets relabeled + 17: "A", # Q-position → A + 23: "Z", # W-position → Z + 44: "^", # [-position → caret + 45: "$", # ]-position → dollar + # Row 3 — A → Q; M moves up to ; position + 1: "Q", # A-position → Q + 48: "M", # ;-position → M + 49: "ù", # '-position → ù + # Row 4 — Z-position becomes W; comma row shifts + 26: "W", # Z-position → W + 13: ",", # M-position → comma + 51: ";", # ,-position → semicolon + 52: ":", # .-position → colon + 53: "!", # /-position → exclamation +} + + +LAYOUT_FULL: Layout = build_layout( + MAIN_ISO, + include_numpad=True, + label_overrides=_OVERRIDES, + description="ISO AZERTY (FR) full-size", +) + + +LAYOUT_TKL: Layout = build_layout( + MAIN_ISO, + include_numpad=False, + label_overrides=_OVERRIDES, + description="ISO AZERTY (FR) tenkeyless", +) diff --git a/lib/solaar/ui/perkey/layouts/keyboard_iso_qwerty.py b/lib/solaar/ui/perkey/layouts/keyboard_iso_qwerty.py new file mode 100644 index 00000000..118b6849 --- /dev/null +++ b/lib/solaar/ui/perkey/layouts/keyboard_iso_qwerty.py @@ -0,0 +1,45 @@ +## 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. + +"""ISO_QWERTY keyboard layouts (UK English ISO + other QWERTY ISO regions). + +Same English keycap legends as ANSI; differs only in shape — the row 2 col 13 +backslash on ANSI doesn't exist on ISO (that position is the upper half of the +L-shape Enter, addressed by zone 37). Used for UK and any other region whose +country code maps to "iso_qwerty" without a more specific layout (Spanish, +Italian, Portuguese, Belgian, Nordic — those keyboards have the same shape +as UK ISO; only their physical keycap legends differ, which our painter +doesn't reproduce verbatim). +""" + +from __future__ import annotations + +from ..layout import Layout +from ._keyboard_base import MAIN_ISO +from ._keyboard_base import build_layout + +LAYOUT_FULL: Layout = build_layout( + MAIN_ISO, + include_numpad=True, + description="ISO QWERTY 103-key full-size", +) + + +LAYOUT_TKL: Layout = build_layout( + MAIN_ISO, + include_numpad=False, + description="ISO QWERTY tenkeyless", +) diff --git a/lib/solaar/ui/perkey/layouts/keyboard_iso_qwertz.py b/lib/solaar/ui/perkey/layouts/keyboard_iso_qwertz.py new file mode 100644 index 00000000..04d52bfe --- /dev/null +++ b/lib/solaar/ui/perkey/layouts/keyboard_iso_qwertz.py @@ -0,0 +1,57 @@ +## 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. + +"""ISO QWERTZ layout (DE / CH). + +ISO shape plus German label overrides (Y/Z swap, Ü/Ö/Ä/ß placement). +Adapted from OpenRGB. +""" + +from __future__ import annotations + +from ..layout import Layout +from ._keyboard_base import MAIN_ISO +from ._keyboard_base import build_layout + +# zone_id → German label +_OVERRIDES: dict[int, str] = { + 50: "^", # row 1 col 0 — caret/degree (DE keycap) + 42: "ß", # row 1 col 11 — eszett + 43: "´", # row 1 col 12 — acute accent + 25: "Z", # row 2 col 6 — Y/Z swap + 44: "Ü", # row 2 col 11 + 45: "+", # row 2 col 12 + 48: "Ö", # row 3 col 10 + 49: "Ä", # row 3 col 11 + 26: "Y", # row 4 col 2 — Y/Z swap + 53: "-", # row 4 col 11 +} + + +LAYOUT_FULL: Layout = build_layout( + MAIN_ISO, + include_numpad=True, + label_overrides=_OVERRIDES, + description="ISO QWERTZ (DE/CH) full-size", +) + + +LAYOUT_TKL: Layout = build_layout( + MAIN_ISO, + include_numpad=False, + label_overrides=_OVERRIDES, + description="ISO QWERTZ (DE/CH) tenkeyless", +) diff --git a/lib/solaar/ui/perkey/layouts/keyboard_jis.py b/lib/solaar/ui/perkey/layouts/keyboard_jis.py new file mode 100644 index 00000000..e320c822 --- /dev/null +++ b/lib/solaar/ui/perkey/layouts/keyboard_jis.py @@ -0,0 +1,52 @@ +## 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. + +"""JIS layout (JP). + +ISO shape with Japanese keycap relabels for the bracket / colon positions. +Adapted from OpenRGB. JIS keyboards also have additional kana-control keys +near the spacebar (henkan/muhenkan/kana) that aren't represented here — +matches OpenRGB's coverage. +""" + +from __future__ import annotations + +from ..layout import Layout +from ._keyboard_base import MAIN_ISO +from ._keyboard_base import build_layout + +# zone_id → JIS label +_OVERRIDES: dict[int, str] = { + 44: "@", # row 2 col 11 — bracket-position becomes at-sign + 45: "[", # row 2 col 12 — bracket shifts left + 49: ":", # row 3 col 11 — quote-position becomes colon +} + + +LAYOUT_FULL: Layout = build_layout( + MAIN_ISO, + include_numpad=True, + label_overrides=_OVERRIDES, + description="JIS (JP) full-size", +) + + +LAYOUT_TKL: Layout = build_layout( + MAIN_ISO, + include_numpad=False, + label_overrides=_OVERRIDES, + description="JIS (JP) tenkeyless", +) diff --git a/lib/solaar/ui/perkey/layouts/mouse_g502x.py b/lib/solaar/ui/perkey/layouts/mouse_g502x.py index 2d0f4b52..b26fbf41 100644 --- a/lib/solaar/ui/perkey/layouts/mouse_g502x.py +++ b/lib/solaar/ui/perkey/layouts/mouse_g502x.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/palette.py b/lib/solaar/ui/perkey/palette.py index a35c8178..1d738e61 100644 --- a/lib/solaar/ui/perkey/palette.py +++ b/lib/solaar/ui/perkey/palette.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/protocol.py b/lib/solaar/ui/perkey/protocol.py index 0feb26bf..dada0f3c 100644 --- a/lib/solaar/ui/perkey/protocol.py +++ b/lib/solaar/ui/perkey/protocol.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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 diff --git a/lib/solaar/ui/perkey/tools.py b/lib/solaar/ui/perkey/tools.py index 98f4482f..b9432dbd 100644 --- a/lib/solaar/ui/perkey/tools.py +++ b/lib/solaar/ui/perkey/tools.py @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## 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