1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// 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.
import QtQuick
// The macropad as it sits on the desk: twelve keys in three rows of four,
// the two encoders to their right. Each key carries its keycap's legends;
// hovering a key or an encoder lists everything it does underneath. The
// last one hovered stays listed, so moving the pointer off does not blank it.
Item {
id: panel
// Binds.padActions(): "k1".."k12", "top", "bottom" to [{ how, label }].
property var actions: ({})
property string hovered: ""
// Keys are unit wide; the pad is four of them, three gaps, a gap and an
// encoder: 5.56 units, fitted to most of the panel's width.
readonly property int unit: Math.floor(width * 0.85 / 5.56)
readonly property int padWidth: Math.round(unit * 5.56)
// The keycaps, left to right and top to bottom, as printed: icon and
// text on top, the shifted (or Alt) legend below, in the palette's
// colours nearest the real caps: orange is peach, purple is mauve.
// Glyphs are checked by name in Inconsolata Nerd Font (see AGENTS.md,
// Theme).
readonly property var caps: [
{ icon: "\u{F0239}", sub: "\u{F05F9}", cap: Theme.red, ink: Theme.base }, // firefox / incognito
{ icon: "\u{F0174}", sub: "\u{F0354}", cap: Theme.teal, ink: Theme.base }, // code_tags / markdown
{ icon: "\u{F338}", cap: Theme.yellow, ink: Theme.base }, // gimp
{ icon: "\u{F0147}", sub: "\u{F0432}", cap: Theme.mauve, ink: Theme.base }, // clipboard / qrcode
{ icon: "\u{F018D}", text: "SSH", cap: Theme.crust, ink: Theme.green }, // console
{ icon: "\u{F0BCC}", text: "split", cap: Theme.peach, ink: Theme.base }, // view_split_vertical
{ icon: "\u{F036C}", cap: Theme.blue, ink: Theme.base }, // microphone
{ icon: "\u{F02C3}", text: "group", cap: Theme.peach, ink: Theme.base }, // group
{ text: "1", sub: "\u{F03B0}", cap: Theme.peach, ink: Theme.base }, // numeric_5_box_outline
{ text: "2", sub: "\u{F03B5}", cap: Theme.peach, ink: Theme.base },
{ text: "3", sub: "\u{F03B8}", cap: Theme.peach, ink: Theme.base },
{ text: "4", sub: "\u{F03BB}", cap: Theme.peach, ink: Theme.base },
]
function title(id) {
if (id === "top") return "Top encoder";
if (id === "bottom") return "Bottom encoder";
return "Key " + id.slice(1);
}
// Pinned at a fixed height rather than centred: centred, the pad moved
// every time the list under it changed length.
Column {
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 0.3
spacing: panel.unit * 0.5
Row {
spacing: panel.unit * 0.3
Grid {
id: keys
columns: 4
spacing: panel.unit * 0.12
Repeater {
model: 12
Rectangle {
id: keycap
required property int index
readonly property string padId: "k" + (index + 1)
readonly property var cap: panel.caps[index]
width: panel.unit
height: panel.unit
radius: panel.unit * 0.12
color: cap.cap
border.color: panel.hovered === padId ? Theme.text : "transparent"
border.width: 3
Column {
anchors.centerIn: parent
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: !!keycap.cap.icon
text: keycap.cap.icon ?? ""
color: keycap.cap.ink
font.family: Theme.iconFamily
font.pixelSize: panel.unit * 0.34
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: !!keycap.cap.text
text: keycap.cap.text ?? ""
color: keycap.cap.ink
font.family: Theme.fontFamily
font.bold: true
// A lone digit is the cap's main legend.
font.pixelSize: panel.unit * (keycap.cap.icon ? 0.16 : 0.34)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: !!keycap.cap.sub
text: keycap.cap.sub ?? ""
color: keycap.cap.ink
font.family: Theme.iconFamily
font.pixelSize: panel.unit * 0.26
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: panel.hovered = parent.padId
}
}
}
}
// One encoder level with the top row, one with the bottom.
Item {
width: panel.unit * 0.9
height: keys.height
Repeater {
model: ["top", "bottom"]
Rectangle {
required property string modelData
y: modelData === "top" ? 0 : parent.height - height
width: parent.width
height: width
radius: width / 2
color: panel.hovered === modelData ? Theme.surfaceAlt : Theme.surface
border.color: panel.hovered === modelData ? Theme.accent : Theme.overlay
border.width: 2
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: panel.hovered = parent.modelData
}
}
}
}
}
Column {
width: panel.padWidth
spacing: 6
Text {
text: panel.hovered ? panel.title(panel.hovered) : "Hover a key or an encoder"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: 18
font.bold: true
bottomPadding: 4
}
Repeater {
model: panel.actions[panel.hovered] ?? []
// Fixed widths summing to the column's: a Row sizes to its
// children.
Row {
required property var modelData
spacing: 12
Text {
width: panel.padWidth * 0.4
text: modelData.how
color: Theme.subtext
font.family: Theme.fontFamily
font.pixelSize: 15
}
Text {
width: panel.padWidth * 0.6 - 12
text: modelData.label
wrapMode: Text.Wrap
color: Theme.text
font.family: Theme.fontFamily
font.pixelSize: 15
}
}
}
}
}
}
|