1
0
forked from Mirrors/RGSX

v2.2.0.3 - fix virtual keyboard now showing when controller is plugged in or handheld like steamdeck

This commit is contained in:
skymike03
2025-09-09 17:33:55 +02:00
parent 6684993f72
commit be51aa0cea
4 changed files with 165 additions and 208 deletions

View File

@@ -1,8 +1,14 @@
import os
os.environ["SDL_FBDEV"] = "/dev/fb0"
import pygame # type: ignore
import asyncio
import platform
# Ne pas forcer SDL_FBDEV ici; si déjà défini par l'environnement, on le garde
try:
if "SDL_FBDEV" in os.environ:
pass # respecter la configuration existante
except Exception:
pass
import pygame # type: ignore
import time
import asyncio
import logging
import requests
import queue
@@ -27,8 +33,8 @@ from controls import handle_controls, validate_menu_state, process_key_repeats,
from controls_mapper import map_controls, draw_controls_mapping, get_actions
from controls import load_controls_config
from utils import (
detect_non_pc, load_sources, check_extension_before_download, extract_zip_data,
play_random_music, load_music_config, silence_alsa_warnings, enable_alsa_stderr_filter
load_sources, check_extension_before_download, extract_zip_data,
play_random_music, load_music_config
)
from history import load_history, save_history
from config import OTA_data_ZIP
@@ -79,6 +85,37 @@ def _run_windows_gamelist_update():
_run_windows_gamelist_update()
# Vérifier et appliquer les mises à jour AVANT tout chargement des contrôles
try:
# Internet rapide (synchrone) avant init graphique complète
if test_internet():
logger.debug("Pré-boot: connexion Internet OK, vérification des mises à jour")
# Initialiser un mini-contexte de chargement pour feedback si l'écran est prêt
config.menu_state = "loading"
config.current_loading_system = _("loading_check_updates")
config.loading_progress = 5.0
config.needs_redraw = True
# Lancer la vérification en mode synchrone via boucle temporaire
# Utilise une boucle d'événements courte pour permettre pygame d'initialiser
try:
# Créer une boucle asyncio ad-hoc si non présente
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
success, _msg = loop.run_until_complete(check_for_updates())
logger.debug(f"Pré-boot: check_for_updates terminé success={success}")
except Exception as e:
logger.error(f"Pré-boot: échec check_for_updates: {e}")
finally:
try:
loop.close()
except Exception:
pass
config.update_checked = True
else:
logger.debug("Pré-boot: pas d'Internet, pas de vérification des mises à jour")
except Exception as e:
logger.exception(f"Pré-boot update check a échoué: {e}")
# Initialisation de Pygame
pygame.init()
@@ -96,56 +133,7 @@ except Exception as e:
logger.exception(f"Échec du nettoyage des anciens fichiers: {e}")
#Récupération des noms des joysticks si pas de joystick connecté, verifier si clavier connecté
joystick_names = [pygame.joystick.Joystick(i).get_name() for i in range(pygame.joystick.get_count())]
if not joystick_names:
joystick_names = ["Clavier"]
print("Aucun joystick détecté, utilisation du clavier par défaut")
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
config.joystick = False
config.keyboard = True
else:
config.joystick = True
config.keyboard = False
print(f"Joysticks détectés: {joystick_names}")
logger.debug(f"Joysticks détectés: {joystick_names}, utilisation du joystick par défaut.")
# Test des boutons du joystick
for name in joystick_names:
if "Xbox" in name or "360" in name or "X-Box" in name:
config.xbox_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "PlayStation" in name:
config.playstation_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "Nintendo" in name:
config.nintendo_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
elif "Logitech" in name:
config.logitech_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
elif "8Bitdo" in name:
config.eightbitdo_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
elif "Steam" in name:
config.steam_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
elif "TRIMUI Smart Pro" in name:
config.trimui_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
else:
config.generic_controller = True
logger.debug(f"Generic controller detected : {name}")
print(f"Generic controller detected : {name}")
# Chargement des paramètres d'accessibilité
config.accessibility_settings = load_accessibility_settings()
# Appliquer la grille d'affichage depuis les paramètres
@@ -169,8 +157,22 @@ config.sources_mode = get_sources_mode()
config.custom_sources_url = get_custom_sources_url()
logger.debug(f"Mode sources initial: {config.sources_mode}, URL custom: {config.custom_sources_url}")
# Détection du système non-PC
config.is_non_pc = detect_non_pc()
# Détection du système grace a une commande windows / linux (on oublie is non-pc c'est juste pour connaitre le materiel et le systeme d'exploitation)
def detect_system_info():
"""Détecte les informations système (OS, architecture) via des commandes appropriées."""
try:
if platform.system() == "Windows":
# Commande pour Windows
result = subprocess.run(["wmic", "os", "get", "caption"], capture_output=True, text=True)
if result.returncode == 0:
logger.info(f"Système détecté (Windows): {result.stdout.strip()}")
else:
# Commande pour Linux
result = subprocess.run(["lsb_release", "-d"], capture_output=True, text=True)
if result.returncode == 0:
logger.info(f"Système détecté (Linux): {result.stdout.strip()}")
except Exception as e:
logger.error(f"Erreur lors de la détection du système: {e}")
# Initialisation de lécran
screen = init_display()
@@ -185,6 +187,64 @@ config.init_font()
config.screen_width, config.screen_height = pygame.display.get_surface().get_size()
logger.debug(f"Résolution d'écran : {config.screen_width}x{config.screen_height}")
# Détection des joysticks après init_display (plus stable sur Batocera)
try:
if platform.system() != "Windows":
time.sleep(0.05) # petite latence pour stabiliser SDL sur certains builds
count = pygame.joystick.get_count()
except Exception:
count = 0
joystick_names = []
for i in range(count):
try:
j = pygame.joystick.Joystick(i)
joystick_names.append(j.get_name())
except Exception as e:
logger.debug(f"Impossible de lire le nom du joystick {i}: {e}")
normalized_names = [n.lower() for n in joystick_names]
if not joystick_names:
joystick_names = ["Clavier"]
print("Aucun joystick détecté, utilisation du clavier par défaut")
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
config.joystick = False
config.keyboard = True
else:
config.joystick = True
config.keyboard = False
print(f"Joysticks détectés: {joystick_names}")
logger.debug(f"Joysticks détectés: {joystick_names}, utilisation du joystick par défaut.")
for idx, name in enumerate(joystick_names):
lname = name.lower()
if ("xbox" in lname) or ("x-box" in lname) or ("xinput" in lname) or ("microsoft x-box" in lname) or ("x-box 360" in lname) or ("360" in lname):
config.xbox_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "playstation" in lname:
config.playstation_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "nintendo" in lname:
config.nintendo_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
elif "logitech" in lname:
config.logitech_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
elif "8bitdo" in name or "8-bitdo" in lname:
config.eightbitdo_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
elif "steam" in lname:
config.steam_controller = True
logger.debug(f"Controller detected : {name}")
print(f"Controller detected : {name}")
# Note: virtual keyboard display now depends on controller presence (config.joystick)
print(f"Generic controller detected : {name}")
logger.debug(f"Flags contrôleur: xbox={config.xbox_controller}, ps={config.playstation_controller}, nintendo={config.nintendo_controller}, eightbitdo={config.eightbitdo_controller}, steam={config.steam_controller}, trimui={config.trimui_controller}, logitech={config.logitech_controller}, generic={config.generic_controller}")
# Vérification des dossiers pour le débogage
logger.debug(f"SYSTEM_FOLDER: {config.SYSTEM_FOLDER}")
@@ -205,19 +265,12 @@ config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = 0
# Initialisation des variables pour la popup de musique
# Réduction du bruit ALSA (VM Batocera/alsa)
try:
silence_alsa_warnings()
enable_alsa_stderr_filter()
except Exception:
pass
# Initialisation du mixer Pygame
pygame.mixer.pre_init(44100, -16, 2, 4096)
pygame.mixer.init()
try:
pygame.mixer.init()
except Exception as e:
logger.warning(f"Échec init mixer: {e}")
# Charger la configuration de la musique AVANT de lancer la musique
load_music_config()
@@ -245,7 +298,7 @@ config.current_music = current_music # Met à jour la musique en cours dans con
config.history = load_history()
logger.debug(f"Historique de téléchargement : {len(config.history)} entrées")
# Vérification et chargement de la configuration des contrôles
# Vérification et chargement de la configuration des contrôles (après mises à jour et détection manette)
config.controls_config = load_controls_config()
# S'assurer que config.controls_config n'est jamais None
@@ -267,14 +320,16 @@ else:
# Initialisation du gamepad
joystick = None
if pygame.joystick.get_count() > 0:
joystick = pygame.joystick.Joystick(0)
joystick.init()
logger.debug("Gamepad initialisé")
try:
joystick = pygame.joystick.Joystick(0)
joystick.init()
logger.debug("Gamepad initialisé")
except Exception as e:
logger.warning(f"Échec initialisation gamepad: {e}")
# Boucle principale
async def main():
# amazonq-ignore-next-line
global current_music, music_files, music_folder
logger.debug("Début main")
running = True
@@ -728,7 +783,7 @@ async def main():
draw_game_list(screen)
if config.search_mode:
draw_game_list(screen)
if config.is_non_pc:
if getattr(config, 'joystick', False):
draw_virtual_keyboard(screen)
elif config.menu_state == "download_progress":
draw_progress_screen(screen)
@@ -847,6 +902,14 @@ async def main():
config.needs_redraw = True
logger.debug(f"Erreur : {config.error_message}")
elif loading_step == "check_ota":
# Si mise à jour déjà vérifiée au pré-boot, sauter cette étape
if getattr(config, "update_checked", False):
logger.debug("Mises à jour déjà vérifiées au pré-boot, on saute check_for_updates()")
loading_step = "check_data"
config.current_loading_system = _("loading_downloading_games_images")
config.loading_progress = max(config.loading_progress, 50.0)
config.needs_redraw = True
continue
logger.debug("Exécution de check_for_updates()")
success, message = await check_for_updates()
logger.debug(f"Résultat de check_for_updates : success={success}, message={message}")
@@ -986,11 +1049,15 @@ async def main():
pygame.quit()
logger.debug("Application terminée")
result2 = subprocess.run(["batocera-es-swissknife", "--emukill"])
if result2 == 0:
logger.debug(f"Quitté avec succès")
else:
logger.debug("Error en essayant de quitter batocera-es-swissknife.")
try:
if platform.system() != "Windows":
result2 = subprocess.run(["batocera-es-swissknife", "--emukill"])
if result2 == 0:
logger.debug(f"Quitté avec succès")
else:
logger.debug("Error en essayant de quitter batocera-es-swissknife.")
except FileNotFoundError:
logger.debug("batocera-es-swissknife introuvable, saut de l'étape d'arrêt (environnement non Batocera)")

