add keyboard rendering

main
Ilya Zhuravlev 2020-10-14 12:56:25 -04:00
parent cbe3a2dd9e
commit 8f1123ee77
7 changed files with 685 additions and 18 deletions

130
g60.json 100644
View File

@ -0,0 +1,130 @@
{
"name": "g60",
"vendorId": "0xFEED",
"productId": "0x6464",
"lighting": "none",
"matrix": {
"rows": 5,
"cols": 15
},
"layouts": {
"keymap": [
[
"0,0",
"0,1",
"0,2",
"0,3",
"0,4",
"0,5",
"0,6",
"0,7",
"0,8",
"0,9",
"0,10",
"0,11",
"0,12",
"0,13",
"0,14"
],
[
{
"w": 1.5
},
"1,0",
"1,2",
"1,3",
"1,4",
"1,5",
"1,6",
"1,7",
"1,8",
"1,9",
"1,10",
"1,11",
"1,12",
"1,13",
{
"w": 1.5
},
"1,14"
],
[
{
"w": 1.75
},
"2,0",
"2,2",
"2,3",
"2,4",
"2,5",
"2,6",
"2,7",
"2,8",
"2,9",
"2,10",
"2,11",
"2,12",
{
"w": 2.25
},
"2,13"
],
[
{
"w": 1.25
},
"3,0",
"3,1",
"3,2",
"3,3",
"3,4",
"3,5",
"3,6",
"3,7",
"3,8",
"3,9",
"3,10",
"3,11",
{
"w": 1.75
},
"3,13",
"3,14"
],
[
{
"w": 1.25
},
"4,0",
{
"w": 1.25
},
"4,1",
{
"w": 1.25
},
"4,3",
{
"w": 6.25
},
"4,6",
{
"w": 1.25
},
"4,10",
{
"w": 1.25
},
"4,11",
{
"w": 1.25
},
"4,13",
{
"w": 1.25
},
"4,14"
]
]
}
}

301
plain60.json 100644
View File

