Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 main.py

View raw Download
text/plain • 24.84 kiB
Python script, ASCII text executable
        
            
1
"""
2
Kineboard: an experimental touchscreen keyboard using swipes to allow
3
having fewer touch targets.
4
Copyright 2025-2026, roundabout-host.com <vlad@roundabout-host.com>
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public Licence as published by
7
the Free Software Foundation, either version 3 of the Licence, or
8
(at your option) any later version.
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
GNU General Public Licence for more details.
13
You should have received a copy of the GNU General Public Licence
14
along with this program. If not, see <https://www.gnu.org/licenses/>.
15
"""
16
17
import itertools
18
import sys
19
import os
20
import time
21
22
import ruamel.yaml as yaml
23
from pathlib import Path
24
25
os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
26
from ctypes import CDLL
27
CDLL("libgtk4-layer-shell.so")
28
29
import gi
30
gi.require_version("Gtk", "4.0")
31
gi.require_version("Gtk4LayerShell", "1.0")
32
from gi.repository import (
33
Gtk, Gdk, GObject, Gtk4LayerShell, GLib, Gio, GObject
34
)
35
36
try:
37
gi.require_version("Lfb", "0.0")
38
from gi.repository import Lfb
39
except ImportError:
40
Lfb = None
41
42
from pywayland.client import Display, EventQueue
43
from pywayland.protocol.wayland import WlRegistry, WlSeat
44
from pywayland.protocol.input_method_unstable_v1 import (
45
ZwpInputMethodContextV1,
46
ZwpInputMethodV1
47
)
48
from pywayland.protocol.input_method_unstable_v2 import (
49
ZwpInputMethodV2,
50
ZwpInputMethodManagerV2
51
)
52
from pywayland.protocol.virtual_keyboard_unstable_v1 import ZwpVirtualKeyboardManagerV1
53
54
from xkbcommon import xkb
55
56
import ctypes
57
from cffi import FFI
58
ffi = FFI()
59
ffi.cdef("""
60
void * gdk_wayland_display_get_wl_display (void * display);
61
void * gdk_wayland_surface_get_wl_surface (void * surface);
62
void * gdk_wayland_monitor_get_wl_output (void * monitor);
63
""")
64
gtk = ffi.dlopen("libgtk-4.so.1")
65
66
67
module_directory = Path(__file__).resolve().parent
68
69
70
custom_css = """
71
.kineboard-selected-character {
72
text-decoration: underline;
73
}
74
"""
75
css_provider = Gtk.CssProvider()
76
css_provider.load_from_data(custom_css)
77
Gtk.StyleContext.add_provider_for_display(
78
Gdk.Display.get_default(),
79
css_provider,
80
100
81
)
82
83
84
def get_layout_directories():
85
data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
86
data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")]
87
all_paths = [data_home / "kineboard" / "layouts"] + [d / "kineboard" / "layouts" for d in data_dirs]
88
return [d for d in all_paths if d.is_dir()]
89
90
91
class GestureZone(GObject.GEnum):
92
NONE = 0
93
CENTRAL = 1
94
TOP_LEFT = 2
95
TOP_RIGHT = 3
96
BOTTOM_RIGHT = 4
97
BOTTOM_LEFT = 5
98
99
100
class SwipeButton(Gtk.Frame):
101
@GObject.Signal
102
def top_left(self):
103
pass
104
105
@GObject.Signal
106
def top_right(self):
107
pass
108
109
@GObject.Signal
110
def bottom_left(self):
111
pass
112
113
@GObject.Signal
114
def bottom_right(self):
115
pass
116
117
@GObject.Signal
118
def central(self):
119
pass
120
121
@GObject.Signal(arg_types=(int, bool))
122
def drag_changed(self, zone, first):
123
pass
124
125
def __init__(self, **kwargs):
126
Gtk.Frame.__init__(self, **kwargs)
127
self.zone = GestureZone.NONE
128
self.gesture_drag = Gtk.GestureDrag()
129
self.gesture_drag.connect("drag-end", self.finish_drag)
130
self.gesture_drag.connect("drag-update", self.move_drag)
131
self.gesture_drag.connect("drag-begin", self.begin_drag)
132
self.add_controller(self.gesture_drag)
133
134
def finish_drag(self, gesture, offset_x, offset_y):
135
self.zone = GestureZone.NONE
136
if abs(offset_x) / self.get_width() + abs(offset_y) / self.get_height() > 1 / 2:
137
if offset_x > 0 and offset_y > 0:
138
self.emit("bottom_right")
139
elif offset_x <= 0 and offset_y > 0:
140
self.emit("bottom_left")
141
elif offset_x > 0 and offset_y <= 0:
142
self.emit("top_right")
143
elif offset_x <= 0 and offset_y <= 0:
144
self.emit("top_left")
145
else:
146
self.emit("central")
147
148
def move_drag(self, gesture, offset_x, offset_y):
149
new_zone = self.zone
150
if abs(offset_x) / self.get_width() + abs(offset_y) / self.get_height() > 1 / 2:
151
if offset_x > 0 and offset_y > 0:
152
new_zone = GestureZone.BOTTOM_RIGHT
153
elif offset_x <= 0 and offset_y > 0:
154
new_zone = GestureZone.BOTTOM_LEFT
155
elif offset_x > 0 and offset_y <= 0:
156
new_zone = GestureZone.TOP_RIGHT
157
elif offset_x <= 0 and offset_y <= 0:
158
new_zone = GestureZone.TOP_LEFT
159
else:
160
new_zone = GestureZone.CENTRAL
161
162
if new_zone != self.zone:
163
self.zone = new_zone
164
self.emit("drag_changed", self.zone, False)
165
166
def begin_drag(self, gesture, start_x, start_y):
167
self.zone = GestureZone.CENTRAL
168
self.emit("drag_changed", GestureZone.CENTRAL, True)
169
170
171
class SwipeTyper(SwipeButton):
172
@GObject.Signal(arg_types=(str,))
173
def typed(self, character):
174
if self.release_feedback:
175
self.release_feedback.trigger_feedback()
176
# Since the drag is finished, remove the underlines
177
for label in self.label_map.values():
178
label.remove_css_class("kineboard-selected-character")
179
180
def underline(self, zone, first):
181
fb = self.depress_feedback if first else self.change_feedback
182
if fb:
183
fb.trigger_feedback()
184
for label in self.label_map.values():
185
label.remove_css_class("kineboard-selected-character")
186
self.label_map[zone].add_css_class("kineboard-selected-character")
187
188
def __init__(self, tl, tr, bl, br, c, depress_feedback, change_feedback, release_feedback, **kwargs):
189
SwipeButton.__init__(self, **kwargs)
190
self.depress_feedback = depress_feedback
191
self.change_feedback = change_feedback
192
self.release_feedback = release_feedback
193
self.tl = tl
194
self.tr = tr
195
self.bl = bl
196
self.br = br
197
self.c = c
198
self.grid = Gtk.Grid(hexpand=True, vexpand=True, row_homogeneous=True, column_homogeneous=True, margin_top=4, margin_bottom=4, margin_start=4, margin_end=4)
199
self.set_child(self.grid)
200
self.tl_label = Gtk.Label(label=tl)
201
self.grid.attach(self.tl_label, 0, 0, 1, 1)
202
self.tr_label = Gtk.Label(label=tr)
203
self.grid.attach(self.tr_label, 2, 0, 1, 1)
204
self.bl_label = Gtk.Label(label=bl)
205
self.grid.attach(self.bl_label, 0, 2, 1, 1)
206
self.br_label = Gtk.Label(label=br)
207
self.grid.attach(self.br_label, 2, 2, 1, 1)
208
self.c_label = Gtk.Label(label=c)
209
self.grid.attach(self.c_label, 1, 1, 1, 1)
210
self.label_map = {
211
GestureZone.CENTRAL: self.c_label,
212
GestureZone.TOP_LEFT: self.tl_label,
213
GestureZone.TOP_RIGHT: self.tr_label,
214
GestureZone.BOTTOM_RIGHT: self.br_label,
215
GestureZone.BOTTOM_LEFT: self.bl_label
216
}
217
self.connect("top_left", lambda p: self.emit("typed", tl))
218
self.connect("top_right", lambda p: self.emit("typed", tr))
219
self.connect("bottom_left", lambda p: self.emit("typed", bl))
220
self.connect("bottom_right", lambda p: self.emit("typed", br))
221
self.connect("central", lambda p: self.emit("typed", c))
222
self.connect("drag_changed", lambda p, zone, first: self.underline(zone, first))
223
224
225
class Plane(Gtk.Grid):
226
def __init__(self, app, data, depress_feedback, change_feedback, release_feedback, **kwargs):
227
Gtk.Grid.__init__(self, **kwargs)
228
self.app = app
229
self.depress_feedback = depress_feedback
230
self.change_feedback = change_feedback
231
self.release_feedback = release_feedback
232
self.all_characters = ""
233
for j, column in enumerate(data):
234
for k, key in enumerate(column):
235
widget = SwipeTyper(key["top_left"], key["top_right"], key["bottom_left"],
236
key["bottom_right"], key["central"], self.depress_feedback,
237
self.change_feedback, self.release_feedback)
238
widget.connect("typed", self.app.typed)
239
self.attach(widget, k, j, 1, 1)
240
self.all_characters += key["top_left"] + key["top_right"] + key["bottom_left"] + key["bottom_right"] + key["central"]
241
242
243
def make_xkb_keymap_from_unicodes(unicodes):
244
symbols = [
245
"key <LEFT> { [ Left ] };"
246
"key <RGHT> { [ Right ] };"
247
"key <UP> { [ Up ] };"
248
"key <DOWN> { [ Down ] };"
249
"key <TAB> { [ Tab ] };",
250
"key <LCTL> { [ Control_L ] };",
251
"key <LALT> { [ Alt_L ] };",
252
"key <LSHF> { [ Shift_L ] };",
253
"key <LSPR> { [ Super_L ] };",
254
"key <ESCP> { [ Escape ] };",
255
"key <BKSP> { [ BackSpace ] };",
256
"key <RTRN> { [ Return ] };",
257
]
258
keycodes = [
259
"<LEFT> = 4084;",
260
"<RGHT> = 4085;",
261
"<UP> = 4086;",
262
"<DOWN> = 4087;",
263
"<TAB> = 4088;",
264
"<LCTL> = 4089;",
265
"<LALT> = 4090;",
266
"<LSHF> = 4091;",
267
"<LSPR> = 4092;",
268
"<ESCP> = 4093;",
269
"<BKSP> = 4094;",
270
"<RTRN> = 4095;",
271
]
272
for i, char in enumerate(unicodes):
273
keycode = 0x1000 + i
274
keysym = f"U{ord(char):04X}"
275
symbols.append(f"key <K{i:03X}> {{ [ {keysym} ] }};")
276
keycodes.append(f"<K{i:03X}> = {keycode};")
277
278
return ("xkb_keymap { xkb_keycodes \"kineboard\" { minimum = 8; maximum = 65535; "
279
+ " ".join(keycodes) + " }; xkb_symbols \"kineboard\" { " + " ".join(symbols)
280
+ " }; xkb_types \"kineboard\" { type \"ONE_LEVEL\" { modifiers = none; level_name[Level1] = \"Any\"; }; }; xkb_compatibility \"kineboard\" {}; };")
281
282
283
from gi.repository import Gio, GLib
284
285
286
def register_client(client_id: str):
287
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
288
289
proxy = Gio.DBusProxy.new_sync(
290
bus,
291
Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES |
292
Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION,
293
None,
294
"org.gnome.SessionManager",
295
"/org/gnome/SessionManager",
296
"org.gnome.SessionManager",
297
None
298
)
299
300
startup_id = GLib.getenv("DESKTOP_AUTOSTART_ID") or ""
301
302
def on_registered(proxy, result, loop):
303
try:
304
proxy.call_finish(result)
305
except Exception as e:
306
print("cannot register client:", e)
307
loop.quit()
308
309
loop = GLib.MainLoop()
310
311
proxy.call(
312
"RegisterClient",
313
GLib.Variant("(ss)", (client_id, startup_id)),
314
Gio.DBusCallFlags.NONE,
315
-1,
316
None,
317
lambda p, r, l: on_registered(p, r, loop),
318
loop
319
)
320
321
loop.run()
322
return proxy
323
324
325
class ModifierButton(Gtk.ToggleButton):
326
def __init__(self, app, mask, shift_feedback, unshift_feedback, **kwargs):
327
super().__init__(**kwargs)
328
self.app = app
329
self.mask = mask
330
self.shift_feedback = shift_feedback
331
self.unshift_feedback = unshift_feedback
332
333
self.connect("toggled", self.toggle_modifier)
334
335
def toggle_modifier(self, button):
336
if button.get_active():
337
self.app.depressed_modifiers |= self.mask
338
if self.shift_feedback:
339
self.shift_feedback.trigger_feedback()
340
else:
341
self.app.depressed_modifiers &= ~self.mask
342
if self.unshift_feedback:
343
self.unshift_feedback.trigger_feedback()
344
345
self.app.send_modifiers()
346
347
348
class Kineboard(Gtk.Application):
349
def __init__(self):
350
Gtk.Application.__init__(
351
self,
352
application_id="sm.puri.OSK0",
353
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
354
)
355
self.main_box = None
356
self.depress_feedback = None
357
self.change_feedback = None
358
self.release_feedback = None
359
self.shift_feedback = None
360
self.unshift_feedback = None
361
self.xkb_context = xkb.Context()
362
self.revealer = None
363
self.switcher = None
364
self.depressed_modifiers = 0
365
self.character_index = {}
366
self.keymap_fd = None
367
self.vk = None
368
self.keymap_text = ""
369
self.all_characters = ""
370
self.context = None
371
self.input_method_manager = None
372
self.input_method = None
373
self.registry = None
374
self.box = None
375
self.window = None
376
self.stack = None
377
self.display = None
378
self.wl_display = None
379
self.seat = None
380
self.vkm = None
381
self.serial = 0
382
self.extra_revealer = None
383
self.extra_viewport = None
384
self.extra_scroll = None
385
self.extra_controls = None
386
self.add_main_option(
387
"invoke",
388
ord("i"),
389
GLib.OptionFlags.NONE,
390
GLib.OptionArg.NONE,
391
"Manually bring up the keyboard",
392
None
393
)
394
self.add_main_option(
395
"hide",
396
ord("h"),
397
GLib.OptionFlags.NONE,
398
GLib.OptionArg.NONE,
399
"Hide the keyboard",
400
None
401
)
402
403
def send_modifiers(self):
404
self.vk.modifiers(self.depressed_modifiers, 0, 0, 0)
405
406
def do_startup(self):
407
Gtk.Application.do_startup(self)
408
register_client("sm.puri.OSK0")
409
if Lfb:
410
Lfb.init("sm.puri.OSK0")
411
self.depress_feedback = Lfb.Event.new("key-pressed")
412
self.change_feedback = Lfb.Event.new("key-changed")
413
self.release_feedback = Lfb.Event.new("key-released")
414
self.shift_feedback = Lfb.Event.new("button-pressed")
415
self.unshift_feedback = Lfb.Event.new("button-released")
416
self.display = Gdk.Display.get_default()
417
self.window = Gtk.Window(application=self, title="Demo", focus_on_click=False, focusable=False, can_focus=False)
418
Gtk4LayerShell.init_for_window(self.window)
419
Gtk4LayerShell.set_keyboard_mode(self.window, Gtk4LayerShell.KeyboardMode.NONE)
420
self.window.set_default_size(16384, -1) # large width so that it spans the bottom
421
Gtk4LayerShell.set_anchor(self.window, Gtk4LayerShell.Edge.BOTTOM, True)
422
Gtk4LayerShell.set_anchor(self.window, Gtk4LayerShell.Edge.LEFT, True)
423
Gtk4LayerShell.set_anchor(self.window, Gtk4LayerShell.Edge.RIGHT, True)
424
Gtk4LayerShell.auto_exclusive_zone_enable(self.window)
425
Gtk4LayerShell.set_namespace(self.window, "osk")
426
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
427
self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, margin_top=8)
428
self.window.set_child(self.main_box)
429
self.stack = Gtk.Stack()
430
self.box.append(self.stack)
431
layouts = itertools.chain.from_iterable((f for f in path.iterdir() if f.is_file()) for path in get_layout_directories())
432
self.all_characters = " "
433
self.switcher = Gtk.StackSwitcher(stack=self.stack)
434
self.revealer = Gtk.Revealer(child=Gtk.ScrolledWindow(child=Gtk.Viewport(child=self.switcher)))
435
self.box.prepend(self.revealer)
436
437
self.extra_controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8,
438
margin_bottom=8, margin_start=8, margin_end=8)
439
self.extra_viewport = Gtk.Viewport(child=self.extra_controls)
440
self.extra_scroll = Gtk.ScrolledWindow(child=self.extra_viewport, propagate_natural_height=True)
441
self.extra_revealer = Gtk.Revealer(child=self.extra_scroll)
442
443
for layout in layouts:
444
stack = Gtk.Stack()
445
446
yaml_loader = yaml.YAML(typ="rt")
447
data = yaml_loader.load(layout)
448
449
controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8, margin_top=0,
450
margin_bottom=8, margin_start=8, margin_end=8)
451
452
layout_button = Gtk.ToggleButton(child=Gtk.Image.new_from_icon_name("edit-symbolic"))
453
layout_button.set_size_request(48, -1)
454
layout_button.update_property([Gtk.AccessibleProperty.LABEL], ["Switch layout"])
455
controls.append(layout_button)
456
layout_button.bind_property("active", self.revealer, "reveal-child", GObject.BindingFlags.BIDIRECTIONAL)
457
458
control_button = Gtk.ToggleButton(child=Gtk.Image.new_from_icon_name("utilities-terminal-symbolic"))
459
control_button.set_size_request(48, -1)
460
control_button.update_property([Gtk.AccessibleProperty.LABEL], ["Terminal controls"])
461
controls.append(control_button)
462
control_button.bind_property("active", self.extra_revealer, "reveal-child", GObject.BindingFlags.BIDIRECTIONAL)
463
464
if len(data["keys"]) >= 2:
465
shift_button = Gtk.ToggleButton(
466
child=Gtk.Image.new_from_icon_name("go-up-symbolic")
467
)
468
shift_button.set_size_request(48, -1)
469
shift_button.update_property([Gtk.AccessibleProperty.LABEL], ["Shift"])
470
controls.append(shift_button)
471
472
def shift(button: Gtk.ToggleButton):
473
if button.get_active():
474
if self.shift_feedback:
475
self.shift_feedback.trigger_feedback()
476
else:
477
if self.unshift_feedback:
478
self.unshift_feedback.trigger_feedback()
479
480
stack.set_visible_child_name(f"plane-{int(button.get_active())}")
481
482
shift_button.connect("toggled", shift)
483
484
space_bar = Gtk.Button(hexpand=True)
485
space_bar.update_property([Gtk.AccessibleProperty.LABEL], ["Space"])
486
487
def space(x):
488
self.typed(x, " ")
489
if self.unshift_feedback:
490
self.unshift_feedback.trigger_feedback()
491
492
space_bar.connect("clicked", space)
493
controls.append(space_bar)
494
495
backspace_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("edit-clear-symbolic"))
496
backspace_button.set_size_request(48, -1)
497
backspace_button.update_property([Gtk.AccessibleProperty.LABEL], ["Backspace"])
498
backspace_button.connect("clicked", lambda x: self.send_keysym(0xFF6))
499
controls.append(backspace_button)
500
501
enter_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("keyboard-enter-symbolic"))
502
enter_button.set_size_request(48, -1)
503
enter_button.update_property([Gtk.AccessibleProperty.LABEL], ["Enter"])
504
enter_button.add_css_class("suggested-action")
505
enter_button.connect("clicked", lambda x: self.send_keysym(0xFF7))
506
controls.append(enter_button)
507
508
for i, layer in enumerate(data["keys"]):
509
grid = Plane(self, layer, row_spacing=8, column_spacing=8, row_homogeneous=True,
510
column_homogeneous=True, margin_bottom=8,
511
margin_end=8, margin_start=8,
512
depress_feedback=self.depress_feedback,
513
change_feedback=self.change_feedback,
514
release_feedback=self.release_feedback)
515
self.all_characters += grid.all_characters
516
stack.add_named(grid, f"plane-{i}")
517
518
outer_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
519
outer_box.append(stack)
520
outer_box.append(controls)
521
self.stack.add_titled(outer_box, data["symbol"], data["symbol"])
522
523
self.main_box.append(self.extra_revealer)
524
self.main_box.append(self.revealer)
525
self.main_box.append(self.box)
526
527
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
528
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)
529
530
self.wl_display = Display()
531
wl_display_ptr = gtk.gdk_wayland_display_get_wl_display(
532
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.display.__gpointer__, None)))
533
self.wl_display._ptr = wl_display_ptr
534
self.registry = self.wl_display.get_registry()
535
self.registry.dispatcher["global"] = self.on_global
536
self.registry.dispatcher["global_remove"] = self.on_global_remove
537
self.wl_display.roundtrip()
538
539
if self.input_method_manager:
540
self.input_method = self.input_method_manager.get_input_method(self.seat)
541
self.input_method.dispatcher["activate"] = self.activate_input
542
self.input_method.dispatcher["deactivate"] = self.deactivate_input
543
else:
544
dialog = Gtk.AlertDialog(message="Input method protocol not supported.")
545
dialog.show()
546
547
self.keymap_text = make_xkb_keymap_from_unicodes(self.all_characters)
548
xkb_keymap = self.xkb_context.keymap_new_from_string(self.keymap_text)
549
state = xkb_keymap.state_new()
550
551
escape_button = Gtk.Button(label="Escape")
552
escape_button.connect("clicked", lambda x: self.send_keysym(0xFF5))
553
self.extra_controls.append(escape_button)
554
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Control"), self.shift_feedback, self.unshift_feedback, label="Control"))
555
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Mod1"), self.shift_feedback, self.unshift_feedback, label="Alt"))
556
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Shift"), self.shift_feedback, self.unshift_feedback, label="Shift"))
557
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Mod4"), self.shift_feedback, self.unshift_feedback, label="Super"))
558
tab_button = Gtk.Button(label="Tab")
559
tab_button.connect("clicked", lambda x: self.send_keysym(0xFF0))
560
self.extra_controls.append(tab_button)
561
left_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-previous-symbolic"))
562
left_button.update_property([Gtk.AccessibleProperty.LABEL], ["Left"])
563
left_button.connect("clicked", lambda x: self.send_keysym(0xFEC))
564
self.extra_controls.append(left_button)
565
down_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-down-symbolic"))
566
down_button.update_property([Gtk.AccessibleProperty.LABEL], ["Down"])
567
down_button.connect("clicked", lambda x: self.send_keysym(0xFEF))
568
self.extra_controls.append(down_button)
569
up_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-up-symbolic"))
570
up_button.update_property([Gtk.AccessibleProperty.LABEL], ["Up"])
571
up_button.connect("clicked", lambda x: self.send_keysym(0xFEE))
572
self.extra_controls.append(up_button)
573
right_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-next-symbolic"))
574
right_button.update_property([Gtk.AccessibleProperty.LABEL], ["Right"])
575
right_button.connect("clicked", lambda x: self.send_keysym(0xFED))
576
self.extra_controls.append(right_button)
577
578
self.character_index = {}
579
for i, character in enumerate(self.all_characters):
580
self.character_index[character] = i
581
self.vk = self.vkm.create_virtual_keyboard(self.seat)
582
583
self.keymap_fd = os.memfd_create("keymap")
584
os.write(self.keymap_fd, self.keymap_text.encode("utf-8"))
585
os.lseek(self.keymap_fd, 0, os.SEEK_SET)
586
self.vk.keymap(1, self.keymap_fd, len(self.keymap_text.encode("utf-8")))
587
588
def activate_input(self, im):
589
self.window.present()
590
591
def deactivate_input(self, im):
592
self.window.set_visible(False)
593
self.serial = 0
594
595
def on_global(self, registry, name, interface, version):
596
if interface == "zwp_input_method_manager_v2":
597
self.input_method_manager = registry.bind(name, ZwpInputMethodManagerV2, version)
598
elif interface == "wl_seat":
599
self.seat = registry.bind(name, WlSeat, version)
600
elif interface == "zwp_virtual_keyboard_manager_v1":
601
self.vkm = registry.bind(name, ZwpVirtualKeyboardManagerV1, version)
602
603
def on_global_remove(self, registry, name):
604
pass
605
606
def send_keysym(self, key):
607
clock = int(time.monotonic() * 1000)
608
if self.vk:
609
self.vk.key(clock, key, 1)
610
self.vk.key(clock, key, 0)
611
if self.unshift_feedback:
612
self.unshift_feedback.trigger_feedback()
613
614
def typed(self, button, characters):
615
for char in characters:
616
self.send_keysym(0xFF8 + self.character_index[char])
617
618
def do_activate(self):
619
Gtk.Application.do_activate(self)
620
# self.window.present()
621
622
def do_command_line(self, command_line: Gio.ApplicationCommandLine):
623
options = command_line.get_options_dict()
624
args = command_line.get_arguments()[1:]
625
if options.contains("invoke"):
626
self.activate_input(None, None)
627
elif options.contains("hide"):
628
self.deactivate_input(None, None)
629
return 0
630
631
632
if __name__ == "__main__":
633
kineboard = Kineboard()
634
kineboard.run(sys.argv)
635