View File

@@ -4,7 +4,7 @@ import logging
import platform
# Version actuelle de l'application
app_version = "2.2.0.0"
app_version = "2.2.0.3"
def get_operating_system():
"""Renvoie le nom du système d'exploitation."""
@@ -196,7 +196,6 @@ last_state_change_time = 0 # Temps du dernier changement d'état pour debounce
debounce_delay = 200 # Délai de debounce en millisecondes
platform_dicts = [] # Liste des dictionnaires de plateformes
selected_key = (0, 0) # Position du curseur dans le clavier virtuel
is_non_pc = True # Indicateur pour plateforme non-PC (par exemple, console)
redownload_confirm_selection = 0 # Sélection pour la confirmation de redownload
popup_message = "" # Message à afficher dans les popups
popup_timer = 0 # Temps restant pour le popup en millisecondes (0 = inactif)
@@ -208,6 +207,18 @@ batch_download_indices = [] # File d'attente des indices de jeux à traiter en
batch_in_progress = False # Indique qu'un lot est en cours
batch_pending_game = None # Données du jeu en attente de confirmation d'extension
# Indicateurs d'entrée (détectés au démarrage)
joystick = False
keyboard = False
xbox_controller = False
playstation_controller = False
nintendo_controller = False
logitech_controller = False
eightbitdo_controller = False
steam_controller = False
trimui_controller = False
generic_controller = False
# --- Filtre plateformes (UI) ---
selected_filter_index = 0 # index dans la liste visible triée
filter_platforms_scroll_offset = 0 # défilement si liste longue
@@ -266,6 +277,9 @@ def init_font():
search_font = None
small_font = None
# Indique si une vérification/installation des mises à jour a déjà été effectuée au démarrage
update_checked = False
def validate_resolution():
"""Valide la résolution de l'écran par rapport aux capacités de l'écran."""
display_info = pygame.display.Info()