@ -0,0 +1,301 @@
{
"name": "plain60",
"vendorId": "0x4705",
"productId": "0x0160",
"lighting": "none",
"matrix": {
"rows": 5,
"cols": 15
},
"layouts": {
"labels": [
[
"Enter Key",
"ANSI",
"ISO"
],
"Split Left Shift",
"Split Right Shift",
"Split Backspace",
[
"Bottom row",
"Default",
"Tsangan",
"WKL",
"HHKB"
]
],
"keymap": [
[
{
"x": 2.75,
"c": "#777777"
},
"0,0",
{
"c": "#cccccc"
},
"0,1",
"0,2",
"0,3",
"0,4",
"0,5",
"0,6",
"0,7",
"0,8",
"0,9",
"0,10",
"0,11",
"0,12",
{
"c": "#aaaaaa",
"w": 2
},
"0,14\n\n\n3,0",
{
"x": 0.5,
"c": "#cccccc"
},
"0,13\n\n\n3,1",
"0,14\n\n\n3,1"
],
[
{
"x": 2.75,
"c": "#aaaaaa",
"w": 1.5
},
"1,0",
{
"c": "#cccccc"
},
"1,1",
"1,2",
"1,3",
"1,4",
"1,5",
"1,6",
"1,7",
"1,8",
"1,9",
"1,10",
"1,11",
"1,12",
{
"w": 1.5
},
"1,13\n\n\n0,0",
{
"x": 1.5,
"c": "#777777",
"w": 1.25,
"h": 2,
"w2": 1.5,
"h2": 1,
"x2": -0.25
},
"2,13\n\n\n0,1"
],
[
{
"x": 2.75,
"c": "#aaaaaa",
"w": 1.75
},
"2,0",
{
"c": "#cccccc"
},
"2,1",
"2,2",
"2,3",
"2,4",
"2,5",
"2,6",
"2,7",
"2,8",
"2,09",
"2,10",
"2,11",
{
"c": "#777777",
"w": 2.25
},
"2,13\n\n\n0,0",
{
"x": 0.5,
"c": "#cccccc"
},
"2,12\n\n\n0,1"
],
[
{
"c": "#aaaaaa",
"w": 1.25
},
"3,0\n\n\n1,1",
{
"c": "#cccccc"
},
"3,1\n\n\n1,1",
{
"x": 0.5,
"c": "#aaaaaa",
"w": 2.25
},
"3,0\n\n\n1,0",
{
"c": "#cccccc"
},
"3,2",
"3,3",
"3,4",
"3,5",
"3,6",
"3,7",
"3,8",
"3,9",
"3,10",
"3,11",
{
"c": "#aaaaaa",
"w": 2.75
},
"3,12\n\n\n2,0",
{
"x": 0.5,
"w": 1.75
},
"3,12\n\n\n2,1",
"3,13\n\n\n2,1"
],
[
{
"x": 2.75,
"w": 1.25
},
"4,0\n\n\n4,0",
{
"w": 1.25
},
"4,1\n\n\n4,0",
{
"w": 1.25
},
"4,2\n\n\n4,0",
{
"c": "#cccccc",
"w": 6.25
},
"4,6\n\n\n4,0",
{
"c": "#aaaaaa",
"w": 1.25
},
"4,10\n\n\n4,0",
{
"w": 1.25
},
"4,11\n\n\n4,0",
{
"w": 1.25
},
"4,12\n\n\n4,0",
{
"w": 1.25
},
"4,13\n\n\n4,0"
],
[
{
"y": 0.25,
"x": 2.75,
"w": 1.5
},
"4,0\n\n\n4,1",
"4,1\n\n\n4,1",
{
"w": 1.5
},
"4,2\n\n\n4,1",
{
"c": "#cccccc",
"w": 7
},
"4,6\n\n\n4,1",
{
"c": "#aaaaaa",
"w": 1.5
},
"4,11\n\n\n4,1",
"4,12\n\n\n4,1",
{
"w": 1.5
},
"4,13\n\n\n4,1"
],
[
{
"x": 2.75,
"w": 1.5
},
"4,0\n\n\n4,2",
{
"d": true
},
"\n\n\n4,2",
{
"w": 1.5
},
"4,2\n\n\n4,2",
{
"c": "#cccccc",
"w": 7
},
"4,6\n\n\n4,2",
{
"c": "#aaaaaa",
"w": 1.5
},
"4,11\n\n\n4,2",
{
"d": true
},
"\n\n\n4,2",
{
"w": 1.5
},
"4,13\n\n\n4,2"
],
[
{
"x": 2.75,
"w": 1.5,
"d": true
},
"\n\n\n4,3",
"4,1\n\n\n4,3",
{
"w": 1.5
},
"4,2\n\n\n4,3",
{
"c": "#cccccc",
"w": 7
},
"4,6\n\n\n4,3",
{
"c": "#aaaaaa",
"w": 1.5
},
"4,11\n\n\n4,3",
"4,12\n\n\n4,3",
{
"w": 1.5,
"d": true
},
"\n\n\n4,3"
]
]
}
}

View File

@ -0,0 +1,7 @@
import hid
VIAL_SERIAL_NUMBER_MAGIC = "vial:f64c2b3c"
def find_vial_keyboards():
for dev in hid.enumerate():
print(dev)

View File

@ -0,0 +1,11 @@
REPORT_LEN = 32
def hid_send(dev, msg):
if len(report) > REPORT_LEN:
raise RuntimeError("report must be less than 64 bytes")
msg += b"\x00" * (REPORT_LEN - len(msg))
# add 00 at start for hidapi report id
dev.write(b"\x00" + report)
return dev.read(REPORT_LEN)

View File

