by roundabout, Tuesday, 6 October 2026, 14:18:06 (1791296286), pushed by roundabout, Tuesday, 6 October 2026, 14:18:11 (1791296291)
Author identity: Vlad <vlad.muntoiu@gmail.com>
e99bfc7ff76a150a6dfc599e4450aeb5e2d0781f
main.py
@@ -1,7 +1,7 @@
"""
Kineboard: an experimental touchscreen keyboard using swipes to allow
having fewer touch targets.
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
Copyright 2025-2026, roundabout-host.com <vlad@roundabout-host.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public Licence as published by
the Free Software Foundation, either version 3 of the Licence, or
@@ -29,7 +29,15 @@ CDLL("libgtk4-layer-shell.so")
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gtk4LayerShell", "1.0")
from gi.repository import Gtk, Gdk, GObject, Gtk4LayerShell, GLib, Gio, GObject
from gi.repository import (
Gtk, Gdk, GObject, Gtk4LayerShell, GLib, Gio, GObject
)
try:
gi.require_version("Lfb", "0.0")
from gi.repository import Lfb
except ImportError:
Lfb = None
from pywayland.client import Display, EventQueue
from pywayland.protocol.wayland import WlRegistry, WlSeat
@@ -81,11 +89,12 @@ def get_layout_directories():
class GestureZone(GObject.GEnum):
CENTRAL = 0
TOP_LEFT = 1
TOP_RIGHT = 2
BOTTOM_RIGHT = 3
BOTTOM_LEFT = 4
NONE = 0
CENTRAL = 1
TOP_LEFT = 2
TOP_RIGHT = 3
BOTTOM_RIGHT = 4
BOTTOM_LEFT = 5
class SwipeButton(Gtk.Frame):
@@ -115,6 +124,7 @@ class SwipeButton(Gtk.Frame):
def __init__(self, **kwargs):
Gtk.Frame.__init__(self, **kwargs)
self.zone = GestureZone.NONE
self.gesture_drag = Gtk.GestureDrag()
self.gesture_drag.connect("drag-end", self.finish_drag)
self.gesture_drag.connect("drag-update", self.move_drag)
@@ -135,17 +145,22 @@ class SwipeButton(Gtk.Frame):
self.emit("central")
def move_drag(self, gesture, offset_x, offset_y):
new_zone = self.zone
if abs(offset_x) / self.get_width() + abs(offset_y) / self.get_height() > 1 / 2:
if offset_x > 0 and offset_y > 0:
self.emit("drag_changed", GestureZone.BOTTOM_RIGHT)
new_zone = GestureZone.BOTTOM_RIGHT
elif offset_x <= 0 and offset_y > 0:
self.emit("drag_changed", GestureZone.BOTTOM_LEFT)
new_zone = GestureZone.BOTTOM_LEFT
elif offset_x > 0 and offset_y <= 0:
self.emit("drag_changed", GestureZone.TOP_RIGHT)
new_zone = GestureZone.TOP_RIGHT
elif offset_x <= 0 and offset_y <= 0:
self.emit("drag_changed", GestureZone.TOP_LEFT)
new_zone = GestureZone.TOP_LEFT
else:
self.emit("drag_changed", GestureZone.CENTRAL)
new_zone = GestureZone.CENTRAL
if new_zone != self.zone:
self.zone = new_zone
self.emit("drag_changed", self.zone)
def begin_drag(self, gesture, start_x, start_y):
self.emit("drag_changed", GestureZone.CENTRAL)
@@ -154,17 +169,23 @@ class SwipeButton(Gtk.Frame):
class SwipeTyper(SwipeButton):
@GObject.Signal(arg_types=(str,))
def typed(self, character):
if self.release_feedback:
self.release_feedback.trigger_feedback()
# Since the drag is finished, remove the underlines
for label in self.label_map.values():
label.remove_css_class("kineboard-selected-character")
def underline(self, zone):
if self.depress_feedback:
self.depress_feedback.trigger_feedback()
for label in self.label_map.values():
label.remove_css_class("kineboard-selected-character")
self.label_map[zone].add_css_class("kineboard-selected-character")
def __init__(self, tl, tr, bl, br, c, **kwargs):
def __init__(self, tl, tr, bl, br, c, depress_feedback, release_feedback, **kwargs):
SwipeButton.__init__(self, **kwargs)
self.depress_feedback = depress_feedback
self.release_feedback = release_feedback
self.tl = tl
self.tr = tr
self.bl = bl
@@ -198,20 +219,21 @@ class SwipeTyper(SwipeButton):
class Plane(Gtk.Grid):
def __init__(self, app, data, **kwargs):
def __init__(self, app, data, depress_feedback, release_feedback, **kwargs):
Gtk.Grid.__init__(self, **kwargs)
self.app = app
self.depress_feedback = depress_feedback
self.release_feedback = release_feedback
self.all_characters = ""
for j, column in enumerate(data):
for k, key in enumerate(column):
widget = SwipeTyper(key["top_left"], key["top_right"], key["bottom_left"],
key["bottom_right"], key["central"])
key["bottom_right"], key["central"], self.depress_feedback, self.release_feedback)
widget.connect("typed", self.app.typed)
self.attach(widget, k, j, 1, 1)
self.all_characters += key["top_left"] + key["top_right"] + key["bottom_left"] + key["bottom_right"] + key["central"]
def make_xkb_keymap_from_unicodes(unicodes):
symbols = [
"key <LEFT> { [ Left ] };"
@@ -318,6 +340,8 @@ class Kineboard(Gtk.Application):
application_id="sm.puri.OSK0",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
)
self.depress_feedback = None
self.release_feedback = None
self.xkb_context = xkb.Context()
self.revealer = None
self.switcher = None
@@ -366,6 +390,12 @@ class Kineboard(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
register_client("sm.puri.OSK0")
self.depress_feedback = None
self.release_feedback = None
if Lfb:
Lfb.init("sm.puri.OSK0")
self.depress_feedback = Lfb.Event("button-pressed")
self.release_feedback = Lfb.Event("button-released")
self.display = Gdk.Display.get_default()
self.window = Gtk.Window(application=self, title="Demo", focus_on_click=False, focusable=False, can_focus=False)
Gtk4LayerShell.init_for_window(self.window)
@@ -448,7 +478,9 @@ class Kineboard(Gtk.Application):
for i, layer in enumerate(data["keys"]):
grid = Plane(self, layer, row_spacing=8, column_spacing=8, row_homogeneous=True,
column_homogeneous=True, margin_bottom=8,
margin_end=8, margin_start=8)
margin_end=8, margin_start=8,
depress_feedback=self.depress_feedback,
release_feedback=self.release_feedback)
self.all_characters += grid.all_characters
stack.add_named(grid, f"plane-{i}")