Compare commits
2 Commits
v2.12
...
gamepad-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3df85e9443 | ||
|
|
2d72f22bbf |
@@ -96,9 +96,75 @@
|
|||||||
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
|
"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",
|
"name" : "blueprint-compiler",
|
||||||
"buildsystem" : "meson",
|
"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
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import lzma
|
import lzma
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -25,9 +26,10 @@ import gi
|
|||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("Adw", "1")
|
gi.require_version("Adw", "1")
|
||||||
|
gi.require_version("Manette", "0.2")
|
||||||
|
|
||||||
# pylint: disable=wrong-import-position
|
# 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 import shared
|
||||||
from src.details_window import DetailsWindow
|
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.legendary_source import LegendarySource
|
||||||
from src.importer.sources.lutris_source import LutrisSource
|
from src.importer.sources.lutris_source import LutrisSource
|
||||||
from src.importer.sources.steam_source import SteamSource
|
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.logging.setup import log_system_info, setup_logging
|
||||||
from src.preferences import PreferencesWindow
|
from src.preferences import PreferencesWindow
|
||||||
from src.store.managers.display_manager import DisplayManager
|
from src.store.managers.display_manager import DisplayManager
|
||||||
@@ -53,15 +56,81 @@ from src.window import CartridgesWindow
|
|||||||
|
|
||||||
class CartridgesApplication(Adw.Application):
|
class CartridgesApplication(Adw.Application):
|
||||||
win = None
|
win = None
|
||||||
|
window_controller = None
|
||||||
|
keyboard_emulator = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE
|
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
|
def do_activate(self): # pylint: disable=arguments-differ
|
||||||
"""Called on app creation"""
|
"""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
|
# Set fallback icon-name
|
||||||
Gtk.Window.set_default_icon_name(shared.APP_ID)
|
Gtk.Window.set_default_icon_name(shared.APP_ID)
|
||||||
|
|
||||||
@@ -70,6 +139,16 @@ class CartridgesApplication(Adw.Application):
|
|||||||
if not self.win:
|
if not self.win:
|
||||||
shared.win = self.win = CartridgesWindow(application=self)
|
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
|
# Save window geometry
|
||||||
shared.state_schema.bind(
|
shared.state_schema.bind(
|
||||||
"width", self.win, "default-width", Gio.SettingsBindFlags.DEFAULT
|
"width", self.win, "default-width", Gio.SettingsBindFlags.DEFAULT
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ install_data(
|
|||||||
'details_window.py',
|
'details_window.py',
|
||||||
'game.py',
|
'game.py',
|
||||||
'game_cover.py',
|
'game_cover.py',
|
||||||
|
'keyboard_emulator.py',
|
||||||
configure_file(
|
configure_file(
|
||||||
input: 'shared.py.in',
|
input: 'shared.py.in',
|
||||||
output: 'shared.py',
|
output: 'shared.py',
|
||||||
|
|||||||
Reference in New Issue
Block a user