Add files via upload

This commit is contained in:
Mohamed Mahjoub
2026-09-11 21:08:24 +01:00
committed by GitHub
parent c7c02edb60
commit 5b7bf9dadd
17 changed files with 164 additions and 36 deletions

View File

@@ -8,7 +8,8 @@ import sys
from pathlib import Path
from PySide6.QtCore import QEvent, QSettings, Qt, QTimer, Signal
from PySide6.QtGui import QColor, QFont, QFontDatabase, QGuiApplication, QPalette
from PySide6.QtGui import (QColor, QFont, QFontDatabase, QGuiApplication, QIcon,
QPalette)
from PySide6.QtWidgets import (QApplication, QButtonGroup, QFileDialog, QFrame,
QGridLayout, QHBoxLayout, QLabel, QLineEdit,
QMainWindow, QMessageBox, QPlainTextEdit,
@@ -201,9 +202,11 @@ class Inspector(QWidget):
modes.setSpacing(6)
self.mode_keys = _button("Shortcut", checkable=True)
self.mode_media = _button("Media key", checkable=True)
self.mode_none = _button("Nothing", checkable=True)
self.mode_none.setToolTip("Make this control do nothing at all")
self.mode_group = QButtonGroup(self)
self.mode_group.setExclusive(True)
for i, b in enumerate((self.mode_keys, self.mode_media)):
for i, b in enumerate((self.mode_keys, self.mode_media, self.mode_none)):
self.mode_group.addButton(b, i)
modes.addWidget(b)
modes.addStretch()
@@ -239,9 +242,12 @@ class Inspector(QWidget):
self.media_group.addButton(b, [m for m, _ in core.MEDIA].index(mid))
self.media_buttons[mid] = b
ml.addWidget(b, r, col)
self.pages = (kp, mp)
np_ = _label("Pressing or turning it will do nothing. Handy for a key "
"you keep hitting by accident.", "dim", wrap=True)
self.pages = (kp, mp, np_)
ev.addWidget(kp)
ev.addWidget(mp)
ev.addWidget(np_)
ev.addSpacing(10)
actions = QHBoxLayout()
@@ -325,7 +331,12 @@ class Inspector(QWidget):
self.onpad_why.hide()
shown = desired or (None if entry.unknown else entry.binding)
if shown is not None and shown.media:
if shown is not None and shown.none:
self.mode_none.setChecked(True)
self._page(2)
self.keys_edit.clear()
self._clear_media()
elif shown is not None and shown.media:
self.mode_media.setChecked(True)
self._page(1)
self.media_buttons[shown.media].setChecked(True)
@@ -357,6 +368,8 @@ class Inspector(QWidget):
def _mode(self, i):
self._page(i)
if i == 2 and not self._loading and self.control is not None:
self.changed.emit(self.control, core.Binding(none=True))
if i == 0 and self.record.isChecked():
self.keys_edit.setFocus()
@@ -484,7 +497,7 @@ class Window(QMainWindow):
elif e.unknown:
looks[c] = Look("", known=False)
else:
looks[c] = Look(e.binding.label(), known=True)
looks[c] = Look(e.binding.label(), known=True, quiet=e.binding.none)
self.pad.set_looks(looks)
n = len(pending)
@@ -569,7 +582,10 @@ class Window(QMainWindow):
if len(done) == 1:
c, b = done[0]
self._say(f"{core.control_name(c)} is now {b.label()}.", GOOD)
if b.none:
self._say(f"{core.control_name(c)} now does nothing.", GOOD)
else:
self._say(f"{core.control_name(c)} is now {b.label()}.", GOOD)
else:
self._say(f"Wrote {len(done)} changes to the pad.", GOOD)
self._refresh()
@@ -637,10 +653,17 @@ def apply_theme(app):
app.setStyleSheet(STYLE)
ICON = Path(__file__).resolve().parent.parent / "packaging" / "macropad.svg"
def run():
app = QApplication(sys.argv)
app.setApplicationName("macropad")
app.setDesktopFileName("macropad")
# The launcher gives Plasma the icon; this covers the window itself, X11
# sessions, and running straight from a git checkout.
if ICON.exists():
app.setWindowIcon(QIcon(str(ICON)))
apply_theme(app)
w = Window()
w.resize(1000, 800)

View File

@@ -144,6 +144,7 @@ class Binding:
Binding(keys="ctrl+c")
Binding(keys="a,b,c", delay=50)
Binding(media="volumeup")
Binding(none=True) the control does nothing
The kind is explicit rather than guessed from the string, because
'pause' is both a keyboard key and a media key.
@@ -151,17 +152,18 @@ class Binding:
keys: str = ""
media: str = ""
delay: int = 0
none: bool = False
def validate(self):
if bool(self.keys) == bool(self.media):
raise ValueError("a binding is either keys or media, not both or neither")
if sum(map(bool, (self.keys, self.media, self.none))) != 1:
raise ValueError("a binding is exactly one of keys, media or nothing")
if self.media and self.media not in proto.MEDIA_KEYS:
raise ValueError(f"unknown media key '{self.media}'")
if self.keys:
proto.parse_keystroke(self.keys) # raises ValueError
if not 0 <= self.delay <= 0xFFFF:
raise ValueError("delay must be 0-65535 ms")
if self.media and self.delay:
if self.delay and not self.keys:
raise ValueError("delay only applies to key sequences")
return self
@@ -169,9 +171,18 @@ class Binding:
def steps(self):
return len(proto.parse_keystroke(self.keys)) if self.keys else 1
@property
def is_nothing(self):
return self.none
def reports(self, report_id, action, layer=1):
"""The exact reports to send, built entirely by macropad.py."""
self.validate()
if self.none:
# A keystroke with no modifier and no key: count 0, pair 00 00.
# This is ch57x-keyboard-tool's encoding for a modifier-only key,
# with no modifier. Needs confirming on hardware (see README).
return proto.native_key(report_id, action, [(0, 0)], layer, 0)
if self.media:
return proto.native_media(report_id, action,
proto.MEDIA_KEYS[self.media], layer)
@@ -185,12 +196,16 @@ class Binding:
def label(self):
"""What a person reads on the key: 'Ctrl+Shift+N', 'Volume up'."""
if self.none:
return "Nothing"
if self.media:
return MEDIA_LABEL.get(self.media, self.media)
text = ", ".join(pretty_step(s) for s in self.keys.split(",") if s.strip())
return text + (f" ({self.delay} ms)" if self.delay else "")
def to_json(self):
if self.none:
return {"none": True}
if self.media:
return {"media": self.media}
d = {"keys": self.keys}
@@ -201,7 +216,8 @@ class Binding:
@classmethod
def from_json(cls, d):
return cls(keys=d.get("keys", ""), media=d.get("media", ""),
delay=int(d.get("delay", 0))).validate()
delay=int(d.get("delay", 0)),
none=bool(d.get("none", False))).validate()
# ------------------------------------------------------------------- files
@@ -426,8 +442,10 @@ def diagnose(keyd=True):
"worked out. Pads in this family differ in layout and "
"protocol, so writing to yours with the wrong one could "
"fail or scramble it. The app won't try.",
"You can help add it: the README's \"Other pads\" section "
"shows how to capture what the vendor software sends."],
"ch57x-keyboard-tool, a command line tool, supports several "
"of these pads today. To get yours into this app, the "
"README's \"Other pads\" section shows how to capture what "
"the vendor software sends."],
warnings=warnings)
if not found:
@@ -570,6 +588,12 @@ def decode_config(report):
media = _MEDIA_BY_USAGE.get(report[11] | (report[12] << 8))
return (control, layer, Binding(media=media)) if media else None
if kind == proto.KeyType.BASIC and count == 0:
mods, code = report[11], report[12]
if not mods and not code:
return control, layer, Binding(none=True)
return None
if kind == proto.KeyType.BASIC and count:
steps = []
for i in range(count):

View File

@@ -43,6 +43,7 @@ class Look:
text: str = "" # what's on the pad, or will be once written
known: bool = False
pending: bool = False
quiet: bool = False # known, but does nothing: legend drawn dimmed
def _key_grid(orientation):
@@ -268,7 +269,7 @@ class PadView(QWidget):
if look.pending:
p.setPen(TAPE)
else:
p.setPen(INK if look.known else INK_DIM)
p.setPen(INK if look.known and not look.quiet else INK_DIM)
text = look.text if (look.known or look.pending) else "Unknown"
body = rect.adjusted(8, 22, -8, -10)
# Let 'Meta+Ctrl+Left' break after a '+' instead of being cut off.
@@ -379,7 +380,8 @@ class PadView(QWidget):
f = QFont(self.font())
f.setPixelSize(12)
p.setFont(f)
p.setPen(TAPE if look.pending else (INK if look.known else INK_DIM))
p.setPen(TAPE if look.pending else
(INK if look.known and not look.quiet else INK_DIM))
text = look.text if (look.known or look.pending) else "Unknown"
body = rect.adjusted(22, 0, -26, 0)
p.drawText(body, Qt.AlignLeft | Qt.AlignVCenter,