View File

@@ -338,7 +338,7 @@ def handle_controls(event, sources, joystick, screen):
# Jeux
elif config.menu_state == "game":
games = config.filtered_games if config.filter_active or config.search_mode else config.games
if config.search_mode and config.is_non_pc:
if config.search_mode and getattr(config, 'joystick', False):
keyboard_layout = [
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
@@ -416,7 +416,7 @@ def handle_controls(event, sources, joystick, screen):
config.filter_active = bool(config.search_query)
config.needs_redraw = True
logger.debug(f"Validation du filtre avec manette: query={config.search_query}, filter_active={config.filter_active}")
elif config.search_mode and not config.is_non_pc:
elif config.search_mode and not getattr(config, 'joystick', False):
# Gestion de la recherche sur PC (clavier et manette)
if is_input_matched(event, "confirm"):
config.search_mode = False

View File

@@ -30,115 +30,7 @@ unavailable_systems = []
# Cache/process flags for extensions generation/loading
def silence_alsa_warnings():
"""Silence ALSA stderr spam (e.g., 'underrun occurred') on Linux by overriding the error handler.
Safe no-op on non-Linux or if libasound is unavailable.
"""
try:
if platform.system() == "Linux":
import ctypes
import ctypes.util
lib = ctypes.util.find_library('asound')
if not lib:
return
asound = ctypes.CDLL(lib)
CErrorHandler = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p)
def py_error_handler(filename, line, function, err, fmt):
return
handler = CErrorHandler(py_error_handler)
try:
asound.snd_lib_error_set_handler(handler)
logger.info("ALSA warnings silenced via snd_lib_error_set_handler")
except Exception as inner:
logger.debug(f"snd_lib_error_set_handler not available: {inner}")
except Exception as e:
logger.debug(f"Unable to silence ALSA warnings: {e}")
def enable_alsa_stderr_filter():
"""Filter ALSA 'underrun occurred' spam from stderr by intercepting FD 2.
Works on Linux by routing stderr through a pipe and dropping matching lines.
No-op on non-Linux systems. Safe to call multiple times; installs once.
"""
try:
if platform.system() != "Linux":
return
# Avoid double-install
if getattr(config, "_alsa_filter_installed", False):
return
import os as _os
import threading as _threading
patterns = [
"ALSA lib pcm.c:",
"snd_pcm_recover) underrun occurred",
]
# Save original stderr fd and create pipe
save_fd = _os.dup(2)
rfd, wfd = _os.pipe()
_os.dup2(wfd, 2) # redirect current process stderr to pipe writer
_os.close(wfd)
stop_event = _threading.Event()
def _reader():
try:
with _os.fdopen(rfd, 'rb', buffering=0) as r, _os.fdopen(save_fd, 'wb', buffering=0) as orig:
buf = b''
while not stop_event.is_set():
chunk = r.read(1024)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
try:
s = line.decode('utf-8', errors='ignore')
if not any(p in s for p in patterns):
orig.write(line + b"\n")
orig.flush()
except Exception:
# Swallow any decode/write errors; keep filtering
pass
if buf:
try:
s = buf.decode('utf-8', errors='ignore')
if not any(p in s for p in patterns):
orig.write(buf)
orig.flush()
except Exception:
pass
except Exception as e:
try:
# Best-effort: restore original stderr on failure
_os.dup2(save_fd, 2)
except Exception:
pass
logger.debug(f"ALSA stderr filter reader error: {e}")
t = _threading.Thread(target=_reader, daemon=True)
t.start()
def _restore():
try:
_os.dup2(save_fd, 2)
except Exception:
pass
stop_event.set()
# Expose restore in config for future use if needed
config._alsa_filter_installed = True
config._alsa_filter_restore = _restore
logger.info("ALSA underrun stderr filter installed")
except Exception as e:
logger.debug(f"Unable to install ALSA stderr filter: {e}")
def restart_application(delay_ms: int = 2000):
"""Schedule a restart with a visible popup; actual restart happens in the main loop.
@@ -181,22 +73,6 @@ _extensions_cache = None # type: ignore
_extensions_json_regenerated = False
# Détection système non-PC
def detect_non_pc():
arch = platform.machine()
try:
result = subprocess.run(["batocera-es-swissknife", "--arch"], capture_output=True, text=True, timeout=2)
if result.returncode == 0:
arch = result.stdout.strip()
#logger.debug(f"Architecture via batocera-es-swissknife: {arch}")
except (subprocess.SubprocessError, FileNotFoundError):
logger.debug(f"batocera-es-swissknife non disponible, utilisation de platform.machine(): {arch}")
is_non_pc = arch not in ["x86_64", "amd64", "AMD64"]
logger.debug(f"Système détecté: {platform.system()}, architecture: {arch}, is_non_pc={is_non_pc}")
return is_non_pc
# Fonction pour charger le fichier JSON des extensions supportées
def load_extensions_json():
"""Charge le JSON des extensions supportées.