@ -0,0 +1,202 @@
# Based on https://github.com/ijprest/kle-serial
import json
from copy import copy
class KeyDefaults:
def __init__(self):
self.textColor = "#000000"
self.textSize = 3
class Key:
def __init__(self):
self.color = "#cccccc"
self.labels = []
self.textColor = [None] * 12
self.textSize = []
self.default = KeyDefaults()
self.x = 0
self.y = 0
self.width = 1
self.height = 1
self.x2 = 0
self.y2 = 0
self.width2 = 1
self.height2 = 1
self.rotation_x = 0
self.rotation_y = 0
self.rotation_angle = 0
self.decal = False
self.ghost = False
self.stepped = False
self.nub = False
self.profile = ""
self.sm = ""
self.sb = ""
self.st = ""
class KeyboardMetadata:
def __init__(self):
self.author = ""
self.backcolor = "#eeeeee"
self.background = None
self.name = ""
self.notes = ""
self.radii = ""
self.switchBrand = ""
self.switchMount = ""
self.switchType = ""
class Keyboard:
def __init__(self):
self.meta = KeyboardMetadata()
self.keys = []
class Serial:
labelMap = [
# 0 1 2 3 4 5 6 7 8 9 10 11 # align flags
[ 0, 6, 2, 8, 9,11, 3, 5, 1, 4, 7,10], # 0 = no centering
[ 1, 7,-1,-1, 9,11, 4,-1,-1,-1,-1,10], # 1 = center x
[ 3,-1, 5,-1, 9,11,-1,-1, 4,-1,-1,10], # 2 = center y
[ 4,-1,-1,-1, 9,11,-1,-1,-1,-1,-1,10], # 3 = center x & y
[ 0, 6, 2, 8,10,-1, 3, 5, 1, 4, 7,-1], # 4 = center front (default)
[ 1, 7,-1,-1,10,-1, 4,-1,-1,-1,-1,-1], # 5 = center front & x
[ 3,-1, 5,-1,10,-1,-1,-1, 4,-1,-1,-1], # 6 = center front & y
[ 4,-1,-1,-1,10,-1,-1,-1,-1,-1,-1,-1], # 7 = center front & x & y
]
def reorderLabelsIn(self, labels, align):
ret = [None] * 12
for i in range(len(labels)):
if labels[i]:
ret[self.labelMap[align][i]] = labels[i]
return ret
def deserializeError(self, msg, data):
raise RuntimeError("Error: {} {}".format(msg, data))
def deserialize(self, rows):
current = Key()
kbd = Keyboard()
align = 4
for r in range(len(rows)):
if isinstance(rows[r], list):
for k in range(len(rows[r])):
item = rows[r][k]
if isinstance(item, str):
newKey = copy(current)
# Calculate some generated values
newKey.width2 = current.width if newKey.width2 == 0 else current.width2
newKey.height2 = current.height if newKey.height2 == 0 else current.height2
newKey.labels = self.reorderLabelsIn(item.split("\n"), align)
newKey.textSize = self.reorderLabelsIn(newKey.textSize, align)
# Clean up the data
for i in range(12):
if newKey.labels[i] is None:
newKey.textSize[i] = newKey.textColor[i] = None
if newKey.textSize[i] == newKey.default.textSize:
newKey.textSize[i] = None
if newKey.textColor[i] == newKey.default.textColor:
newKey.textColor[i] = None
# Add the key!
kbd.keys.append(newKey)
# Set up for the next key
current.x += current.width
current.width = current.height = 1
current.x2 = current.y2 = current.width2 = current.height2 = 0
current.nub = current.stepped = current.decal = False
else:
if k != 0 and ("r" in item or "rx" in item or "ry" in item):
self.deserializeError("rotation can only be specified on the first key in a row", item)
if "r" in item:
current.rotation_angle = item["r"]
if "rx" in item:
current.rotation_x = item["rx"]
if "ry" in item:
current.rotation_y = item["ry"]
if "a" in item:
align = item["a"]
if "f" in item:
current.default.textSize = item["f"]
current.textSize = []
if "f2" in item:
for i in range(1, 12):
current.textSize[i] = item["f2"]
if "fa" in item:
current.textSize = item["fa"]
if "p" in item:
current.profile = item["p"]
if "c" in item:
current.color = item["c"]
if "t" in item:
split = item["t"].split("\n")
if split[0] != "":
current.default.textColor = split[0]
current.textColor = self.reorderLabelsIn(split, align)
if "x" in item:
current.x += item["x"]
if "y" in item:
current.y += item["y"]
if "w" in item:
current.width = current.width2 = item["w"]
if "h" in item:
current.height = current.height2 = item["h"]
if "x2" in item:
current.x2 = item["x2"]
if "y2" in item:
current.y2 = item["y2"]
if "w2" in item:
current.width2 = item["w2"]
if "h2" in item:
current.height2 = item["h2"]
if "n" in item:
current.nub = item["n"]
if "l" in item:
current.stepped = item["l"]
if "d" in item:
current.decal = item["d"]
if "g" in item and item["g"]:
current.ghost = item.g
if "sm" in item:
current.sm = item["sm"]
if "sb" in item:
current.sb = item["sb"]
if "st" in item:
current.st = item["st"]
# End of the row
current.y += 1
current.x = current.rotation_x
elif isinstance(item, dict):
if r != 0:
self.deserializeError("keyboard metadata must the be first element", rows[r])
# TODO: parse prop
else:
self.deserializeError("unexpected", rows[r])
return kbd
# TODO: add tests
# serial = Serial()
# data = open("plain60.json", "r").read()
# data = json.loads(data)
# kb = serial.deserialize(data["layouts"]["keymap"])
# for key in kb.keys:
# print("{} {}x{}+{}x{}".format(key.labels, key.x, key.y, key.width, key.height))

