import pygame # type: ignore import json import os import logging import config from config import CONTROLS_CONFIG_PATH from display import draw_gradient import xml.etree.ElementTree as ET logger = logging.getLogger(__name__) # Chemin du fichier de configuration des contrôles CONTROLS_CONFIG_PATH = os.path.join(config.SAVE_FOLDER, "controls.json") # Actions internes de RGSX à mapper ACTIONS = [ {"name": "confirm", "display": "Confirmer", "description": "Valider (Recommandé: Entrée, A/Croix)"}, {"name": "cancel", "display": "Annuler", "description": "Annuler/Retour (Recommandé: Retour Arrière, B/Rond)"}, {"name": "up", "display": "Haut", "description": "Naviguer vers le haut"}, {"name": "down", "display": "Bas", "description": "Naviguer vers le bas"}, {"name": "left", "display": "Gauche", "description": "Naviguer à gauche"}, {"name": "right", "display": "Droite", "description": "Naviguer à droite"}, {"name": "start", "display": "Start", "description": "Menu pause / Paramètres (Recommandé: Start, AltGr)"}, {"name": "filter", "display": "Filtrer", "description": "Ouvrir filtre (Recommandé: F, Select)"}, {"name": "page_up", "display": "Page Précédente", "description": "Page précédente/Défilement Rapide Haut (Recommandé: PageUp, LB/L1)"}, {"name": "page_down", "display": "Page Suivante", "description": "Page suivante/Défilement Rapide Bas (Recommandé: PageDown, RB/R1)"}, {"name": "history", "display": "Historique", "description": "Ouvrir l'historique (Recommandé: H, Y/Carré)"}, {"name": "progress", "display": "Progression", "description": "Historique : Effacer la liste (Recommandé: X/Triangle)"}, {"name": "delete", "display": "Supprimer", "description": "Mode Fitre : Supprimer caractère en mode recherche (Recommandé: DEL, LT/L2)"}, {"name": "space", "display": "Espace", "description": "Mode Filtre : Ajouter espace (Recommandé: Espace, RT/R2)"}, ] # Mappage des valeurs SDL vers les constantes Pygame SDL_TO_PYGAME_KEY = { 1073741906: pygame.K_UP, # Flèche Haut 1073741905: pygame.K_DOWN, # Flèche Bas 1073741904: pygame.K_LEFT, # Flèche Gauche 1073741903: pygame.K_RIGHT, # Flèche Droite 1073742050: pygame.K_LALT, # Alt gauche 1073742054: pygame.K_RALT, # Alt droit (AltGr) 1073742049: pygame.K_LCTRL, # Ctrl gauche 1073742053: pygame.K_RCTRL, # Ctrl droit 1073742048: pygame.K_LSHIFT, # Shift gauche 1073742052: pygame.K_RSHIFT, # Shift droit } # Noms lisibles pour les touches clavier KEY_NAMES = { pygame.K_RETURN: "Entrée", pygame.K_ESCAPE: "Échap", pygame.K_SPACE: "Espace", pygame.K_UP: "Flèche Haut", pygame.K_DOWN: "Flèche Bas", pygame.K_LEFT: "Flèche Gauche", pygame.K_RIGHT: "Flèche Droite", pygame.K_BACKSPACE: "Retour Arrière", pygame.K_TAB: "Tab", pygame.K_LALT: "Alt", pygame.K_RALT: "AltGR", pygame.K_LCTRL: "LCtrl", pygame.K_RCTRL: "RCtrl", pygame.K_LSHIFT: "LShift", pygame.K_RSHIFT: "RShift", pygame.K_LMETA: "LMeta", pygame.K_RMETA: "RMeta", pygame.K_CAPSLOCK: "Verr Maj", pygame.K_NUMLOCK: "Verr Num", pygame.K_SCROLLOCK: "Verr Déf", pygame.K_a: "A", pygame.K_b: "B", pygame.K_c: "C", pygame.K_d: "D", pygame.K_e: "E", pygame.K_f: "F", pygame.K_g: "G", pygame.K_h: "H", pygame.K_i: "I", pygame.K_j: "J", pygame.K_k: "K", pygame.K_l: "L", pygame.K_m: "M", pygame.K_n: "N", pygame.K_o: "O", pygame.K_p: "P", pygame.K_q: "Q", pygame.K_r: "R", pygame.K_s: "S", pygame.K_t: "T", pygame.K_u: "U", pygame.K_v: "V", pygame.K_w: "W", pygame.K_x: "X", pygame.K_y: "Y", pygame.K_z: "Z", pygame.K_0: "0", pygame.K_1: "1", pygame.K_2: "2", pygame.K_3: "3", pygame.K_4: "4", pygame.K_5: "5", pygame.K_6: "6", pygame.K_7: "7", pygame.K_8: "8", pygame.K_9: "9", pygame.K_KP0: "Pavé 0", pygame.K_KP1: "Pavé 1", pygame.K_KP2: "Pavé 2", pygame.K_KP3: "Pavé 3", pygame.K_KP4: "Pavé 4", pygame.K_KP5: "Pavé 5", pygame.K_KP6: "Pavé 6", pygame.K_KP7: "Pavé 7", pygame.K_KP8: "Pavé 8", pygame.K_KP9: "Pavé 9", pygame.K_KP_PERIOD: "Pavé .", pygame.K_KP_DIVIDE: "Pavé /", pygame.K_KP_MULTIPLY: "Pavé *", pygame.K_KP_MINUS: "Pavé -", pygame.K_KP_PLUS: "Pavé +", pygame.K_KP_ENTER: "Pavé Entrée", pygame.K_KP_EQUALS: "Pavé =", pygame.K_F1: "F1", pygame.K_F2: "F2", pygame.K_F3: "F3", pygame.K_F4: "F4", pygame.K_F5: "F5", pygame.K_F6: "F6", pygame.K_F7: "F7", pygame.K_F8: "F8", pygame.K_F9: "F9", pygame.K_F10: "F10", pygame.K_F11: "F11", pygame.K_F12: "F12", pygame.K_F13: "F13", pygame.K_F14: "F14", pygame.K_F15: "F15", pygame.K_INSERT: "Inser", pygame.K_DELETE: "Suppr", pygame.K_HOME: "Début", pygame.K_END: "Fin", pygame.K_PAGEUP: "Page Haut", pygame.K_PAGEDOWN: "Page Bas", pygame.K_PRINT: "Impr Écran", pygame.K_SYSREQ: "SysReq", pygame.K_BREAK: "Pause", pygame.K_PAUSE: "Pause", pygame.K_BACKQUOTE: "`", pygame.K_MINUS: "-", pygame.K_EQUALS: "=", pygame.K_LEFTBRACKET: "[", pygame.K_RIGHTBRACKET: "]", pygame.K_BACKSLASH: "\\", pygame.K_SEMICOLON: ";", pygame.K_QUOTE: "'", pygame.K_COMMA: ",", pygame.K_PERIOD: ".", pygame.K_SLASH: "/", } def get_controller_button_names(): """Récupère les noms des boutons depuis es_input.cfg""" es_input_path = "/usr/share/emulationstation/es_input.cfg" button_names = {} if not os.path.exists(es_input_path): return {i: f"Bouton {i}" for i in range(16)} try: tree = ET.parse(es_input_path) root = tree.getroot() # Mapping des noms ES vers des noms lisibles es_button_names = { "a": "A", "b": "B", "x": "X", "y": "Y", "leftshoulder": "LB", "rightshoulder": "RB", "lefttrigger": "LT", "righttrigger": "RT", "select": "Select", "start": "Start", "leftstick": "L3", "rightstick": "R3" } for inputConfig in root.findall("inputConfig"): if inputConfig.get("type") == "joystick": for input_tag in inputConfig.findall("input"): if input_tag.get("type") == "button": es_name = input_tag.get("name") button_id = int(input_tag.get("id")) readable_name = es_button_names.get(es_name, es_name.upper()) button_names[button_id] = readable_name break except Exception as e: logger.error(f"Erreur parsing es_input.cfg: {e}") # Compléter avec des noms génériques for i in range(16): if i not in button_names: button_names[i] = f"Bouton {i}" return button_names def get_controller_axis_names(): """Récupère les noms des axes depuis es_input.cfg""" es_input_path = "/usr/share/emulationstation/es_input.cfg" axis_names = {} if not os.path.exists(es_input_path): return {(i, d): f"Axe {i}{'+' if d > 0 else '-'}" for i in range(8) for d in [-1, 1]} try: tree = ET.parse(es_input_path) root = tree.getroot() # Mapping des noms ES vers des noms lisibles es_axis_names = { "leftx": "Joy G", "lefty": "Joy G", "rightx": "Joy D", "righty": "Joy D", "lefttrigger": "LT", "righttrigger": "RT" } for inputConfig in root.findall("inputConfig"): if inputConfig.get("type") == "joystick": for input_tag in inputConfig.findall("input"): if input_tag.get("type") == "axis": es_name = input_tag.get("name") axis_id = int(input_tag.get("id")) value = int(input_tag.get("value", "1")) direction = 1 if value > 0 else -1 if es_name in es_axis_names: base_name = es_axis_names[es_name] if "Joy" in base_name: if "leftx" in es_name or "rightx" in es_name: axis_names[(axis_id, direction)] = f"{base_name} {'Droite' if direction > 0 else 'Gauche'}" else: axis_names[(axis_id, direction)] = f"{base_name} {'Bas' if direction > 0 else 'Haut'}" else: axis_names[(axis_id, direction)] = base_name break except Exception as e: logger.error(f"Erreur parsing es_input.cfg: {e}") # Compléter avec des noms génériques for i in range(8): for d in [-1, 1]: if (i, d) not in axis_names: axis_names[(i, d)] = f"Axe {i}{'+' if d > 0 else '-'}" return axis_names # Charger les noms depuis es_input.cfg BUTTON_NAMES = get_controller_button_names() AXIS_NAMES = get_controller_axis_names() # Noms pour la croix directionnelle HAT_NAMES = { (0, 1): "D-Pad Haut", (0, -1): "D-Pad Bas", (-1, 0): "D-Pad Gauche", (1, 0): "D-Pad Droite", } # Noms pour les boutons de souris MOUSE_BUTTON_NAMES = { 1: "Clic Gauche", 2: "Clic Milieu", 3: "Clic Droit", } # Durée de maintien pour valider une entrée (en millisecondes) HOLD_DURATION = 1000 JOYHAT_DEBOUNCE = 200 # Délai anti-rebond pour JOYHATMOTION (ms) def load_controls_config(): """Charge la configuration des contrôles depuis controls.json ou EmulationStation""" try: if os.path.exists(CONTROLS_CONFIG_PATH): with open(CONTROLS_CONFIG_PATH, "r") as f: config = json.load(f) logger.debug(f"Configuration des contrôles chargée : {config}") return config else: logger.debug("Aucun fichier controls.json trouvé, tentative d'importation depuis EmulationStation") # Essayer d'importer depuis EmulationStation from es_input_parser import parse_es_input_config es_config = parse_es_input_config() if es_config: logger.info("Configuration importée depuis EmulationStation") save_controls_config(es_config) return es_config else: logger.debug("Importation depuis EmulationStation échouée, configuration par défaut") return {} except Exception as e: logger.error(f"Erreur lors du chargement de controls.json : {e}") return {} def save_controls_config(controls_config): """Enregistre la configuration des contrôles dans controls.json""" try: os.makedirs(os.path.dirname(CONTROLS_CONFIG_PATH), exist_ok=True) with open(CONTROLS_CONFIG_PATH, "w") as f: json.dump(controls_config, f, indent=4) logger.debug(f"Configuration des contrôles enregistrée : {controls_config}") except Exception as e: logger.error(f"Erreur lors de l'enregistrement de controls.json : {e}") def get_readable_input_name(event): """Retourne un nom lisible pour une entrée (touche, bouton, axe, hat, ou souris)""" if event.type == pygame.KEYDOWN: key_value = SDL_TO_PYGAME_KEY.get(event.key, event.key) return KEY_NAMES.get(key_value, pygame.key.name(key_value) or f"Touche {key_value}") elif event.type == pygame.JOYBUTTONDOWN: return BUTTON_NAMES.get(event.button, f"Bouton {event.button}") elif event.type == pygame.JOYAXISMOTION: if abs(event.value) > 0.5: # Seuil pour détecter un mouvement significatif return AXIS_NAMES.get((event.axis, 1 if event.value > 0 else -1), f"Axe {event.axis} {'Positif' if event.value > 0 else 'Négatif'}") elif event.type == pygame.JOYHATMOTION: if event.value != (0, 0): # Ignorer la position neutre return HAT_NAMES.get(event.value, f"D-Pad {event.value}") elif event.type == pygame.MOUSEBUTTONDOWN: return MOUSE_BUTTON_NAMES.get(event.button, f"Souris Bouton {event.button}") return "Inconnu" def map_controls(screen): """Interface de mappage des contrôles avec maintien de 3 secondes""" controls_config = load_controls_config() current_action_index = 0 current_input = None input_held_time = 0 last_input_name = None last_frame_time = pygame.time.get_ticks() config.needs_redraw = True last_joyhat_time = 0 # État des entrées maintenues held_keys = set() held_buttons = set() held_axes = {} held_hats = {} held_mouse_buttons = set() while current_action_index < len(ACTIONS): if config.needs_redraw: progress = min(input_held_time / HOLD_DURATION, 1.0) if current_input else 0.0 draw_controls_mapping(screen, ACTIONS[current_action_index], last_input_name, current_input is not None, progress) pygame.display.flip() config.needs_redraw = False current_time = pygame.time.get_ticks() delta_time = current_time - last_frame_time last_frame_time = current_time for event in pygame.event.get(): if event.type == pygame.QUIT: return False # Gestion des relâchements if event.type == pygame.KEYUP and event.key in held_keys: held_keys.remove(event.key) if current_input and current_input["type"] == "key" and current_input["value"] == event.key: current_input = None input_held_time = 0 last_input_name = None config.needs_redraw = True elif event.type == pygame.JOYBUTTONUP and event.button in held_buttons: held_buttons.remove(event.button) if current_input and current_input["type"] == "button" and current_input["value"] == event.button: current_input = None input_held_time = 0 last_input_name = None config.needs_redraw = True elif event.type == pygame.JOYAXISMOTION and abs(event.value) < 0.5: if event.axis in held_axes: held_direction = held_axes[event.axis] if current_input and current_input["type"] == "axis" and current_input["value"][0] == event.axis and current_input["value"][1] == held_direction: current_input = None input_held_time = 0 last_input_name = None config.needs_redraw = True del held_axes[event.axis] elif event.type == pygame.JOYHATMOTION and event.value == (0, 0): if event.hat in held_hats: del held_hats[event.hat] if current_input and current_input["type"] == "hat": current_input = None input_held_time = 0 last_input_name = None config.needs_redraw = True continue elif event.type == pygame.MOUSEBUTTONUP and event.button in held_mouse_buttons: held_mouse_buttons.remove(event.button) if current_input and current_input["type"] == "mouse" and current_input["value"] == event.button: current_input = None input_held_time = 0 last_input_name = None config.needs_redraw = True # Détection des nouvelles entrées if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN): if event.type == pygame.JOYHATMOTION: if (current_time - last_joyhat_time) < JOYHAT_DEBOUNCE: continue last_joyhat_time = current_time input_name = get_readable_input_name(event) if input_name == "Inconnu": continue # Déterminer le type et la valeur if event.type == pygame.KEYDOWN: input_type = "key" input_value = SDL_TO_PYGAME_KEY.get(event.key, event.key) elif event.type == pygame.JOYBUTTONDOWN: input_type = "button" input_value = event.button elif event.type == pygame.JOYAXISMOTION and abs(event.value) > 0.5: input_type = "axis" direction = 1 if event.value > 0 else -1 input_value = (event.axis, direction) # Ignorer si c'est juste un changement de direction du même axe if event.axis in held_axes and held_axes[event.axis] != direction: continue elif event.type == pygame.JOYHATMOTION: input_type = "hat" input_value = event.value elif event.type == pygame.MOUSEBUTTONDOWN: input_type = "mouse" input_value = event.button else: continue # Nouvelle entrée détectée if (current_input is None or current_input["type"] != input_type or current_input["value"] != input_value): current_input = {"type": input_type, "value": input_value} input_held_time = 0 last_input_name = input_name config.needs_redraw = True # Mettre à jour les entrées maintenues if input_type == "key": held_keys.add(input_value) elif input_type == "button": held_buttons.add(input_value) elif input_type == "axis": held_axes[input_value[0]] = input_value[1] elif input_type == "hat": held_hats[event.hat] = input_value elif input_type == "mouse": held_mouse_buttons.add(input_value) # Mise à jour du temps de maintien if current_input: input_held_time += delta_time if input_held_time >= HOLD_DURATION: action_name = ACTIONS[current_action_index]["name"] # Sauvegarder avec la structure attendue par controls.py if current_input["type"] == "key": controls_config[action_name] = { "type": "key", "key": current_input["value"], "display": last_input_name } elif current_input["type"] == "button": controls_config[action_name] = { "type": "button", "button": current_input["value"], "display": last_input_name } elif current_input["type"] == "axis": axis, direction = current_input["value"] controls_config[action_name] = { "type": "axis", "axis": axis, "direction": direction, "display": last_input_name } elif current_input["type"] == "hat": controls_config[action_name] = { "type": "hat", "value": current_input["value"], "display": last_input_name } elif current_input["type"] == "mouse": controls_config[action_name] = { "type": "mouse", "button": current_input["value"], "display": last_input_name } logger.debug(f"Contrôle mappé: {action_name} -> {controls_config[action_name]}") current_action_index += 1 current_input = None input_held_time = 0 last_input_name = None config.needs_redraw = True # Réinitialiser les entrées maintenues held_keys.clear() held_buttons.clear() held_axes.clear() held_hats.clear() held_mouse_buttons.clear() config.needs_redraw = True pygame.time.wait(10) save_controls_config(controls_config) config.controls_config = controls_config return True def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_progress): #Affiche l'interface de mappage des contrôles avec une barre de progression pour le maintien draw_gradient(screen, (28, 37, 38), (47, 59, 61)) # Paramètres de l'interface padding_horizontal = 40 padding_vertical = 30 padding_between = 15 border_radius = 24 border_width = 4 shadow_offset = 8 # Titre principal title_text = "Configuration des contrôles" title_surface = config.title_font.render(title_text, True, (255, 255, 255)) title_rect = title_surface.get_rect(center=(config.screen_width // 2, 80)) screen.blit(title_surface, title_rect) # Instructions instruction_text = "Maintenez pendant 3s pour configurer :" description_text = action['description'] instruction_surface = config.small_font.render(instruction_text, True, (255, 255, 255)) description_surface = config.font.render(description_text, True, (200, 200, 200)) instruction_width, instruction_height = instruction_surface.get_size() description_width, description_height = description_surface.get_size() # Input détecté input_text = last_input or (f"En attente d'une touche ou bouton..." if waiting_for_input else "Appuyez sur une touche ou un bouton") input_surface = config.small_font.render(input_text, True, (0, 255, 0) if last_input else (255, 255, 255)) input_width, input_height = input_surface.get_size() # Dimensions de la popup text_width = max(instruction_width, description_width, input_width) text_height = instruction_height + description_height + input_height + 2 * padding_between popup_width = text_width + 2 * padding_horizontal popup_height = text_height + 40 + 2 * padding_vertical # +40 pour la barre de progression popup_x = (config.screen_width - popup_width) // 2 popup_y = (config.screen_height - popup_height) // 2 # Ombre portée shadow_rect = pygame.Rect(popup_x + shadow_offset, popup_y + shadow_offset, popup_width, popup_height) shadow_surface = pygame.Surface((popup_width, popup_height), pygame.SRCALPHA) pygame.draw.rect(shadow_surface, (0, 0, 0, 100), shadow_surface.get_rect(), border_radius=border_radius) screen.blit(shadow_surface, shadow_rect.topleft) # Fond semi-transparent popup_rect = pygame.Rect(popup_x, popup_y, popup_width, popup_height) popup_surface = pygame.Surface((popup_width, popup_height), pygame.SRCALPHA) pygame.draw.rect(popup_surface, (30, 30, 30, 220), popup_surface.get_rect(), border_radius=border_radius) screen.blit(popup_surface, popup_rect.topleft) # Bordure blanche pygame.draw.rect(screen, (255, 255, 255), popup_rect, border_width, border_radius=border_radius) # Afficher les textes start_y = popup_y + padding_vertical instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, start_y + instruction_height // 2)) screen.blit(instruction_surface, instruction_rect) start_y += instruction_height + padding_between description_rect = description_surface.get_rect(center=(config.screen_width // 2, start_y + description_height // 2)) screen.blit(description_surface, description_rect) start_y += description_height + padding_between input_rect = input_surface.get_rect(center=(config.screen_width // 2, start_y + input_height // 2)) screen.blit(input_surface, input_rect) start_y += input_height + padding_between # Barre de progression pour le maintien bar_width = 300 bar_height = 25 bar_x = (config.screen_width - bar_width) // 2 bar_y = start_y + 20 pygame.draw.rect(screen, (50, 50, 50), (bar_x, bar_y, bar_width, bar_height)) progress_width = bar_width * hold_progress pygame.draw.rect(screen, (0, 255, 0), (bar_x, bar_y, progress_width, bar_height)) pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 2) # Afficher le pourcentage de progression if hold_progress > 0: progress_text = f"{int(hold_progress * 100)}%" progress_surface = config.small_font.render(progress_text, True, (255, 255, 255)) progress_rect = progress_surface.get_rect(center=(config.screen_width // 2, bar_y + bar_height + 30)) screen.blit(progress_surface, progress_rect)