Add files via upload
This commit is contained in:
347
docs/make_screenshots.py
Normal file
347
docs/make_screenshots.py
Normal file
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Regenerate every image in docs/images from the real app, offscreen.
|
||||
|
||||
QT_QPA_PLATFORM=offscreen QT_SCALE_FACTOR=2 python3 docs/make_screenshots.py
|
||||
|
||||
Uses a fake pad and a throwaway config folder, so it never touches your real
|
||||
pad or your saved state. Looks best with Noto Sans and Hack installed, which
|
||||
are Plasma's defaults.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OUT = ROOT / "docs" / "images"
|
||||
sys.path.insert(0, str(ROOT))
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
os.environ["XDG_CONFIG_HOME"] = tempfile.mkdtemp()
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt # noqa: E402
|
||||
from PySide6.QtGui import (QColor, QFont, QFontDatabase, QImage, # noqa: E402
|
||||
QLinearGradient, QPainter, QPen)
|
||||
from PySide6.QtSvg import QSvgRenderer # noqa: E402
|
||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from macropad_gui import app as A, core, padview # noqa: E402
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
except ImportError:
|
||||
Image = None
|
||||
|
||||
B = core.Binding
|
||||
DEMO = {
|
||||
"key4": B(keys="super+ctrl+left"), "key8": B(keys="super+w"),
|
||||
"key12": B(keys="super+ctrl+right"),
|
||||
"key3": B(media="prev"), "key7": B(media="playpause"), "key11": B(media="next"),
|
||||
"key2": B(keys="ctrl+z"), "key6": B(keys="ctrl+shift+z"), "key10": B(keys="ctrl+s"),
|
||||
"key1": B(keys="printscreen"), "key5": B(keys="ctrl+alt+t"), "key9": B(media="mycomputer"),
|
||||
"dial1-left": B(media="volumedown"), "dial1-push": B(media="mute"),
|
||||
"dial1-right": B(media="volumeup"),
|
||||
"dial2-left": B(media="brightnessdown"), "dial2-push": B(media="calculator"),
|
||||
"dial2-right": B(media="brightnessup"),
|
||||
}
|
||||
|
||||
READY = core.Diagnosis("ready", "Ready on /dev/hidraw10.",
|
||||
device=core.Device("/dev/hidraw10", 3, True))
|
||||
|
||||
|
||||
def fonts(app):
|
||||
ui = QFont("Noto Sans", 10)
|
||||
if "Noto Sans" in QFontDatabase.families():
|
||||
app.setFont(ui)
|
||||
if "Hack" in QFontDatabase.families():
|
||||
hack = QFont("Hack", 10)
|
||||
real = QFontDatabase.systemFont
|
||||
QFontDatabase.systemFont = staticmethod(
|
||||
lambda which: hack if which == QFontDatabase.FixedFont else real(which))
|
||||
|
||||
|
||||
def window(diag=READY, bindings=None, note=None):
|
||||
with mock.patch.object(core, "_keyd_warning", return_value=note), \
|
||||
mock.patch.object(core, "diagnose", return_value=diag):
|
||||
w = A.Window()
|
||||
w.timer.stop()
|
||||
w.diag = diag
|
||||
w.note = note
|
||||
w.state = core.State(Path(tempfile.mkdtemp()) / "state.json")
|
||||
for c, b in (bindings or {}).items():
|
||||
w.state.record(c, b)
|
||||
w.resize(1000, 800)
|
||||
w._poll_fake = diag
|
||||
w.inspector.show_problem(diag, note)
|
||||
status_colour = {"ready": A.GOOD, "unplugged": padview.INK_DIM.name()}.get(diag.status, A.BAD)
|
||||
w.dot.setStyleSheet(f"background: {status_colour}; border-radius: 4px;")
|
||||
w.status.setText({"ready": "Pad ready on /dev/hidraw10",
|
||||
"unplugged": "No pad connected",
|
||||
"no-permission": "No permission to write to the pad",
|
||||
"unsupported": "Unsupported pad"}[diag.status])
|
||||
w._refresh()
|
||||
return w
|
||||
|
||||
|
||||
def select(w, control):
|
||||
w.pad.select(control)
|
||||
w._refresh()
|
||||
|
||||
|
||||
def save(w, name, frame=True):
|
||||
w.show()
|
||||
QApplication.processEvents()
|
||||
img = w.grab().toImage()
|
||||
path = OUT / f"{name}.png"
|
||||
img.save(str(path))
|
||||
if frame and Image:
|
||||
_frame(path)
|
||||
w.desired.clear()
|
||||
w.close()
|
||||
print("wrote", path.relative_to(ROOT))
|
||||
|
||||
|
||||
def _frame(path, radius=20, pad=56):
|
||||
"""Rounded corners, a hairline edge and a soft shadow, on transparency."""
|
||||
shot = Image.open(path).convert("RGBA")
|
||||
w, h = shot.size
|
||||
mask = Image.new("L", (w, h), 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle([0, 0, w - 1, h - 1], radius, fill=255)
|
||||
shot.putalpha(mask)
|
||||
edge = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(edge).rounded_rectangle([0, 0, w - 1, h - 1], radius,
|
||||
outline=(255, 255, 255, 34), width=2)
|
||||
shot = Image.alpha_composite(shot, edge)
|
||||
|
||||
canvas = Image.new("RGBA", (w + 2 * pad, h + 2 * pad), (0, 0, 0, 0))
|
||||
shadow = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
||||
ImageDraw.Draw(shadow).rounded_rectangle(
|
||||
[pad, pad + 14, pad + w, pad + h + 14], radius, fill=(0, 0, 0, 120))
|
||||
shadow = shadow.filter(ImageFilter.GaussianBlur(22))
|
||||
canvas = Image.alpha_composite(canvas, shadow)
|
||||
canvas.alpha_composite(shot, (pad, pad))
|
||||
canvas.save(path, optimize=True)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ scenes
|
||||
|
||||
def scene_hero():
|
||||
w = window(bindings=DEMO)
|
||||
select(w, "key12")
|
||||
w._say("Wrote 3 changes to the pad.", A.GOOD)
|
||||
save(w, "hero")
|
||||
|
||||
|
||||
def scene_first_launch():
|
||||
w = window()
|
||||
save(w, "first-launch")
|
||||
|
||||
|
||||
def scene_select_and_type():
|
||||
w = window(bindings={k: v for k, v in DEMO.items() if k.startswith("dial")})
|
||||
select(w, "key2")
|
||||
w.inspector.keys_edit.setText("ctrl+z")
|
||||
w.inspector._keys_typed("ctrl+z")
|
||||
save(w, "step-type")
|
||||
|
||||
|
||||
def scene_record():
|
||||
w = window(bindings={k: v for k, v in DEMO.items() if k.startswith("dial")})
|
||||
select(w, "key6")
|
||||
w.inspector.record.setChecked(True)
|
||||
w.inspector.keys_edit.setText("ctrl+shift+…")
|
||||
save(w, "step-record")
|
||||
|
||||
|
||||
def scene_media():
|
||||
w = window(bindings={k: v for k, v in DEMO.items() if k.startswith("key")})
|
||||
select(w, "dial1-left")
|
||||
w.inspector.mode_media.click()
|
||||
w.inspector.media_buttons["volumedown"].click()
|
||||
save(w, "step-media")
|
||||
|
||||
|
||||
def scene_pending():
|
||||
w = window(bindings={k: v for k, v in DEMO.items()
|
||||
if k not in ("key2", "key6", "key10")})
|
||||
for k in ("key2", "key6", "key10"):
|
||||
w.desired[k] = DEMO[k]
|
||||
select(w, "key10")
|
||||
w.inspector.keys_hint.setText("Sends Ctrl+S")
|
||||
w._refresh(editor=False)
|
||||
save(w, "step-pending")
|
||||
|
||||
|
||||
def scene_written():
|
||||
w = window(bindings=DEMO)
|
||||
select(w, "key10")
|
||||
w._say("Wrote 3 changes to the pad.", A.GOOD)
|
||||
save(w, "step-written")
|
||||
|
||||
|
||||
def scene_flat():
|
||||
w = window(bindings=DEMO)
|
||||
w.pad.set_orientation("flat")
|
||||
select(w, "dial2-push")
|
||||
save(w, "flat-view")
|
||||
|
||||
|
||||
def scene_permission():
|
||||
d = core.Diagnosis(
|
||||
"no-permission", "/dev/hidraw10 isn't writable.",
|
||||
device=core.Device("/dev/hidraw10", 3, False),
|
||||
detail=["No udev rule lets you write to the pad yet. Running the installer "
|
||||
"again adds it, or paste these commands into a terminal:"],
|
||||
fix=[f"echo '{core.RULE_LINE}' | sudo tee /etc/udev/rules.d/60-macropad.rules",
|
||||
"sudo udevadm control --reload-rules && sudo udevadm trigger",
|
||||
"# then unplug and replug the pad"])
|
||||
w = window(diag=d, bindings=DEMO)
|
||||
select(w, "key4")
|
||||
save(w, "permission")
|
||||
|
||||
|
||||
def scene_keyd():
|
||||
note = ("keyd, a key remapping service, is running and /etc/keyd/default.conf "
|
||||
"lists this pad. It can change keys on their way from the pad to your "
|
||||
"desktop, so a key may not do exactly what you set here. If you only "
|
||||
"set keyd up for this pad, you can switch it off with: "
|
||||
"sudo systemctl disable --now keyd")
|
||||
w = window(bindings=DEMO, note=note)
|
||||
save(w, "keyd-note")
|
||||
|
||||
|
||||
def scene_unsupported():
|
||||
d = core.Diagnosis(
|
||||
"unsupported", "Found a 1189:8890 pad, which this app doesn't know yet.",
|
||||
detail=["So far only 1189:8840 (12 keys, 2 dials) has been 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."])
|
||||
w = window(diag=d)
|
||||
save(w, "unsupported")
|
||||
|
||||
|
||||
def scene_legend():
|
||||
"""Four keycaps showing every state the drawing uses."""
|
||||
scale = 2
|
||||
names = [("Written", "on the pad", padview.Look("Ctrl+C", known=True), None),
|
||||
("Unknown", "could be anything", padview.Look(), None),
|
||||
("Changed", "not written yet", padview.Look("Mute", known=True, pending=True), None),
|
||||
("Selected", "being edited", padview.Look("Ctrl+S", known=True), "sel")]
|
||||
cell, gap, top = 132, 26, 24
|
||||
width = len(names) * cell + (len(names) - 1) * gap + 2 * 30
|
||||
img = QImage(width * scale, 210 * scale, QImage.Format_ARGB32)
|
||||
img.fill(padview.BOARD)
|
||||
p = QPainter(img)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
p.scale(scale, scale)
|
||||
pv = padview.PadView()
|
||||
for i, (title, sub, look, sel) in enumerate(names):
|
||||
x = 30 + i * (cell + gap)
|
||||
rect = QRectF(x + (cell - 96) / 2, top, 96, 96)
|
||||
pv.looks = {f"key{i + 1}": look}
|
||||
pv.current = f"key{i + 1}" if sel else None
|
||||
pv._paint_key(p, f"key{i + 1}", rect)
|
||||
f = QFont(QApplication.font())
|
||||
f.setPixelSize(15)
|
||||
f.setWeight(QFont.DemiBold)
|
||||
p.setFont(f)
|
||||
p.setPen(padview.TAPE if look.pending or sel else padview.INK)
|
||||
p.drawText(QRectF(x, top + 110, cell, 22), Qt.AlignHCenter, title)
|
||||
f.setWeight(QFont.Normal)
|
||||
f.setPixelSize(12)
|
||||
p.setFont(f)
|
||||
p.setPen(padview.INK_DIM)
|
||||
p.drawText(QRectF(x, top + 134, cell, 20), Qt.AlignHCenter, sub)
|
||||
p.end()
|
||||
path = OUT / "legend.png"
|
||||
img.save(str(path))
|
||||
if Image:
|
||||
_frame(path, radius=24, pad=40)
|
||||
print("wrote", path.relative_to(ROOT))
|
||||
|
||||
|
||||
def scene_social():
|
||||
"""1280x640: GitHub's social preview, which Reddit and Discord show for links."""
|
||||
W, H = 1280, 640
|
||||
img = QImage(W, H, QImage.Format_ARGB32)
|
||||
img.fill(padview.WINDOW)
|
||||
p = QPainter(img)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
# faint board-grid, like a PCB's copper pour
|
||||
p.setPen(QPen(QColor(255, 255, 255, 10), 1))
|
||||
for x in range(0, W, 32):
|
||||
p.drawLine(x, 0, x, H)
|
||||
for y in range(0, H, 32):
|
||||
p.drawLine(0, y, W, y)
|
||||
|
||||
pv = padview.PadView()
|
||||
pv.resize(460, 640)
|
||||
pv.looks = {c: padview.Look(b.label(), known=True) for c, b in DEMO.items()}
|
||||
pv.looks["key12"] = padview.Look("Meta+Ctrl+Right", known=True, pending=True)
|
||||
pv.current = "key12"
|
||||
p.save()
|
||||
p.translate(70, 0)
|
||||
p.setTransform(pv._transform(), True)
|
||||
board = QRectF(pv.board)
|
||||
path = __import__("PySide6.QtGui", fromlist=["QPainterPath"]).QPainterPath()
|
||||
path.addRoundedRect(board, 18, 18)
|
||||
p.fillPath(path, padview.BOARD)
|
||||
p.setPen(QPen(padview.INK_DIM, 1.2))
|
||||
p.drawPath(path)
|
||||
for key, rect in pv.keys.items():
|
||||
pv._paint_key(p, key, rect)
|
||||
for dial, centre in pv.dials.items():
|
||||
pv._paint_dial(p, dial, centre)
|
||||
p.restore()
|
||||
|
||||
x0 = 600
|
||||
f = QFont(QApplication.font())
|
||||
f.setPixelSize(92)
|
||||
f.setWeight(QFont.Bold)
|
||||
p.setFont(f)
|
||||
p.setPen(padview.INK)
|
||||
p.drawText(QRectF(x0, 150, 640, 110), Qt.AlignLeft | Qt.AlignVCenter, "MacroPad")
|
||||
p.fillRect(QRectF(x0 + 4, 268, 120, 6), padview.TAPE)
|
||||
f.setPixelSize(34)
|
||||
f.setWeight(QFont.Normal)
|
||||
p.setFont(f)
|
||||
p.drawText(QRectF(x0, 300, 640, 140), Qt.AlignLeft | Qt.TextWordWrap,
|
||||
"Set up your 12 key, 2 knob USB macro pad on Linux.")
|
||||
f.setPixelSize(26)
|
||||
p.setFont(f)
|
||||
p.setPen(padview.INK_DIM)
|
||||
p.drawText(QRectF(x0, 430, 640, 40), Qt.AlignLeft, "No Windows. No VM. No Wine.")
|
||||
mono = QFontDatabase.systemFont(QFontDatabase.FixedFont)
|
||||
mono.setPixelSize(22)
|
||||
p.setFont(mono)
|
||||
p.drawText(QRectF(x0, 540, 640, 30), Qt.AlignLeft, "github.com/Probler-Yt/MacroPad")
|
||||
p.end()
|
||||
img.save(str(OUT / "social-preview.png"))
|
||||
print("wrote docs/images/social-preview.png")
|
||||
|
||||
|
||||
def scene_icon():
|
||||
r = QSvgRenderer(str(ROOT / "packaging" / "macropad.svg"))
|
||||
img = QImage(256, 256, QImage.Format_ARGB32)
|
||||
img.fill(Qt.transparent)
|
||||
p = QPainter(img)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
r.render(p, QRectF(0, 0, 256, 256))
|
||||
p.end()
|
||||
img.save(str(OUT / "icon.png"))
|
||||
print("wrote docs/images/icon.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
app = QApplication(sys.argv)
|
||||
fonts(app)
|
||||
A.apply_theme(app)
|
||||
for name, fn in list(globals().items()):
|
||||
if name.startswith("scene_"):
|
||||
fn()
|
||||
Reference in New Issue
Block a user