Merge pull request #120 from kra-mo/initial-gamepad-support
initial work on controller support
This commit is contained in:
@@ -96,9 +96,75 @@
|
||||
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "python3-snegg",
|
||||
"buildsystem" : "simple",
|
||||
"build-commands": [
|
||||
"cd snegg",
|
||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} . --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type" : "git",
|
||||
"url": "https://gitlab.freedesktop.org/libinput/snegg.git",
|
||||
"tag": "main"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "python3-attrs",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"attrs\" --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/f0/eb/fcb708c7bf5056045e9e98f62b93bd7467eb718b0202e7698eb11d66416c/attrs-23.1.0-py3-none-any.whl",
|
||||
"sha256": "1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "python3-jinja2",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"jinja2\" --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl",
|
||||
"sha256": "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/6d/7c/59a3248f411813f8ccba92a55feaac4bf360d29e2ff05ee7d8e1ef2d7dbf/MarkupSafe-2.1.3.tar.gz",
|
||||
"sha256": "af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "libei",
|
||||
"buildsystem" : "meson",
|
||||
"config-opts": [
|
||||
"-Ddocumentation=[]",
|
||||
"-Dtests=disabled"
|
||||
],
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"url" : "https://gitlab.freedesktop.org/libinput/libei.git",
|
||||
"tag" : "1.0.0"
|
||||
}
|
||||
],
|
||||
"cleanup" : [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "blueprint-compiler",
|
||||
"buildsystem" : "meson",
|
||||
|
||||
99
src/keyboard_emulator.py
Normal file
99
src/keyboard_emulator.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import logging
|
||||
import select
|
||||
from collections import deque
|
||||
|
||||
from gi.repository import GLib
|
||||
from snegg.oeffis import DisconnectedError, Oeffis, SessionClosedError
|
||||
from snegg.ei import Sender, DeviceCapability, EventType, Seat, Device, Event
|
||||
|
||||
|
||||
class PortalError(Exception):
|
||||
"""Error raised when a oeffis portal can't be acquired"""
|
||||
|
||||
|
||||
class KeyboardEmulator:
|
||||
"""
|
||||
A class that triggers keypresses with libei
|
||||
|
||||
Libei docs: https://libinput.pages.freedesktop.org/libei/
|
||||
Snegg docs: https://libinput.pages.freedesktop.org/snegg/snegg.ei.html
|
||||
"""
|
||||
|
||||
app = None
|
||||
queue: deque = None
|
||||
|
||||
sender: Sender = None
|
||||
seat: Seat = None
|
||||
keyboard: Device = None
|
||||
|
||||
def __init__(self, app) -> None:
|
||||
self.app = app
|
||||
self.queue = deque()
|
||||
|
||||
self.app.connect("emulate-key", self.on_emulate_key)
|
||||
GLib.Thread.new(None, self.thread_func)
|
||||
|
||||
def on_emulate_key(self, keyval):
|
||||
self.queue.append(keyval)
|
||||
|
||||
@staticmethod
|
||||
def get_eis_portal() -> Oeffis:
|
||||
"""Get a portal to the eis server"""
|
||||
portal = Oeffis.create()
|
||||
if portal is None:
|
||||
raise PortalError()
|
||||
poll = select.poll()
|
||||
poll.register(portal.fd)
|
||||
while poll.poll():
|
||||
try:
|
||||
if portal.dispatch():
|
||||
# We need to keep the portal object alive so we don't get disconnected
|
||||
return portal
|
||||
except (SessionClosedError, DisconnectedError) as error:
|
||||
raise PortalError() from error
|
||||
|
||||
def thread_func(self):
|
||||
"""Daemon thread entry point"""
|
||||
|
||||
# Connect to the EIS server
|
||||
try:
|
||||
portal = self.get_eis_portal()
|
||||
except PortalError as error:
|
||||
logging.error("Can't get EIS portal", exc_info=error)
|
||||
raise
|
||||
self.sender = Sender.create_for_fd(fd=portal.eis_fd, name="ei-debug-events")
|
||||
|
||||
# Handle sender events
|
||||
poll = select.poll()
|
||||
poll.register(self.sender.fd)
|
||||
while poll.poll():
|
||||
self.sender.dispatch()
|
||||
for event in self.sender.events:
|
||||
self.handle_sender_event(event)
|
||||
|
||||
def handle_sender_event(self, event: Event):
|
||||
"""Handle libei sender (input producer) events"""
|
||||
|
||||
match event.event_type:
|
||||
# The emulated seat is created, we need to specify its capabilities
|
||||
case EventType.SEAT_ADDED:
|
||||
if not event.seat:
|
||||
return
|
||||
self.seat = event.seat
|
||||
self.seat.bind(DeviceCapability.KEYBOARD)
|
||||
|
||||
# A device was added to the seat (here, we're only doing a keyboard)
|
||||
case EventType.DEVICE_ADDED:
|
||||
if not event.device:
|
||||
return
|
||||
self.keyboard = event.device
|
||||
|
||||
# Input can be processed, send keys
|
||||
case EventType.DEVICE_RESUMED:
|
||||
self.keyboard.start_emulating()
|
||||
keyval = self.queue.popleft()
|
||||
self.keyboard.keyboard_key(keyval, True)
|
||||
self.keyboard.frame()
|
||||
self.keyboard.keyboard_key(keyval, False)
|
||||
self.keyboard.frame()
|
||||
self.keyboard.stop_emulating()
|
||||
81
src/main.py
81
src/main.py
@@ -18,6 +18,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import logging
|
||||
import lzma
|
||||
import sys
|
||||
|
||||
@@ -25,9 +26,10 @@ import gi
|
||||
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Adw", "1")
|
||||
gi.require_version("Manette", "0.2")
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Adw, Gio, GLib, Gtk
|
||||
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk, Manette
|
||||
|
||||
from src import shared
|
||||
from src.details_window import DetailsWindow
|
||||
@@ -39,6 +41,7 @@ from src.importer.sources.itch_source import ItchSource
|
||||
from src.importer.sources.legendary_source import LegendarySource
|
||||
from src.importer.sources.lutris_source import LutrisSource
|
||||
from src.importer.sources.steam_source import SteamSource
|
||||
from src.keyboard_emulator import KeyboardEmulator
|
||||
from src.logging.setup import log_system_info, setup_logging
|
||||
from src.preferences import PreferencesWindow
|
||||
from src.store.managers.display_manager import DisplayManager
|
||||
@@ -53,15 +56,81 @@ from src.window import CartridgesWindow
|
||||
|
||||
class CartridgesApplication(Adw.Application):
|
||||
win = None
|
||||
window_controller = None
|
||||
keyboard_emulator = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE
|
||||
)
|
||||
|
||||
@GObject.Signal(name="emulate-key", arg_types=[int])
|
||||
def emulate_key(self, keyval) -> None:
|
||||
"""Signal emitted when the app wants to emulate a keypress"""
|
||||
|
||||
def gamepad_abs_axis(self, _device, event):
|
||||
logging.debug(event.get_absolute())
|
||||
|
||||
def gamepad_hat_axis(self, device, event):
|
||||
device.rumble(1000, 1000, 50)
|
||||
logging.debug(
|
||||
"Gamepad: hat axis: %s, value: %s", *(hat := event.get_hat())[1:3]
|
||||
)
|
||||
|
||||
if hat[2] != 0:
|
||||
self.navigate(hat[1] + hat[2])
|
||||
|
||||
def navigate(self, direction: int):
|
||||
match direction:
|
||||
case 16:
|
||||
self.emit("emulate-key", Gdk.KEY_Up)
|
||||
print("up")
|
||||
case 18:
|
||||
print("down")
|
||||
case 15:
|
||||
print("left")
|
||||
case 17:
|
||||
print("right")
|
||||
|
||||
def print_args(self, *args):
|
||||
print(*args)
|
||||
|
||||
def gamepad_button_pressed(self, _device, event):
|
||||
logging.debug("Gamepad: %s pressed", (button := event.get_button()[1]))
|
||||
|
||||
match button:
|
||||
case 304:
|
||||
print("A button pressed")
|
||||
case 305:
|
||||
print("B button pressed")
|
||||
case 307:
|
||||
print("X button pressed")
|
||||
case 308:
|
||||
print("Y button pressed")
|
||||
case 314:
|
||||
self.win.on_show_hidden_action()
|
||||
|
||||
def gamepad_listen(self, device, *_args):
|
||||
device.connect("button-press-event", self.gamepad_button_pressed)
|
||||
device.connect("hat-axis-event", self.gamepad_hat_axis)
|
||||
|
||||
def log_connected():
|
||||
logging.debug("%s connected", device.get_name())
|
||||
GLib.timeout_add_seconds(60, log_connected)
|
||||
|
||||
log_connected()
|
||||
|
||||
def do_activate(self): # pylint: disable=arguments-differ
|
||||
"""Called on app creation"""
|
||||
|
||||
# Setup gamepads
|
||||
manette_monitor = Manette.Monitor.new()
|
||||
manette_iter = manette_monitor.iterate()
|
||||
|
||||
while (device := manette_iter.next())[0]:
|
||||
self.gamepad_listen(device[1])
|
||||
self.keyboard_emulator = KeyboardEmulator(self)
|
||||
|
||||
# Set fallback icon-name
|
||||
Gtk.Window.set_default_icon_name(shared.APP_ID)
|
||||
|
||||
@@ -70,6 +139,16 @@ class CartridgesApplication(Adw.Application):
|
||||
if not self.win:
|
||||
shared.win = self.win = CartridgesWindow(application=self)
|
||||
|
||||
for index in range(
|
||||
(list_model := self.win.observe_controllers()).get_n_items()
|
||||
):
|
||||
if isinstance((item := list_model.get_item(index)), Gtk.EventControllerKey):
|
||||
self.window_controller = item
|
||||
break
|
||||
|
||||
self.window_controller.connect("key-pressed", self.print_args)
|
||||
self.window_controller.connect("key-released", self.print_args)
|
||||
|
||||
# Save window geometry
|
||||
shared.state_schema.bind(
|
||||
"width", self.win, "default-width", Gio.SettingsBindFlags.DEFAULT
|
||||
|
||||
@@ -21,6 +21,7 @@ install_data(
|
||||
'details_window.py',
|
||||
'game.py',
|
||||
'game_cover.py',
|
||||
'keyboard_emulator.py',
|
||||
configure_file(
|
||||
input: 'shared.py.in',
|
||||
output: 'shared.py',
|
||||
|
||||
Reference in New Issue
Block a user