Add files via upload
This commit is contained in:
648
macropad_gui/app.py
Normal file
648
macropad_gui/app.py
Normal file
@@ -0,0 +1,648 @@
|
||||
"""
|
||||
The window. Open it, change what a control does, write it, close it. The
|
||||
pad keeps its bindings in its own memory, so nothing here needs to keep
|
||||
running afterwards.
|
||||
"""
|
||||
|
||||
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.QtWidgets import (QApplication, QButtonGroup, QFileDialog, QFrame,
|
||||
QGridLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QMainWindow, QMessageBox, QPlainTextEdit,
|
||||
QPushButton, QVBoxLayout, QWidget)
|
||||
|
||||
from . import core, keymap
|
||||
from .padview import INK, INK_DIM, TAPE, WINDOW, Look, PadView
|
||||
|
||||
GOOD = "#8db36b"
|
||||
BAD = "#e0584f"
|
||||
LINE = "#3a3e42"
|
||||
FIELD = "#121416"
|
||||
|
||||
# Media keys as pairs, so opposites sit side by side.
|
||||
MEDIA_GRID = [("playpause", "stop"), ("prev", "next"),
|
||||
("volumedown", "volumeup"), ("mute", None),
|
||||
("brightnessdown", "brightnessup"),
|
||||
("calculator", "mail"), ("browser", "mycomputer")]
|
||||
|
||||
STYLE = f"""
|
||||
QWidget {{ color: {INK.name()}; background: {WINDOW.name()}; }}
|
||||
QLabel[role="title"] {{ font-size: 18px; font-weight: 600; }}
|
||||
QLabel[role="dim"] {{ color: {INK_DIM.name()}; }}
|
||||
QLabel[role="caption"] {{ color: {INK_DIM.name()}; padding: 14px 0px 0px 0px; margin: 0px; }}
|
||||
QLabel[role="value"] {{ font-size: 15px; }}
|
||||
QPushButton {{ background: transparent; border: 1px solid {LINE};
|
||||
border-radius: 4px; padding: 6px 12px; }}
|
||||
QPushButton:hover {{ border-color: {INK_DIM.name()}; }}
|
||||
QPushButton:checked {{ border-color: {TAPE.name()}; color: {TAPE.name()}; }}
|
||||
QPushButton:disabled {{ color: #4a4e52; border-color: #2a2e32; }}
|
||||
QPushButton[role="primary"] {{ background: {TAPE.name()}; color: #1b1206;
|
||||
border: none; font-weight: 600; padding: 8px 16px; }}
|
||||
QPushButton[role="primary"]:hover {{ background: #f39a45; }}
|
||||
QPushButton[role="primary"]:disabled {{ background: #2c3034; color: #5a5e62; }}
|
||||
QPushButton[role="flat"] {{ border: none; color: {INK_DIM.name()}; padding: 6px 8px; }}
|
||||
QPushButton[role="flat"]:hover {{ color: {INK.name()}; }}
|
||||
QPushButton[role="media"] {{ text-align: left; padding: 7px 10px; }}
|
||||
QLineEdit {{ background: {FIELD}; border: 1px solid {LINE}; border-radius: 4px;
|
||||
padding: 7px 8px; }}
|
||||
QLineEdit:focus {{ border-color: {INK_DIM.name()}; }}
|
||||
QLineEdit[recording="true"] {{ border-color: {TAPE.name()}; background: #2a1d10; }}
|
||||
QPlainTextEdit {{ background: {FIELD}; border: 1px solid {LINE}; border-radius: 4px; }}
|
||||
QFrame[role="banner"] {{ border: 1px solid {LINE}; border-left: 3px solid {BAD};
|
||||
border-radius: 4px; }}
|
||||
QFrame[role="banner"][tone="quiet"] {{ border-left: 3px solid {INK_DIM.name()}; }}
|
||||
QFrame[role="banner"] QLabel, QFrame[role="banner"] QPushButton {{ background: transparent; }}
|
||||
QFrame[role="footer"] {{ border-top: 1px solid {LINE}; }}
|
||||
"""
|
||||
|
||||
|
||||
def _label(text="", role=None, wrap=False):
|
||||
lab = QLabel(text)
|
||||
lab.setIndent(0)
|
||||
if role:
|
||||
lab.setProperty("role", role)
|
||||
lab.setWordWrap(wrap)
|
||||
return lab
|
||||
|
||||
|
||||
def _button(text, role=None, checkable=False):
|
||||
b = QPushButton(text)
|
||||
if role:
|
||||
b.setProperty("role", role)
|
||||
b.setCheckable(checkable)
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
return b
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ recorder
|
||||
|
||||
class RecorderEdit(QLineEdit):
|
||||
"""
|
||||
A text field that can also listen for a key combo. Type 'ctrl+shift+n',
|
||||
or press Record and then press the combo itself.
|
||||
"""
|
||||
recorded = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.recording = False
|
||||
self.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
|
||||
|
||||
def set_recording(self, on):
|
||||
self.recording = on
|
||||
self.setProperty("recording", "true" if on else "false")
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
if on:
|
||||
self._before = self.text()
|
||||
self.clear()
|
||||
self.setPlaceholderText("Press the combo now")
|
||||
self.setFocus()
|
||||
else:
|
||||
self.setPlaceholderText("ctrl+shift+n")
|
||||
|
||||
def event(self, e):
|
||||
# Catch Tab and friends too, which would otherwise move focus.
|
||||
if self.recording and e.type() == QEvent.KeyPress:
|
||||
self._record(e)
|
||||
return True
|
||||
if self.recording and e.type() == QEvent.ShortcutOverride:
|
||||
e.accept()
|
||||
return True
|
||||
return super().event(e)
|
||||
|
||||
def _record(self, e):
|
||||
mods = keymap.modifier_names(e)
|
||||
if e.key() in keymap.MODIFIER_KEYS:
|
||||
self.setText("+".join(mods + ["…"]))
|
||||
return
|
||||
name = keymap.key_name(e)
|
||||
if name is None:
|
||||
self.setText("")
|
||||
self.setPlaceholderText("The pad can't send that key. Try another.")
|
||||
return
|
||||
combo = "+".join(mods + [name])
|
||||
self.set_recording(False)
|
||||
self.setText(combo)
|
||||
self.recorded.emit(combo)
|
||||
|
||||
def focusOutEvent(self, e):
|
||||
if self.recording:
|
||||
self.set_recording(False)
|
||||
self.setText(self._before)
|
||||
super().focusOutEvent(e)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ inspector
|
||||
|
||||
class Inspector(QWidget):
|
||||
"""Right-hand panel: what the selected control does, and what to change it to."""
|
||||
changed = Signal(str, object) # control, Binding or None (revert)
|
||||
write = Signal(str)
|
||||
revert = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFixedWidth(360)
|
||||
self.control = None
|
||||
self._loading = False
|
||||
|
||||
v = QVBoxLayout(self)
|
||||
v.setContentsMargins(24, 24, 24, 16)
|
||||
v.setSpacing(4)
|
||||
|
||||
# problem banner (no pad / no permission)
|
||||
self.banner = QFrame()
|
||||
self.banner.setProperty("role", "banner")
|
||||
bl = QVBoxLayout(self.banner)
|
||||
bl.setContentsMargins(12, 10, 12, 10)
|
||||
self.banner_head = _label("", wrap=True)
|
||||
self.banner_head.setStyleSheet("font-weight: 600;")
|
||||
self.banner_text = _label("", "dim", wrap=True)
|
||||
self.banner_fix = QPlainTextEdit()
|
||||
self.banner_fix.setReadOnly(True)
|
||||
self.banner_fix.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
|
||||
self.banner_fix.setFixedHeight(84)
|
||||
self.banner_fix.setLineWrapMode(QPlainTextEdit.NoWrap)
|
||||
self.banner_copy = _button("Copy commands")
|
||||
self.banner_copy.clicked.connect(self._copy)
|
||||
for w in (self.banner_head, self.banner_text, self.banner_fix, self.banner_copy):
|
||||
bl.addWidget(w)
|
||||
self.banner_copy.setSizePolicy(self.banner_copy.sizePolicy().horizontalPolicy(),
|
||||
self.banner_copy.sizePolicy().verticalPolicy())
|
||||
v.addWidget(self.banner)
|
||||
v.addSpacing(8)
|
||||
|
||||
# empty state
|
||||
self.empty = _label("", "dim", wrap=True)
|
||||
v.addWidget(self.empty)
|
||||
|
||||
# editor
|
||||
self.editor = QWidget()
|
||||
ev = QVBoxLayout(self.editor)
|
||||
ev.setContentsMargins(0, 0, 0, 0)
|
||||
ev.setSpacing(4)
|
||||
self.title = _label("", "title")
|
||||
self.sub = _label("", "dim")
|
||||
ev.addWidget(self.title)
|
||||
ev.addWidget(self.sub)
|
||||
|
||||
ev.addWidget(_label("On the pad now", "caption"))
|
||||
self.onpad = _label("", "value", wrap=True)
|
||||
self.onpad_why = _label("", "dim", wrap=True)
|
||||
ev.addWidget(self.onpad)
|
||||
ev.addWidget(self.onpad_why)
|
||||
|
||||
ev.addWidget(_label("Change it to", "caption"))
|
||||
modes = QHBoxLayout()
|
||||
modes.setSpacing(6)
|
||||
self.mode_keys = _button("Shortcut", checkable=True)
|
||||
self.mode_media = _button("Media key", checkable=True)
|
||||
self.mode_group = QButtonGroup(self)
|
||||
self.mode_group.setExclusive(True)
|
||||
for i, b in enumerate((self.mode_keys, self.mode_media)):
|
||||
self.mode_group.addButton(b, i)
|
||||
modes.addWidget(b)
|
||||
modes.addStretch()
|
||||
ev.addLayout(modes)
|
||||
ev.addSpacing(6)
|
||||
|
||||
# shortcut page
|
||||
kp = QWidget()
|
||||
kl = QVBoxLayout(kp)
|
||||
kl.setContentsMargins(0, 0, 0, 0)
|
||||
row = QHBoxLayout()
|
||||
self.keys_edit = RecorderEdit()
|
||||
self.keys_edit.setPlaceholderText("ctrl+shift+n")
|
||||
self.record = _button("Record", checkable=True)
|
||||
row.addWidget(self.keys_edit, 1)
|
||||
row.addWidget(self.record)
|
||||
kl.addLayout(row)
|
||||
self.keys_hint = _label("", "dim", wrap=True)
|
||||
kl.addWidget(self.keys_hint)
|
||||
# media page
|
||||
mp = QWidget()
|
||||
ml = QGridLayout(mp)
|
||||
ml.setContentsMargins(0, 0, 0, 0)
|
||||
ml.setSpacing(6)
|
||||
self.media_group = QButtonGroup(self)
|
||||
self.media_group.setExclusive(True)
|
||||
self.media_buttons = {}
|
||||
for r, pair in enumerate(MEDIA_GRID):
|
||||
for col, mid in enumerate(pair):
|
||||
if mid is None:
|
||||
continue
|
||||
b = _button(core.MEDIA_LABEL[mid], "media", checkable=True)
|
||||
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)
|
||||
ev.addWidget(kp)
|
||||
ev.addWidget(mp)
|
||||
|
||||
ev.addSpacing(10)
|
||||
actions = QHBoxLayout()
|
||||
self.write_btn = _button("Write to pad", "primary")
|
||||
self.revert_btn = _button("Revert", "flat")
|
||||
actions.addWidget(self.write_btn)
|
||||
actions.addWidget(self.revert_btn)
|
||||
actions.addStretch()
|
||||
ev.addLayout(actions)
|
||||
v.addWidget(self.editor)
|
||||
v.addStretch()
|
||||
|
||||
self.mode_group.idClicked.connect(self._mode)
|
||||
self.keys_edit.textEdited.connect(self._keys_typed)
|
||||
self.keys_edit.recorded.connect(self._keys_typed)
|
||||
self.record.toggled.connect(self._toggle_record)
|
||||
self.media_group.idClicked.connect(self._media_picked)
|
||||
self.write_btn.clicked.connect(lambda: self.write.emit(self.control))
|
||||
self.revert_btn.clicked.connect(lambda: self.revert.emit(self.control))
|
||||
|
||||
self.show_control(None, None, None, False)
|
||||
|
||||
# -------------------------------------------------------------- banner
|
||||
|
||||
def show_problem(self, diag, note=None):
|
||||
if diag is None or diag.ready:
|
||||
if not note:
|
||||
self.banner.hide()
|
||||
return
|
||||
diag = core.Diagnosis("note", "Heads up", detail=[note])
|
||||
quiet = diag.status in ("unplugged", "note", "unsupported")
|
||||
self.banner.setProperty("tone", "quiet" if quiet else "")
|
||||
self.banner.style().unpolish(self.banner)
|
||||
self.banner.style().polish(self.banner)
|
||||
self.banner_head.setText(diag.headline)
|
||||
self.banner_text.setText("\n\n".join(diag.detail))
|
||||
cmds = [c for c in diag.fix if not c.startswith("#")]
|
||||
self.banner_fix.setPlainText("\n".join(cmds))
|
||||
lh = self.banner_fix.fontMetrics().lineSpacing()
|
||||
self.banner_fix.setFixedHeight(lh * max(1, len(cmds)) + 30)
|
||||
self.banner_fix.setVisible(bool(cmds))
|
||||
self.banner_copy.setVisible(bool(cmds))
|
||||
if diag.fix and diag.fix[-1].startswith("#"):
|
||||
self.banner_text.setText(self.banner_text.text() + "\n\nThen unplug the pad and plug it back in.")
|
||||
self.banner.show()
|
||||
|
||||
def _copy(self):
|
||||
QGuiApplication.clipboard().setText(self.banner_fix.toPlainText())
|
||||
self.banner_copy.setText("Copied")
|
||||
QTimer.singleShot(1500, lambda: self.banner_copy.setText("Copy commands"))
|
||||
|
||||
# -------------------------------------------------------------- editor
|
||||
|
||||
def show_control(self, control, entry, desired, can_write, all_unknown=False):
|
||||
self._loading = True
|
||||
self.control = control
|
||||
if control is None:
|
||||
self.editor.hide()
|
||||
self.empty.setText(
|
||||
"Click a key or dial to change what it does.\n\n"
|
||||
+ ("The pad can't report what's on it, so this app only knows "
|
||||
"what it has written itself. Hatched controls are unknown. "
|
||||
"If you set the pad up with the vendor software, you can "
|
||||
"import that capture instead." if all_unknown else ""))
|
||||
self.empty.show()
|
||||
self._loading = False
|
||||
return
|
||||
self.empty.hide()
|
||||
self.editor.show()
|
||||
self.title.setText(core.control_name(control))
|
||||
self.sub.setText(f"Action byte 0x{core.action_byte(control):02x}")
|
||||
|
||||
if entry.unknown:
|
||||
self.onpad.setText("Unknown")
|
||||
self.onpad.setStyleSheet(f"color: {INK_DIM.name()};")
|
||||
self.onpad_why.setText(entry.reason[:1].upper() + entry.reason[1:] + ".")
|
||||
self.onpad_why.show()
|
||||
else:
|
||||
self.onpad.setText(entry.binding.label())
|
||||
self.onpad.setStyleSheet("")
|
||||
self.onpad_why.hide()
|
||||
|
||||
shown = desired or (None if entry.unknown else entry.binding)
|
||||
if shown is not None and shown.media:
|
||||
self.mode_media.setChecked(True)
|
||||
self._page(1)
|
||||
self.media_buttons[shown.media].setChecked(True)
|
||||
self.keys_edit.clear()
|
||||
else:
|
||||
self.mode_keys.setChecked(True)
|
||||
self._page(0)
|
||||
self.keys_edit.setText(shown.keys if shown else "")
|
||||
self._clear_media()
|
||||
self.keys_hint.setText("")
|
||||
self.update_buttons(desired is not None, can_write)
|
||||
self._loading = False
|
||||
|
||||
def update_buttons(self, pending, can_write):
|
||||
self.write_btn.setEnabled(can_write and pending)
|
||||
self.revert_btn.setVisible(pending)
|
||||
|
||||
def _clear_media(self):
|
||||
self.media_group.setExclusive(False)
|
||||
for b in self.media_buttons.values():
|
||||
b.setChecked(False)
|
||||
self.media_group.setExclusive(True)
|
||||
|
||||
def _page(self, i):
|
||||
# Plain show/hide rather than QStackedWidget, which always reserves
|
||||
# the height of its tallest page.
|
||||
for j, page in enumerate(self.pages):
|
||||
page.setVisible(j == i)
|
||||
|
||||
def _mode(self, i):
|
||||
self._page(i)
|
||||
if i == 0 and self.record.isChecked():
|
||||
self.keys_edit.setFocus()
|
||||
|
||||
def _toggle_record(self, on):
|
||||
self.keys_edit.set_recording(on)
|
||||
self.keys_hint.setText(
|
||||
"Plasma keeps some combos for itself, like Meta shortcuts, so the "
|
||||
"app never sees them. Type those instead." if on else "")
|
||||
|
||||
def _keys_typed(self, text):
|
||||
if self.record.isChecked() and not self.keys_edit.recording:
|
||||
self.record.setChecked(False)
|
||||
if self._loading or self.control is None:
|
||||
return
|
||||
text = core.normalize_keys(text)
|
||||
if not text:
|
||||
self.keys_hint.setText("")
|
||||
self.changed.emit(self.control, None)
|
||||
return
|
||||
try:
|
||||
b = core.Binding(keys=text).validate()
|
||||
except ValueError as e:
|
||||
self.keys_hint.setText(f"<span style='color:{BAD}'>{e}</span>")
|
||||
self.changed.emit(self.control, None)
|
||||
return
|
||||
if b.is_sequence and not core.SEQUENCES_VERIFIED:
|
||||
self.keys_hint.setText(
|
||||
f"<span style='color:{BAD}'>Sequences aren't verified on this "
|
||||
"pad yet, so they're switched off for now. One shortcut per "
|
||||
"control.</span>")
|
||||
self.changed.emit(self.control, None)
|
||||
return
|
||||
self.keys_hint.setText(f"Sends {b.label()}")
|
||||
self.changed.emit(self.control, b)
|
||||
|
||||
def _media_picked(self, i):
|
||||
if self._loading or self.control is None:
|
||||
return
|
||||
self.changed.emit(self.control, core.Binding(media=core.MEDIA[i][0]))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ window
|
||||
|
||||
class Window(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("MacroPad")
|
||||
self.settings = QSettings("macropad", "macropad")
|
||||
self.state = core.State()
|
||||
self.desired = {} # control -> Binding, unwritten edits
|
||||
self.diag = None
|
||||
|
||||
root = QWidget()
|
||||
outer = QVBoxLayout(root)
|
||||
outer.setContentsMargins(0, 0, 0, 0)
|
||||
outer.setSpacing(0)
|
||||
|
||||
body = QHBoxLayout()
|
||||
body.setSpacing(0)
|
||||
self.pad = PadView()
|
||||
self.pad.set_orientation(self.settings.value("orientation", "upright"))
|
||||
self.inspector = Inspector()
|
||||
body.addWidget(self.pad, 1)
|
||||
body.addWidget(self.inspector)
|
||||
outer.addLayout(body, 1)
|
||||
|
||||
footer = QFrame()
|
||||
footer.setProperty("role", "footer")
|
||||
fl = QHBoxLayout(footer)
|
||||
fl.setContentsMargins(16, 8, 16, 8)
|
||||
self.dot = QLabel()
|
||||
self.dot.setFixedSize(9, 9)
|
||||
self.status = _label("", "dim")
|
||||
self.message = _label("")
|
||||
self.rotate = _button("Rotate view", "flat")
|
||||
self.import_btn = _button("Import capture…", "flat")
|
||||
self.write_all = _button("", "primary")
|
||||
fl.addWidget(self.dot)
|
||||
fl.addWidget(self.status)
|
||||
fl.addSpacing(16)
|
||||
fl.addWidget(self.message, 1)
|
||||
fl.addWidget(self.rotate)
|
||||
fl.addWidget(self.import_btn)
|
||||
fl.addWidget(self.write_all)
|
||||
outer.addWidget(footer)
|
||||
self.setCentralWidget(root)
|
||||
|
||||
self.pad.selected.connect(self._select)
|
||||
self.inspector.changed.connect(self._changed)
|
||||
self.inspector.revert.connect(self._revert)
|
||||
self.inspector.write.connect(lambda c: self._write([c]))
|
||||
self.write_all.clicked.connect(lambda: self._write(self._pending()))
|
||||
self.rotate.clicked.connect(self._rotate)
|
||||
self.import_btn.clicked.connect(self._import)
|
||||
|
||||
self.note = core._keyd_warning()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self._poll)
|
||||
self.timer.start(2000)
|
||||
self._poll()
|
||||
self._refresh()
|
||||
|
||||
# ------------------------------------------------------------- model
|
||||
|
||||
def _pending(self):
|
||||
out = []
|
||||
for c in core.CONTROL_IDS:
|
||||
b = self.desired.get(c)
|
||||
if b is None:
|
||||
continue
|
||||
e = self.state.get(c)
|
||||
if e.unknown or e.binding != b:
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
def _refresh(self, editor=True):
|
||||
pending = set(self._pending())
|
||||
looks = {}
|
||||
for c in core.CONTROL_IDS:
|
||||
e = self.state.get(c)
|
||||
if c in pending:
|
||||
looks[c] = Look(self.desired[c].label(), known=not e.unknown, pending=True)
|
||||
elif e.unknown:
|
||||
looks[c] = Look("", known=False)
|
||||
else:
|
||||
looks[c] = Look(e.binding.label(), known=True)
|
||||
self.pad.set_looks(looks)
|
||||
|
||||
n = len(pending)
|
||||
self.write_all.setText(f"Write {n} change{'s' if n != 1 else ''}")
|
||||
self.write_all.setVisible(n > 1)
|
||||
self.write_all.setEnabled(bool(self.diag and self.diag.ready))
|
||||
|
||||
c = self.pad.current
|
||||
can_write = bool(self.diag and self.diag.ready)
|
||||
if not editor:
|
||||
self.inspector.update_buttons(c in pending, can_write)
|
||||
return
|
||||
all_unknown = all(self.state.get(x).unknown for x in core.CONTROL_IDS)
|
||||
self.inspector.show_control(
|
||||
c, self.state.get(c) if c else None,
|
||||
self.desired.get(c) if c in pending else None,
|
||||
can_write, all_unknown)
|
||||
|
||||
def _select(self, control):
|
||||
self._refresh()
|
||||
|
||||
def _changed(self, control, binding):
|
||||
if binding is None:
|
||||
self.desired.pop(control, None)
|
||||
else:
|
||||
self.desired[control] = binding
|
||||
self._refresh(editor=False)
|
||||
|
||||
def _revert(self, control):
|
||||
self.desired.pop(control, None)
|
||||
self._refresh()
|
||||
|
||||
# ------------------------------------------------------------ device
|
||||
|
||||
def _poll(self):
|
||||
old = self.diag.status if self.diag else None
|
||||
try:
|
||||
self.diag = core.diagnose(keyd=False)
|
||||
except Exception as e: # never let polling kill the window
|
||||
self._say(f"Couldn't check the pad: {e}", BAD)
|
||||
return
|
||||
colour = {"ready": GOOD, "unplugged": INK_DIM.name()}.get(self.diag.status, BAD)
|
||||
self.dot.setStyleSheet(f"background: {colour}; border-radius: 4px;")
|
||||
self.status.setText({
|
||||
"ready": f"Pad ready on {self.diag.device.path}" if self.diag.device else "Pad ready",
|
||||
"unplugged": "No pad connected",
|
||||
"no-interface": "Pad found, config interface missing",
|
||||
"no-permission": "No permission to write to the pad",
|
||||
"unsupported": "Unsupported pad",
|
||||
}[self.diag.status])
|
||||
self.inspector.show_problem(self.diag, self.note)
|
||||
if self.diag.status != old:
|
||||
self._refresh(editor=False)
|
||||
|
||||
# ------------------------------------------------------------- write
|
||||
|
||||
def _write(self, controls):
|
||||
controls = [c for c in controls if c in self.desired]
|
||||
if not controls:
|
||||
return
|
||||
self._poll()
|
||||
if not self.diag.ready:
|
||||
self._say(self.diag.headline, BAD)
|
||||
return
|
||||
|
||||
done = []
|
||||
for c in controls:
|
||||
b = self.desired[c]
|
||||
try:
|
||||
core.write_binding(self.diag.device, c, b, self.state)
|
||||
except core.WriteError as e:
|
||||
what = core.control_name(c)
|
||||
tail = (f"{what} is now marked unknown, since part of it reached the pad."
|
||||
if e.pad_touched else "Nothing reached the pad.")
|
||||
why = str(e).split(": ", 1)[-1]
|
||||
self._say(f"Couldn't write {what}: {why}. {tail}", BAD)
|
||||
self._refresh()
|
||||
return
|
||||
self.desired.pop(c, None)
|
||||
done.append((c, b))
|
||||
QApplication.processEvents()
|
||||
|
||||
if len(done) == 1:
|
||||
c, b = done[0]
|
||||
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()
|
||||
|
||||
def _say(self, text, colour=None):
|
||||
self.message.setText(text)
|
||||
self.message.setStyleSheet(f"color: {colour};" if colour else "")
|
||||
|
||||
# ------------------------------------------------------------- misc
|
||||
|
||||
def _rotate(self):
|
||||
o = "flat" if self.pad.orientation == "upright" else "upright"
|
||||
self.pad.set_orientation(o)
|
||||
self.settings.setValue("orientation", o)
|
||||
|
||||
def _import(self):
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self, "Import a capture of the vendor software",
|
||||
str(Path.cwd()), "Captures (*.txt);;All files (*)")
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
got = core.import_capture(path, self.state)
|
||||
except (OSError, ValueError) as e:
|
||||
self._say(f"Couldn't read that capture: {e}", BAD)
|
||||
return
|
||||
if not got:
|
||||
self._say("No saved bindings found in that capture.", BAD)
|
||||
return
|
||||
for c, _, _ in got:
|
||||
self.desired.pop(c, None)
|
||||
self._say(f"Imported {len(got)} bindings from {Path(path).name}.", GOOD)
|
||||
self._refresh()
|
||||
|
||||
def closeEvent(self, e):
|
||||
n = len(self._pending())
|
||||
if not n:
|
||||
e.accept()
|
||||
return
|
||||
box = QMessageBox(self)
|
||||
box.setWindowTitle("Unwritten changes")
|
||||
box.setText(f"{n} change{'s are' if n != 1 else ' is'} not on the pad yet.")
|
||||
write = box.addButton("Write and close", QMessageBox.AcceptRole)
|
||||
discard = box.addButton("Close without writing", QMessageBox.DestructiveRole)
|
||||
box.addButton("Cancel", QMessageBox.RejectRole)
|
||||
box.exec()
|
||||
if box.clickedButton() is write:
|
||||
self._write(self._pending())
|
||||
e.accept() if not self._pending() else e.ignore()
|
||||
elif box.clickedButton() is discard:
|
||||
e.accept()
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
|
||||
def apply_theme(app):
|
||||
app.setStyle("Fusion")
|
||||
pal = QPalette()
|
||||
for role, colour in ((QPalette.Window, WINDOW), (QPalette.Base, QColor(FIELD)),
|
||||
(QPalette.Text, INK), (QPalette.WindowText, INK),
|
||||
(QPalette.Button, WINDOW), (QPalette.ButtonText, INK),
|
||||
(QPalette.Highlight, TAPE), (QPalette.HighlightedText, QColor("#1b1206"))):
|
||||
pal.setColor(role, colour)
|
||||
app.setPalette(pal)
|
||||
app.setStyleSheet(STYLE)
|
||||
|
||||
|
||||
def run():
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("macropad")
|
||||
app.setDesktopFileName("macropad")
|
||||
apply_theme(app)
|
||||
w = Window()
|
||||
w.resize(1000, 800)
|
||||
w.show()
|
||||
return app.exec()
|
||||
Reference in New Issue
Block a user