View File

@ -3,8 +3,11 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QTabWidget, QVBoxLayout, QPushButton, QLabel from PyQt5.QtWidgets import QWidget, QTabWidget, QVBoxLayout, QPushButton, QLabel
import sys import sys
import json
from flowlayout import FlowLayout from flowlayout import FlowLayout
from util import tr
from kle_serial import Serial as KleSerial
class TabbedKeycodes(QTabWidget): class TabbedKeycodes(QTabWidget):
@ -13,7 +16,7 @@ class TabbedKeycodes(QTabWidget):
self.tab_basic = QWidget() self.tab_basic = QWidget()
layout = FlowLayout() layout = FlowLayout()
for lbl in ["", "TODO", "Esc", "A", "B", "C", "D", "E", "F"]: for lbl in ["", "hello", "Esc", "A", "B", "C", "D", "E", "F"]:
btn = QPushButton(lbl) btn = QPushButton(lbl)
btn.setFixedSize(50, 50) btn.setFixedSize(50, 50)
layout.addWidget(btn) layout.addWidget(btn)
@ -22,14 +25,14 @@ class TabbedKeycodes(QTabWidget):
self.tab_media = QWidget() self.tab_media = QWidget()
self.tab_macro = QWidget() self.tab_macro = QWidget()
self.addTab(self.tab_basic, "Basic") self.addTab(self.tab_basic, tr("TabbedKeycodes", "Basic"))
self.addTab(self.tab_media, "Media") self.addTab(self.tab_media, tr("TabbedKeycodes", "Media"))
self.addTab(self.tab_macro, "Macro") self.addTab(self.tab_macro, tr("TabbedKeycodes", "Macro"))
KEY_WIDTH = 40 KEY_WIDTH = 40
KEY_HEIGHT = KEY_WIDTH KEY_HEIGHT = KEY_WIDTH
KEY_SPACING = 10 KEY_SPACING = 4
class KeyboardContainer(QWidget): class KeyboardContainer(QWidget):
@ -37,23 +40,32 @@ class KeyboardContainer(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setFixedSize(300, 300) serial = KleSerial()
data = open("g60.json", "r").read()
data = json.loads(data)
kb = serial.deserialize(data["layouts"]["keymap"])
for x, btn in enumerate(["Q", "W", "E", "R"]): max_w = max_h = 0
widget = QLabel(btn)
widget.setParent(self)
widget.setStyleSheet('background-color:red;')
widget.setAlignment(Qt.AlignCenter)
widget.setFixedSize(KEY_WIDTH, KEY_HEIGHT)
widget.move((KEY_WIDTH + KEY_SPACING) * x, 0)
for x, btn in enumerate(["A", "S", "D", "F"]): for key in kb.keys:
widget = QLabel(btn) widget = QLabel(str(key.labels[0]))
widget.setParent(self) widget.setParent(self)
widget.setStyleSheet('background-color:red;') widget.setStyleSheet('background-color:white; border: 1px solid black')
widget.setAlignment(Qt.AlignCenter) widget.setAlignment(Qt.AlignCenter)
widget.setFixedSize(KEY_WIDTH, KEY_HEIGHT)
widget.move((KEY_WIDTH + KEY_SPACING) * (x + 0.25), KEY_HEIGHT + KEY_SPACING) x = (KEY_WIDTH + KEY_SPACING) * key.x
y = (KEY_HEIGHT + KEY_SPACING) * key.y
w = (KEY_WIDTH + KEY_SPACING) * key.width - KEY_SPACING
h = (KEY_HEIGHT + KEY_SPACING) * key.height - KEY_SPACING
widget.setFixedSize(w, h)
widget.move(x, y)
print("{} {}x{}+{}x{}".format(key.labels, key.x, key.y, key.width, key.height))
max_w = max(max_w, x + w)
max_h = max(max_h, y + h)
self.setFixedSize(max_w, max_h)
class MainWindow(QWidget): class MainWindow(QWidget):
@ -67,6 +79,7 @@ class MainWindow(QWidget):
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(self.keyboard_container) layout.addWidget(self.keyboard_container)
layout.setAlignment(self.keyboard_container, Qt.AlignHCenter)
layout.addWidget(self.tabbed_keycodes) layout.addWidget(self.tabbed_keycodes)
self.setLayout(layout) self.setLayout(layout)

View File

@ -0,0 +1,3 @@
from PyQt5.QtCore import QCoreApplication
tr = QCoreApplication.translate