diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index 2a36690..f4f0056 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -16,7 +16,6 @@ import datetime import subprocess import sys import config -import shutil from display import ( init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, @@ -64,7 +63,6 @@ try: # pragma: no cover logger.debug("API key files ensured at startup") except Exception as _e: logger.warning(f"Cannot prepare API key files early: {_e}") - # Mise à jour de la gamelist Windows avant toute initialisation graphique (évite les conflits avec ES) def _run_windows_gamelist_update(): try: @@ -198,6 +196,15 @@ for i in range(count): joystick_names.append(j.get_name()) except Exception as e: logger.debug(f"Impossible de lire le nom du joystick {i}: {e}") + +# Enregistrer le nom du premier joystick détecté pour l'auto-préréglage +try: + if joystick_names: + config.controller_device_name = joystick_names[0] + else: + config.controller_device_name = "" +except Exception: + pass normalized_names = [n.lower() for n in joystick_names] if not joystick_names: joystick_names = ["Clavier"] @@ -205,73 +212,12 @@ if not joystick_names: logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.") config.joystick = False config.keyboard = True - # Si aucune marque spécifique détectée mais un joystick est présent, marquer comme générique - if not any([config.xbox_controller, config.playstation_controller, config.nintendo_controller, - config.eightbitdo_controller, config.steam_controller, config.trimui_controller, - config.logitech_controller]): - config.generic_controller = True - logger.debug("Aucun contrôleur spécifique détecté, utilisation du profil générique") else: - # Des joysticks sont présents, activer le mode joystick et tenter la détection spécifique + # Des joysticks sont présents: activer le mode joystick et mémoriser le nom pour l'auto-préréglage config.joystick = True config.keyboard = False - print(f"Joysticks détectés: YES") - logger.debug(f"Joysticks détectés: YES") - for idx, name in enumerate(joystick_names): - lname = name.lower() - # Détection Anbernic RG35XX - if ("rg35xx" in lname): - config.anbernic_rg35xx_controller = True - logger.debug(f"Anbernic Controller detected : {name}") - print(f"Controller detected : {name}") - break - # Détection spécifique Elite AVANT la détection générique Xbox - elif ("microsoft xbox controller" in lname): - config.xbox_elite_controller = True - logger.debug(f"Controller detected: {name}") - print(f"Controller detected: {name}") - break - elif ("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"Xbox Controller detected : {name}") - print(f"Controller detected : {name}") - break - elif "playstation" in lname or "ps3" in lname or "sony" in lname: - config.playstation_controller = True - logger.debug(f"Playstation Controller detected : {name}") - print(f"Controller detected : {name}") - break - elif "nintendo" in lname: - config.nintendo_controller = True - logger.debug(f"Nintendo Controller detected : {name}") - print(f"Controller detected : {name}") - break - elif "trimui" in lname: - config.trimui_controller = True - logger.debug(f"Trimui Controller detected : {name}") - print(f"Controller detected : {name}") - break - elif "logitech" in lname: - config.logitech_controller = True - logger.debug(f"Logitech Controller detected : {name}") - print(f"Controller detected : {name}") - break - elif "8bitdo" in lname or "8-bitdo" in lname: - config.eightbitdo_controller = True - logger.debug(f"8bitdoController detected : {name}") - print(f"Controller detected : {name}") - break - elif "steam" in lname: - config.steam_controller = True - logger.debug(f"Steam Controller detected : {name}") - print(f"Controller detected : {name}") - else: - # Si aucune marque spécifique détectée mais un joystick est présent, marquer comme générique - config.generic_controller = True - logger.debug(f"Generic Controller detected : {name}") - print(f"Generic Controller detected : {name}") - # Note: virtual keyboard display now depends on controller presence (config.joystick) - 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}") + print("Joystick détecté:", ", ".join(joystick_names)) + logger.debug(f"Joysticks détectés: {joystick_names}") diff --git a/ports/RGSX/assets/controls/nintendo_controller.json b/ports/RGSX/assets/controls/nintendo_controller.json deleted file mode 100644 index cdcd087..0000000 --- a/ports/RGSX/assets/controls/nintendo_controller.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "confirm": { - "type": "button", - "button": 1, - "display": "B" - }, - "cancel": { - "type": "button", - "button": 0, - "display": "A" - }, - "up": { - "type": "hat", - "value": [0, 1], - "display": "↑" - }, - "down": { - "type": "hat", - "value": [0, -1], - "display": "↓" - }, - "left": { - "type": "hat", - "value": [-1, 0], - "display": "←" - }, - "right": { - "type": "hat", - "value": [1, 0], - "display": "→" - }, - "start": { - "type": "button", - "button": 7, - "display": "Start" - }, - "filter": { - "type": "button", - "button": 6, - "display": "Select" - }, - "page_up": { - "type": "axis", - "axis": 4, - "direction": 1, - "display": "ZR" - }, - "page_down": { - "type": "axis", - "axis": 5, - "direction": -1, - "display": "ZL" - }, - "history": { - "type": "button", - "button": 3, - "display": "Y" - }, - "clear_history": { - "type": "button", - "button": 2, - "display": "X" - }, - "delete": { - "type": "button", - "button": 4, - "display": "L" - }, - "space": { - "type": "button", - "button": 5, - "display": "R" - } -} diff --git a/ports/RGSX/assets/controls/ps5_dualsense.json b/ports/RGSX/assets/controls/ps5_dualsense.json new file mode 100644 index 0000000..c7feaa5 --- /dev/null +++ b/ports/RGSX/assets/controls/ps5_dualsense.json @@ -0,0 +1,75 @@ +{ + "device": "DualSense Wireless Controller", + "up": { + "type": "button", + "button": 11, + "display": "\u2191" + }, + "down": { + "type": "button", + "button": 12, + "display": "\u2193" + }, + "left": { + "type": "button", + "button": 13, + "display": "\u2190" + }, + "right": { + "type": "button", + "button": 14, + "display": "\u2192" + }, + "confirm": { + "type": "button", + "button": 0, + "display": "A" + }, + "cancel": { + "type": "button", + "button": 1, + "display": "B" + }, + "history": { + "type": "button", + "button": 3, + "display": "Y" + }, + "clear_history": { + "type": "button", + "button": 2, + "display": "X" + }, + "start": { + "type": "button", + "button": 6, + "display": "Start" + }, + "filter": { + "type": "button", + "button": 4, + "display": "Select" + }, + "delete": { + "type": "button", + "button": 9, + "display": "LB" + }, + "space": { + "type": "button", + "button": 10, + "display": "RB" + }, + "page_up": { + "type": "axis", + "axis": 4, + "direction": 1, + "display": "LT" + }, + "page_down": { + "type": "axis", + "axis": 5, + "direction": 1, + "display": "RT" + } +} \ No newline at end of file diff --git a/ports/RGSX/assets/controls/retroid_pocket_flip_2.json b/ports/RGSX/assets/controls/retroid_pocket_flip_2.json new file mode 100644 index 0000000..5227612 --- /dev/null +++ b/ports/RGSX/assets/controls/retroid_pocket_flip_2.json @@ -0,0 +1,20 @@ +{ + "device": "Retroid Pocket Controller", + "confirm": { "type": "button", "button": 1, "display": "A" }, + "cancel": { "type": "button", "button": 2, "display": "B" }, + "clear_history": { "type": "button", "button": 4, "display": "X" }, + "history": { "type": "button", "button": 3, "display": "Y" }, + "start": { "type": "button", "button": 8, "display": "Start" }, + "filter": { "type": "button", "button": 7, "display": "Select" }, + + "up": { "type": "button", "button": 12, "display": "↑" }, + "down": { "type": "button", "button": 13, "display": "↓" }, + "left": { "type": "button", "button": 14, "display": "←" }, + "right": { "type": "button", "button": 15, "display": "→" }, + + "delete": { "type": "button", "button": 5, "display": "LB" }, + "space": { "type": "button", "button": 6, "display": "RB" }, + + "page_up": { "type": "axis", "axis": 2, "direction": -1, "display": "LT" }, + "page_down": { "type": "axis", "axis": 5, "direction": -1, "display": "RT" } +} diff --git a/ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json b/ports/RGSX/assets/controls/rg34xx_sp.json similarity index 97% rename from ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json rename to ports/RGSX/assets/controls/rg34xx_sp.json index 412b94a..02a6acc 100644 --- a/ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json +++ b/ports/RGSX/assets/controls/rg34xx_sp.json @@ -1,4 +1,5 @@ { + "device": "RG34XX-SP Controller", "confirm": { "type": "button", "button": 3, diff --git a/ports/RGSX/assets/controls/rgb10_ogs.json b/ports/RGSX/assets/controls/rgb10_ogs.json new file mode 100644 index 0000000..173a7f1 --- /dev/null +++ b/ports/RGSX/assets/controls/rgb10_ogs.json @@ -0,0 +1,20 @@ +{ + "device": "GO-Super Gamepad", + "confirm": { "type": "button", "button": 0, "display": "A" }, + "cancel": { "type": "button", "button": 1, "display": "B" }, + "clear_history": { "type": "button", "button": 3, "display": "X" }, + "history": { "type": "button", "button": 2, "display": "Y" }, + "start": { "type": "button", "button": 13, "display": "Start" }, + "filter": { "type": "button", "button": 12, "display": "Select" }, + + "up": { "type": "button", "button": 8, "display": "↑" }, + "down": { "type": "button", "button": 9, "display": "↓" }, + "left": { "type": "button", "button": 10, "display": "←" }, + "right": { "type": "button", "button": 11, "display": "→" }, + + "delete": { "type": "button", "button": 6, "display": "LB" }, + "space": { "type": "button", "button": 7, "display": "RB" }, + + "page_up": { "type": "button", "button": 4, "display": "LT" }, + "page_down": { "type": "button", "button": 5, "display": "RT" } +} diff --git a/ports/RGSX/assets/controls/steam_controller.json b/ports/RGSX/assets/controls/steam_deck.json similarity index 98% rename from ports/RGSX/assets/controls/steam_controller.json rename to ports/RGSX/assets/controls/steam_deck.json index 6cf3cf0..d423e78 100644 --- a/ports/RGSX/assets/controls/steam_controller.json +++ b/ports/RGSX/assets/controls/steam_deck.json @@ -1,4 +1,5 @@ { + "device": "Steam Deck", "confirm": { "type": "button", "button": 3, diff --git a/ports/RGSX/assets/controls/trimui_controller.json b/ports/RGSX/assets/controls/trimui_smart_pro.json similarity index 97% rename from ports/RGSX/assets/controls/trimui_controller.json rename to ports/RGSX/assets/controls/trimui_smart_pro.json index 0003858..fe38054 100644 --- a/ports/RGSX/assets/controls/trimui_controller.json +++ b/ports/RGSX/assets/controls/trimui_smart_pro.json @@ -1,4 +1,5 @@ { + "device": "TRIMUI Smart Pro Controller", "confirm": { "type": "button", "button": 0, diff --git a/ports/RGSX/assets/controls/xbox_controller.json b/ports/RGSX/assets/controls/xbox_360_emulated.json similarity index 96% rename from ports/RGSX/assets/controls/xbox_controller.json rename to ports/RGSX/assets/controls/xbox_360_emulated.json index d79f9d0..d906409 100644 --- a/ports/RGSX/assets/controls/xbox_controller.json +++ b/ports/RGSX/assets/controls/xbox_360_emulated.json @@ -1,4 +1,5 @@ { + "device": "XBOX 360 For Windows (Controller)", "confirm": { "type": "button", "button": 0, diff --git a/ports/RGSX/assets/controls/eightbitdo_controller.json b/ports/RGSX/assets/controls/xbox_360_original.json similarity index 73% rename from ports/RGSX/assets/controls/eightbitdo_controller.json rename to ports/RGSX/assets/controls/xbox_360_original.json index d050482..8ca5df5 100644 --- a/ports/RGSX/assets/controls/eightbitdo_controller.json +++ b/ports/RGSX/assets/controls/xbox_360_original.json @@ -1,4 +1,37 @@ { + "device": "Xbox 360 Controller", + "up": { + "type": "hat", + "value": [ + 0, + 1 + ], + "display": "\u2191" + }, + "down": { + "type": "hat", + "value": [ + 0, + -1 + ], + "display": "\u2193" + }, + "left": { + "type": "hat", + "value": [ + -1, + 0 + ], + "display": "\u2190" + }, + "right": { + "type": "hat", + "value": [ + 1, + 0 + ], + "display": "\u2192" + }, "confirm": { "type": "button", "button": 0, @@ -9,48 +42,6 @@ "button": 1, "display": "B" }, - "up": { - "type": "hat", - "value": [0, 1], - "display": "↑" - }, - "down": { - "type": "hat", - "value": [0, -1], - "display": "↓" - }, - "left": { - "type": "hat", - "value": [-1, 0], - "display": "←" - }, - "right": { - "type": "hat", - "value": [1, 0], - "display": "→" - }, - "start": { - "type": "button", - "button": 7, - "display": "Start" - }, - "filter": { - "type": "button", - "button": 6, - "display": "Select" - }, - "page_up": { - "type": "axis", - "axis": 4, - "direction": 1, - "display": "RT" - }, - "page_down": { - "type": "axis", - "axis": 5, - "direction": -1, - "display": "LT" - }, "history": { "type": "button", "button": 3, @@ -61,6 +52,16 @@ "button": 2, "display": "X" }, + "start": { + "type": "button", + "button": 7, + "display": "Start" + }, + "filter": { + "type": "button", + "button": 6, + "display": "Select" + }, "delete": { "type": "button", "button": 4, @@ -70,5 +71,17 @@ "type": "button", "button": 5, "display": "RB" + }, + "page_up": { + "type": "axis", + "axis": 4, + "direction": -1, + "display": "LT" + }, + "page_down": { + "type": "axis", + "axis": 5, + "direction": -1, + "display": "RT" } -} +} \ No newline at end of file diff --git a/ports/RGSX/assets/controls/xbox_elite_controller.json b/ports/RGSX/assets/controls/xbox_elite.json similarity index 80% rename from ports/RGSX/assets/controls/xbox_elite_controller.json rename to ports/RGSX/assets/controls/xbox_elite.json index 3592391..3d3caf5 100644 --- a/ports/RGSX/assets/controls/xbox_elite_controller.json +++ b/ports/RGSX/assets/controls/xbox_elite.json @@ -1,4 +1,5 @@ { + "device": "Microsoft Xbox Controller", "confirm": { "type": "button", "button": 1, "display": "A" }, "cancel": { "type": "button", "button": 2, "display": "B" }, "clear_history": { "type": "button", "button": 3, "display": "X" }, @@ -12,8 +13,5 @@ "left": { "type": "hat", "value": [-1, 0], "display": "\u2190" }, "right": { "type": "hat", "value": [1, 0], "display": "\u2192" }, "page_up": { "type": "axis", "axis": 5, "direction": -1, "display": "RT" }, - "page_down": { "type": "axis", "axis": 2, "direction": -1, "display": "LT" }, - "meta": { - "notes": "Mapping spécifique Xbox Elite basé sur log fourni. Triggers décalés: LEFT_TRIGGER=AXIS2 -, RIGHT_TRIGGER=AXIS5 -. Les boutons semblent décalés de +1 vs profil 360 standard." - } + "page_down": { "type": "axis", "axis": 2, "direction": -1, "display": "LT" } } diff --git a/ports/RGSX/assets/controls/generic_controller.json b/ports/RGSX/assets/controls/zero_plus.json similarity index 71% rename from ports/RGSX/assets/controls/generic_controller.json rename to ports/RGSX/assets/controls/zero_plus.json index d050482..5f66c58 100644 --- a/ports/RGSX/assets/controls/generic_controller.json +++ b/ports/RGSX/assets/controls/zero_plus.json @@ -1,4 +1,25 @@ { + "device": "ZEROPLUS Controller", + "up": { + "type": "button", + "button": 11, + "display": "\u2191" + }, + "down": { + "type": "button", + "button": 12, + "display": "\u2193" + }, + "left": { + "type": "button", + "button": 13, + "display": "\u2190" + }, + "right": { + "type": "button", + "button": 14, + "display": "\u2192" + }, "confirm": { "type": "button", "button": 0, @@ -9,48 +30,6 @@ "button": 1, "display": "B" }, - "up": { - "type": "hat", - "value": [0, 1], - "display": "↑" - }, - "down": { - "type": "hat", - "value": [0, -1], - "display": "↓" - }, - "left": { - "type": "hat", - "value": [-1, 0], - "display": "←" - }, - "right": { - "type": "hat", - "value": [1, 0], - "display": "→" - }, - "start": { - "type": "button", - "button": 7, - "display": "Start" - }, - "filter": { - "type": "button", - "button": 6, - "display": "Select" - }, - "page_up": { - "type": "axis", - "axis": 4, - "direction": 1, - "display": "RT" - }, - "page_down": { - "type": "axis", - "axis": 5, - "direction": -1, - "display": "LT" - }, "history": { "type": "button", "button": 3, @@ -61,14 +40,36 @@ "button": 2, "display": "X" }, - "delete": { + "start": { + "type": "button", + "button": 6, + "display": "Start" + }, + "filter": { "type": "button", "button": 4, + "display": "Select" + }, + "delete": { + "type": "button", + "button": 9, "display": "LB" }, "space": { "type": "button", - "button": 5, + "button": 10, "display": "RB" + }, + "page_up": { + "type": "axis", + "axis": 4, + "direction": 1, + "display": "LT" + }, + "page_down": { + "type": "axis", + "axis": 5, + "direction": 1, + "display": "RT" } -} +} \ No newline at end of file diff --git a/ports/RGSX/assets/Pixel-UniCode.ttf b/ports/RGSX/assets/fonts/Pixel-UniCode.ttf similarity index 100% rename from ports/RGSX/assets/Pixel-UniCode.ttf rename to ports/RGSX/assets/fonts/Pixel-UniCode.ttf diff --git a/ports/RGSX/assets/images/button_l.svg b/ports/RGSX/assets/images/button_l.svg new file mode 100644 index 0000000..07dc3b0 --- /dev/null +++ b/ports/RGSX/assets/images/button_l.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ports/RGSX/assets/images/button_lt.svg b/ports/RGSX/assets/images/button_lt.svg new file mode 100644 index 0000000..0c4a212 --- /dev/null +++ b/ports/RGSX/assets/images/button_lt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ports/RGSX/assets/images/button_r.svg b/ports/RGSX/assets/images/button_r.svg new file mode 100644 index 0000000..8db5274 --- /dev/null +++ b/ports/RGSX/assets/images/button_r.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ports/RGSX/assets/images/button_rt.svg b/ports/RGSX/assets/images/button_rt.svg new file mode 100644 index 0000000..15f0fd0 --- /dev/null +++ b/ports/RGSX/assets/images/button_rt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ports/RGSX/assets/images/button_select.svg b/ports/RGSX/assets/images/button_select.svg new file mode 100644 index 0000000..eba861d --- /dev/null +++ b/ports/RGSX/assets/images/button_select.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/button_start.svg b/ports/RGSX/assets/images/button_start.svg new file mode 100644 index 0000000..78fb563 --- /dev/null +++ b/ports/RGSX/assets/images/button_start.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/buttons_east.svg b/ports/RGSX/assets/images/buttons_east.svg new file mode 100644 index 0000000..9c220e6 --- /dev/null +++ b/ports/RGSX/assets/images/buttons_east.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/buttons_north.svg b/ports/RGSX/assets/images/buttons_north.svg new file mode 100644 index 0000000..7813ee2 --- /dev/null +++ b/ports/RGSX/assets/images/buttons_north.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/buttons_south.svg b/ports/RGSX/assets/images/buttons_south.svg new file mode 100644 index 0000000..5705926 --- /dev/null +++ b/ports/RGSX/assets/images/buttons_south.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/buttons_west.svg b/ports/RGSX/assets/images/buttons_west.svg new file mode 100644 index 0000000..e1d5a4a --- /dev/null +++ b/ports/RGSX/assets/images/buttons_west.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/dpad_down.svg b/ports/RGSX/assets/images/dpad_down.svg new file mode 100644 index 0000000..84a41fc --- /dev/null +++ b/ports/RGSX/assets/images/dpad_down.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/dpad_left.svg b/ports/RGSX/assets/images/dpad_left.svg new file mode 100644 index 0000000..1b018d6 --- /dev/null +++ b/ports/RGSX/assets/images/dpad_left.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/dpad_right.svg b/ports/RGSX/assets/images/dpad_right.svg new file mode 100644 index 0000000..228f5e0 --- /dev/null +++ b/ports/RGSX/assets/images/dpad_right.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ports/RGSX/assets/images/dpad_up.svg b/ports/RGSX/assets/images/dpad_up.svg new file mode 100644 index 0000000..eb6dc0c --- /dev/null +++ b/ports/RGSX/assets/images/dpad_up.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ports/RGSX/assets/unrar.exe b/ports/RGSX/assets/progs/unrar.exe similarity index 100% rename from ports/RGSX/assets/unrar.exe rename to ports/RGSX/assets/progs/unrar.exe diff --git a/ports/RGSX/assets/xdvdfs b/ports/RGSX/assets/progs/xdvdfs similarity index 100% rename from ports/RGSX/assets/xdvdfs rename to ports/RGSX/assets/progs/xdvdfs diff --git a/ports/RGSX/assets/xdvdfs.exe b/ports/RGSX/assets/progs/xdvdfs.exe similarity index 100% rename from ports/RGSX/assets/xdvdfs.exe rename to ports/RGSX/assets/progs/xdvdfs.exe diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index 1a19a76..2dab57b 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -13,7 +13,7 @@ except Exception: pygame = None # type: ignore # Version actuelle de l'application -app_version = "2.2.2.5" +app_version = "2.2.2.6" def get_application_root(): @@ -78,9 +78,9 @@ OTA_UPDATE_ZIP = os.path.join(OTA_SERVER_URL, "RGSX.zip") OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "games.zip") #CHEMINS DES EXECUTABLES -UNRAR_EXE = os.path.join(APP_FOLDER,"assets", "unrar.exe") -XDVDFS_EXE = os.path.join(APP_FOLDER,"assets", "xdvdfs.exe") -XDVDFS_LINUX = os.path.join(APP_FOLDER,"assets", "xdvdfs") +UNRAR_EXE = os.path.join(APP_FOLDER,"assets","progs","unrar.exe") +XDVDFS_EXE = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs.exe") +XDVDFS_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs") if not HEADLESS: # Print des chemins pour debug @@ -191,6 +191,7 @@ trimui_controller = False generic_controller = False xbox_elite_controller = False # Flag spécifique manette Xbox Elite anbernic_rg35xx_controller = False # Flag spécifique Anbernic RG3xxx +controller_device_name = "" # Nom exact du joystick détecté (pour auto-préréglages) # --- Filtre plateformes (UI) --- selected_filter_index = 0 # index dans la liste visible triée @@ -253,7 +254,7 @@ def init_font(): search_size = 48 small_size = 28 if fam == "pixel": - path = os.path.join(APP_FOLDER, "assets", "Pixel-UniCode.ttf") + path = os.path.join(APP_FOLDER, "assets", "fonts", "Pixel-UniCode.ttf") f = pygame.font.Font(path, int(base_size * font_scale)) t = pygame.font.Font(path, int(title_size * font_scale)) s = pygame.font.Font(path, int(search_size * font_scale)) diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 5b57689..1b5c173 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -2,6 +2,7 @@ import pygame # type: ignore import shutil import asyncio import json +import re import os import config from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE @@ -96,44 +97,65 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH): # 2) Préréglages sans copie si aucun fichier utilisateur try: - candidates = [] - # Si aucun contrôleur détecté, privilégier le préréglage clavier - if not getattr(config, 'joystick', False) or getattr(config, 'keyboard', False): - candidates.append('keyboard.json') - # Déterminer les préréglages disponibles selon les flags détectés au démarrage - if getattr(config, 'steam_controller', False): - candidates.append('steam_controller.json') - if getattr(config, 'trimui_controller', False): - candidates.append('trimui_controller.json') - if getattr(config, 'xbox_elite_controller', False): - candidates.append('xbox_elite_controller.json') - elif getattr(config, 'xbox_controller', False): - candidates.append('xbox_controller.json') - if getattr(config, 'nintendo_controller', False): - candidates.append('nintendo_controller.json') - if getattr(config, 'eightbitdo_controller', False): - candidates.append('8bitdo_controller.json') - if getattr(config, 'anbernic_rg35xx_controller', False): - candidates.append('anbernic_rg34xx_sp_controller.json') - # Fallbacks génériques - if 'generic_controller.json' not in candidates: - candidates.append('generic_controller.json') - if 'xbox_controller.json' not in candidates: - candidates.append('xbox_controller.json') + # --- Auto-match par nom de périphérique détecté --- + def _sanitize(s: str) -> str: + s = (s or "").strip().lower() + s = re.sub(r"[^a-z0-9]+", "_", s) + s = re.sub(r"_+", "_", s).strip("_") + return s - for fname in candidates: - src = os.path.join(config.PRECONF_CONTROLS_PATH, fname) + def _extract_device_from_comment(val: str) -> str: + try: + if not isinstance(val, str): + return "" + # Expect formats like "# Device: NAME" or just NAME + if "Device:" in val: + part = val.split("Device:", 1)[1] + return part.strip().lstrip('#').strip() + return val.strip().lstrip('#').strip() + except Exception: + return "" + + device_name = getattr(config, 'controller_device_name', '') or '' + if getattr(config, 'joystick', False) and device_name: + target_norm = _sanitize(device_name) + try: + for fname in os.listdir(config.PRECONF_CONTROLS_PATH): + if not fname.lower().endswith('.json'): + continue + src = os.path.join(config.PRECONF_CONTROLS_PATH, fname) + try: + with open(src, 'r', encoding='utf-8') as f: + preset = json.load(f) + except Exception: + continue + # Match by explicit device field + dev_field = preset.get('device') if isinstance(preset, dict) else None + if isinstance(dev_field, str) and _sanitize(dev_field) == target_norm: + logging.getLogger(__name__).info(f"Chargement préréglage (device) depuis le fichier: {fname}") + print(f"Chargement préréglage (device) depuis le fichier: {fname}") + return preset + except Exception as e: + logging.getLogger(__name__).warning(f"Échec scan préréglages par device: {e}") + + # Fallback préréglage explicite clavier si pas de joystick + if not getattr(config, 'joystick', False) or getattr(config, 'keyboard', False): + src = os.path.join(config.PRECONF_CONTROLS_PATH, 'keyboard.json') if os.path.exists(src): - with open(src, "r", encoding="utf-8") as f: + with open(src, 'r', encoding='utf-8') as f: data = json.load(f) if isinstance(data, dict) and data: - logging.getLogger(__name__).info(f"Chargement des contrôles préréglés: {fname}") + logging.getLogger(__name__).info("Chargement des contrôles préréglés: keyboard.json") return data except Exception as e: logging.getLogger(__name__).warning(f"Échec du chargement des contrôles préréglés: {e}") - # 3) Fallback clavier par défaut - logging.getLogger(__name__).info("Aucun fichier utilisateur ou préréglage trouvé, utilisation des contrôles par défaut") + # 3) Fallback: si joystick présent mais aucun préréglage trouvé, retourner {} pour déclencher le remap + if getattr(config, 'joystick', False): + logging.getLogger(__name__).info("Aucun préréglage trouvé pour le joystick connecté, ouverture du remap") + return {} + # Sinon, fallback clavier par défaut + logging.getLogger(__name__).info("Aucun fichier utilisateur ou préréglage trouvé, utilisation des contrôles clavier par défaut") return default_config.copy() except Exception as e: diff --git a/ports/RGSX/controls_mapper.py b/ports/RGSX/controls_mapper.py index 2e674dd..87d9487 100644 --- a/ports/RGSX/controls_mapper.py +++ b/ports/RGSX/controls_mapper.py @@ -1,12 +1,21 @@ import pygame # type: ignore import json import os +import io import logging import config import language from config import CONTROLS_CONFIG_PATH from display import draw_gradient import xml.etree.ElementTree as ET +from collections import OrderedDict +from typing import Optional, Tuple + +# Optional: SVG to PNG conversion (if installed) +try: + import cairosvg # type: ignore +except Exception: # pragma: no cover - optional dependency + cairosvg = None # type: ignore logger = logging.getLogger(__name__) @@ -282,9 +291,77 @@ MOUSE_BUTTON_NAMES = { # Durée de maintien pour valider une entrée (en millisecondes) HOLD_DURATION = 1000 +INPUT_ACCEPT_COOLDOWN = 350 # ms to ignore inputs right after accepting one (avoid axis release bounce) JOYHAT_DEBOUNCE = 200 # Délai anti-rebond pour JOYHATMOTION (ms) +# ---- Icônes: helpers pour charger et afficher des SVG ---- +_ICON_CACHE: dict[Tuple[str, int], Optional[pygame.Surface]] = {} + +def _images_base_dir() -> str: + return os.path.join(os.path.dirname(__file__), "assets", "images") + +def _action_icon_filename(action_name: str) -> Optional[str]: + # Map actions to icon filenames present in assets/images + mapping = { + "up": "dpad_up.svg", + "down": "dpad_down.svg", + "left": "dpad_left.svg", + "right": "dpad_right.svg", + "confirm": "buttons_south.svg", # A (south) + "cancel": "buttons_east.svg", # B (east) + "clear_history": "buttons_west.svg", # X (west) + "history": "buttons_north.svg", # Y (north) + "start": "button_start.svg", + "filter": "button_select.svg", + "delete": "button_l.svg", # LB + "space": "button_r.svg", # RB + "page_up": "button_lt.svg", + "page_down": "button_rt.svg", + } + return mapping.get(action_name) + +def _load_svg_icon_surface(svg_path: str, size: int) -> Optional[pygame.Surface]: + # Try to load SVG via cairosvg; fallback: let pygame try to load (only if supported) + try: + if cairosvg is not None: + with open(svg_path, "rb") as f: + svg_bytes = f.read() + png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=size, output_height=size) + return pygame.image.load(io.BytesIO(png_bytes), "icon.png").convert_alpha() + else: + # Some pygame builds may support SVG; try directly. + surf = pygame.image.load(svg_path) + # Scale to requested size while keeping aspect ratio + w, h = surf.get_size() + if w != size or h != size: + # uniform scale to fit in size x size + scale = min(size / max(w, 1), size / max(h, 1)) + new_w = max(1, int(w * scale)) + new_h = max(1, int(h * scale)) + surf = pygame.transform.smoothscale(surf, (new_w, new_h)) + return surf.convert_alpha() + except Exception as e: + logger.debug(f"Icon load failed for {svg_path}: {e}") + return None + +def get_action_icon_surface(action_name: str, size: int) -> Optional[pygame.Surface]: + key = (action_name, size) + if key in _ICON_CACHE: + return _ICON_CACHE[key] + filename = _action_icon_filename(action_name) + if not filename: + _ICON_CACHE[key] = None + return None + full_path = os.path.join(_images_base_dir(), filename) + if not os.path.exists(full_path): + logger.debug(f"Icon file not found: {full_path}") + _ICON_CACHE[key] = None + return None + surf = _load_svg_icon_surface(full_path, size) + _ICON_CACHE[key] = surf + return surf + def load_controls_config(path=CONTROLS_CONFIG_PATH): """Charge la configuration des contrôles depuis controls.json""" try: @@ -347,9 +424,64 @@ def get_readable_input_name(event): return "Inconnu" +def get_preferred_display_for_action(action_name: str, input_type: str, input_value): + """Retourne un libellé display standardisé pour correspondre à controller_debug. + + Règles: + - Pour les actions manette, on force un libellé stable (A/B/X/Y, LB/RB, LT/RT, Start/Select, flèches). + - Pour le clavier, on conserve le nom lisible de la touche. + - Pour le D-Pad et axes directionnels, on affiche des flèches. + """ + # Clavier: garder la touche lisible + if input_type == "key": + try: + key_value = int(input_value) + except Exception: + key_value = input_value + key_value = SDL_TO_PYGAME_KEY.get(key_value, key_value) + return KEY_NAMES.get(key_value, pygame.key.name(key_value) or f"Touche {key_value}") + + # Mapping stable par action (manette) + action_display = { + "confirm": "A", + "cancel": "B", + "clear_history": "X", + "history": "Y", + "start": "Start", + "filter": "Select", + "delete": "LB", + "space": "RB", + "page_up": "LT", + "page_down": "RT", + "up": "↑", + "down": "↓", + "left": "←", + "right": "→", + } + + # Directions: flèches (peu importe hat/axis/button) + if action_name in ("up", "down", "left", "right"): + return action_display[action_name] + + # Autres actions: renvoyer le libellé normalisé ci-dessus + if action_name in action_display: + return action_display[action_name] + + # Fallback: si on ne sait pas, retourner chaîne vide (appelant fera un secours) + return "" + + def map_controls(screen): """Interface de mappage des contrôles avec maintien de 3 secondes""" - controls_config = load_controls_config() + # Construire un objet ordonné pour forcer l'ordre des clés dans le JSON final + # Placer "device" en premier si disponible + controls_config = OrderedDict() + try: + device_name = getattr(config, "controller_device_name", "") or "" + if device_name: + controls_config["device"] = device_name + except Exception: + pass current_action_index = 0 current_input = None input_held_time = 0 @@ -357,6 +489,7 @@ def map_controls(screen): last_frame_time = pygame.time.get_ticks() config.needs_redraw = True last_joyhat_time = 0 + next_input_allowed_time = 0 # timestamp until which new inputs are ignored after accept # État des entrées maintenues held_keys = set() @@ -424,6 +557,9 @@ def map_controls(screen): # Détection des nouvelles entrées if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN): + # Ignorer les événements pendant un court délai après validation d'un mapping + if current_time < next_input_allowed_time: + continue if event.type == pygame.JOYHATMOTION: if (current_time - last_joyhat_time) < JOYHAT_DEBOUNCE: continue @@ -485,36 +621,41 @@ def map_controls(screen): # Sauvegarder avec la structure attendue par controls.py if current_input["type"] == "key": + disp = get_preferred_display_for_action(action_name, "key", current_input["value"]) or last_input_name controls_config[action_name] = { "type": "key", "key": current_input["value"], - "display": last_input_name + "display": disp } elif current_input["type"] == "button": + disp = get_preferred_display_for_action(action_name, "button", current_input["value"]) or last_input_name controls_config[action_name] = { "type": "button", "button": current_input["value"], - "display": last_input_name + "display": disp } elif current_input["type"] == "axis": axis, direction = current_input["value"] + disp = get_preferred_display_for_action(action_name, "axis", (axis, direction)) or last_input_name controls_config[action_name] = { "type": "axis", "axis": axis, "direction": direction, - "display": last_input_name + "display": disp } elif current_input["type"] == "hat": + disp = get_preferred_display_for_action(action_name, "hat", current_input["value"]) or last_input_name controls_config[action_name] = { "type": "hat", "value": current_input["value"], - "display": last_input_name + "display": disp } elif current_input["type"] == "mouse": + disp = get_preferred_display_for_action(action_name, "mouse", current_input["value"]) or last_input_name controls_config[action_name] = { "type": "mouse", "button": current_input["value"], - "display": last_input_name + "display": disp } logger.debug(f"Contrôle mappé: {action_name} -> {controls_config[action_name]}") @@ -523,6 +664,8 @@ def map_controls(screen): input_held_time = 0 last_input_name = None config.needs_redraw = True + # Activer un court délai pour ignorer les rebonds (ex: relâchement d'un axe) + next_input_allowed_time = pygame.time.get_ticks() + INPUT_ACCEPT_COOLDOWN # Réinitialiser les entrées maintenues held_keys.clear() @@ -542,7 +685,7 @@ def map_controls(screen): 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 + # 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 @@ -552,6 +695,8 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr border_radius = 24 border_width = 4 shadow_offset = 8 + bar_height = 25 + min_bar_inner_width = 200 # largeur minimale utile de la barre # Titre principal (traduction) title_text = language.get_text("controls_mapping_title", "Configuration des contrôles") @@ -559,6 +704,10 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr title_rect = title_surface.get_rect(center=(config.screen_width // 2, 80)) screen.blit(title_surface, title_rect) + # Icône de l'action courante (facultatif si dépendances disponibles) + icon_size = 72 # px + icon_surface = get_action_icon_surface(action.get('name', ''), icon_size) + # Instructions (traduction) instruction_text = language.get_text("controls_mapping_instruction", "Maintenez pendant 3s pour configurer :") description_text = action.get('description', '') @@ -574,11 +723,22 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr 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 + # Dimensions de la popup (s'adapte au contenu et inclut la barre) + icon_width, icon_height = (icon_surface.get_size() if icon_surface else (0, 0)) + inner_text_width = max(instruction_width, description_width, input_width, min_bar_inner_width, icon_width) + inner_text_height = 0 + if icon_surface: + inner_text_height += icon_height + padding_between + inner_text_height += instruction_height + description_height + input_height + 2 * padding_between + inner_width = inner_text_width + inner_height = inner_text_height + padding_between + bar_height + + popup_width = inner_width + 2 * padding_horizontal + # Eviter de dépasser l'écran (marge de 20px de chaque côté) + popup_width = min(popup_width, config.screen_width - 40) + popup_height = inner_height + 2 * padding_vertical + popup_height = min(popup_height, config.screen_height - 40) + popup_x = (config.screen_width - popup_width) // 2 popup_y = (config.screen_height - popup_height) // 2 @@ -597,31 +757,40 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr # Bordure blanche pygame.draw.rect(screen, (255, 255, 255), popup_rect, border_width, border_radius=border_radius) - # Afficher les textes + # Afficher les textes (centrés dans la popup) + center_x = popup_x + popup_width // 2 start_y = popup_y + padding_vertical - instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, start_y + instruction_height // 2)) + # Icône en premier (si dispo) + if icon_surface: + icon_rect = icon_surface.get_rect(center=(center_x, start_y + icon_height // 2)) + screen.blit(icon_surface, icon_rect) + start_y += icon_height + padding_between + instruction_rect = instruction_surface.get_rect(center=(center_x, 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)) + + description_rect = description_surface.get_rect(center=(center_x, 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)) + + input_rect = input_surface.get_rect(center=(center_x, 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)) + # Barre de progression pour le maintien (adaptée à la largeur intérieure de la popup) + bar_x = popup_x + padding_horizontal + bar_y = start_y + bar_width = popup_width - 2 * padding_horizontal + + pygame.draw.rect(screen, (50, 50, 50), (bar_x, bar_y, bar_width, bar_height)) + progress_width = int(bar_width * max(0.0, min(1.0, hold_progress))) + if progress_width > 0: + 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 + + # Pourcentage de progression (affiché au centre de la barre) 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)) + progress_rect = progress_surface.get_rect(center=(bar_x + bar_width // 2, bar_y + bar_height // 2)) screen.blit(progress_surface, progress_rect) \ No newline at end of file diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index a9e0901..9023e47 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -1,4 +1,8 @@ + + import pygame # type: ignore +import os +import io import config from utils import truncate_text_middle, wrap_text, load_system_image, truncate_text_end import logging @@ -10,6 +14,145 @@ logger = logging.getLogger(__name__) OVERLAY = None # Initialisé dans init_display() +# --- Helpers: SVG icons for controls (local cache, optional cairosvg) --- +_HELP_ICON_CACHE = {} + +def _images_base_dir(): + try: + base_dir = os.path.join(os.path.dirname(__file__), "assets", "images") + except Exception: + base_dir = "assets/images" + return base_dir + +def _action_icon_filename(action_name: str): + mapping = { + "up": "dpad_up.svg", + "down": "dpad_down.svg", + "left": "dpad_left.svg", + "right": "dpad_right.svg", + "confirm": "buttons_south.svg", + "cancel": "buttons_east.svg", + "clear_history": "buttons_west.svg", + "history": "buttons_north.svg", + "start": "button_start.svg", + "filter": "button_select.svg", + "delete": "button_l.svg", + "space": "button_r.svg", + "page_up": "button_lt.svg", + "page_down": "button_rt.svg", + } + return mapping.get(action_name) + +def _load_svg_icon_surface(svg_path: str, size: int): + try: + # Prefer cairosvg if available for crisp rasterization + try: + import cairosvg # type: ignore + except Exception: + cairosvg = None # type: ignore + if cairosvg is not None: + with open(svg_path, "rb") as f: + svg_bytes = f.read() + png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=size, output_height=size) + return pygame.image.load(io.BytesIO(png_bytes), "icon.png").convert_alpha() + # Fallback: try direct load (works if SDL_image has SVG support) + surf = pygame.image.load(svg_path) + w, h = surf.get_size() + if w != size or h != size: + scale = min(size / max(w, 1), size / max(h, 1)) + new_w = max(1, int(w * scale)) + new_h = max(1, int(h * scale)) + surf = pygame.transform.smoothscale(surf, (new_w, new_h)) + return surf.convert_alpha() + except Exception as e: + try: + logger.debug(f"Help icon load failed for {svg_path}: {e}") + except Exception: + pass + return None + +def get_help_icon_surface(action_name: str, size: int): + key = (action_name, size) + if key in _HELP_ICON_CACHE: + return _HELP_ICON_CACHE[key] + filename = _action_icon_filename(action_name) + if not filename: + _HELP_ICON_CACHE[key] = None + return None + full_path = os.path.join(_images_base_dir(), filename) + if not os.path.exists(full_path): + _HELP_ICON_CACHE[key] = None + return None + surf = _load_svg_icon_surface(full_path, size) + _HELP_ICON_CACHE[key] = surf + return surf + +def _render_icons_line(actions, text, target_col_width, font, text_color, icon_size=28, icon_gap=8, icon_text_gap=12): + """Compose une ligne avec une rangée d'icônes (actions) et un texte à droite. + Renvoie un pygame.Surface prêt à être blité, limité à target_col_width. + """ + # Charger icônes (ignorer celles manquantes) + icon_surfs = [] + for a in actions: + surf = get_help_icon_surface(a, icon_size) + if surf is not None: + icon_surfs.append(surf) + # Si aucune icône, rendre simplement le texte (le layout appelant ajoutera les espacements) + if not icon_surfs: + try: + lines = wrap_text(text, font, target_col_width) + except Exception: + lines = [text] + line_surfs = [font.render(l, True, text_color) for l in lines] + width = max((s.get_width() for s in line_surfs), default=1) + height = sum(s.get_height() for s in line_surfs) + max(0, (len(line_surfs) - 1)) * 4 + surf = pygame.Surface((width, height), pygame.SRCALPHA) + y = 0 + for s in line_surfs: + surf.blit(s, (0, y)) + y += s.get_height() + 4 + return surf + + # Calcul largeur totale des icônes + icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap + if icons_width + icon_text_gap > target_col_width: + scale = (target_col_width - icon_text_gap) / max(1, icons_width) + scale = max(0.6, min(1.0, scale)) + new_icon_surfs = [] + for s in icon_surfs: + new_size = (max(1, int(s.get_width() * scale)), max(1, int(s.get_height() * scale))) + new_icon_surfs.append(pygame.transform.smoothscale(s, new_size)) + icon_surfs = new_icon_surfs + icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap + + text_area_width = max(60, target_col_width - icons_width - icon_text_gap) + try: + lines = wrap_text(text, font, text_area_width) + except Exception: + lines = [text] + line_surfs = [font.render(l, True, text_color) for l in lines] + text_block_width = max((s.get_width() for s in line_surfs), default=1) + text_block_height = sum(s.get_height() for s in line_surfs) + max(0, (len(line_surfs) - 1)) * 4 + + total_width = min(target_col_width, icons_width + icon_text_gap + text_block_width) + total_height = max(max((s.get_height() for s in icon_surfs), default=0), text_block_height) + surf = pygame.Surface((total_width, total_height), pygame.SRCALPHA) + + x = 0 + icon_y_center = total_height // 2 + for idx, s in enumerate(icon_surfs): + r = s.get_rect() + y = icon_y_center - r.height // 2 + surf.blit(s, (x, y)) + x += r.width + (icon_gap if idx < len(icon_surfs) - 1 else 0) + + text_x = x + icon_text_gap + y = (total_height - text_block_height) // 2 + for ls in line_surfs: + surf.blit(ls, (text_x, y)) + y += ls.get_height() + 4 + return surf + # Couleurs modernes pour le thème THEME_COLORS = { # Fond des lignes sélectionnées @@ -1270,6 +1413,24 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start start_button = get_control_display('start', 'START') start_text = _("controls_action_start") control_text = f"RGSX v{config.app_version} - {start_button} : {start_text}" + + # Afficher le nom du joystick s'il est détecté + try: + device_name = getattr(config, 'controller_device_name', '') or '' + if device_name: + # Utilise la clé i18n si disponible, sinon fallback + try: + joy_label = _("footer_joystick") + except Exception: + joy_label = "Joystick: {0}" + # Formater si le placeholder {0} est présent + if isinstance(joy_label, str) and "{0}" in joy_label: + joy_text = joy_label.format(device_name) + else: + joy_text = f"{joy_label} {device_name}" if joy_label else f"Joystick: {device_name}" + control_text += f" | {joy_text}" + except Exception: + pass # Ajouter le nom de la musique si disponible if config.current_music_name and config.music_popup_start_time > 0: @@ -1995,25 +2156,25 @@ def draw_filter_platforms_menu(screen): # Menu aide contrôles def draw_controls_help(screen, previous_state): """Affiche la liste des contrôles (aide) avec mise en page adaptative.""" - # Contenu des catégories + # Contenu des catégories (avec icônes si disponibles) control_categories = { _("controls_category_navigation"): [ - f"{get_control_display('up', '↑')} {get_control_display('down', '↓')} {get_control_display('left', '←')} {get_control_display('right', '→')} : {_('controls_navigation')}", - f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {_('controls_pages')}", + ("icons", ["up", "down", "left", "right"], f"{get_control_display('up', '↑')} {get_control_display('down', '↓')} {get_control_display('left', '←')} {get_control_display('right', '→')} : {_('controls_navigation')}"), + ("icons", ["page_up", "page_down"], f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {_('controls_pages')}"), ], _("controls_category_main_actions"): [ - f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}", - f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}", - f"{get_control_display('start', 'Start')} : {_('controls_action_start')}", + ("icons", ["confirm"], f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}"), + ("icons", ["cancel"], f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}"), + ("icons", ["start"], f"{get_control_display('start', 'Start')} : {_('controls_action_start')}"), ], _("controls_category_downloads"): [ - f"{get_control_display('history', 'Y')} : {_('controls_action_history')}", - f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}", + ("icons", ["history"], f"{get_control_display('history', 'Y')} : {_('controls_action_history')}"), + ("icons", ["clear_history"], f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}"), ], _("controls_category_search"): [ - f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}", - f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}", - f"{get_control_display('space', 'Espace')} : {_('controls_action_space')}", + ("icons", ["filter"], f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}"), + ("icons", ["delete"], f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}"), + ("icons", ["space"], f"{get_control_display('space', 'Espace')} : {_('controls_action_space')}"), ], } @@ -2062,28 +2223,40 @@ def draw_controls_help(screen, previous_state): total_height += sec_surf.get_height() + line_spacing for raw_line in lines: - # Wrap par mots - words = raw_line.split() - cur = "" - for word in words: - test = (cur + " " + word).strip() - if font.size(test)[0] <= target_col_width: - cur = test - else: - if cur: - line_surf = font.render(cur, True, THEME_COLORS["text"]) - - - - wrapped.append((False, line_surf)) - total_height += line_surf.get_height() + line_spacing - max_width = max(max_width, line_surf.get_width()) - cur = word - if cur: - line_surf = font.render(cur, True, THEME_COLORS["text"]) - wrapped.append((False, line_surf)) - total_height += line_surf.get_height() + line_spacing - max_width = max(max_width, line_surf.get_width()) + # Deux formats possibles: + # - tuple ("icons", [actions], text) + # - chaîne texte simple + line_surface = None + if isinstance(raw_line, tuple) and len(raw_line) >= 3 and raw_line[0] == "icons": + _, actions, text = raw_line + try: + line_surface = _render_icons_line(actions, text, target_col_width, font, THEME_COLORS["text"]) + except Exception: + line_surface = None + if line_surface is None: + # Fallback: traitement texte comme avant + words = str(raw_line).split() + cur = "" + for word in words: + test = (cur + " " + word).strip() + if font.size(test)[0] <= target_col_width: + cur = test + else: + if cur: + line_surf = font.render(cur, True, THEME_COLORS["text"]) + wrapped.append((False, line_surf)) + total_height += line_surf.get_height() + line_spacing + max_width = max(max_width, line_surf.get_width()) + cur = word + if cur: + line_surf = font.render(cur, True, THEME_COLORS["text"]) + wrapped.append((False, line_surf)) + total_height += line_surf.get_height() + line_spacing + max_width = max(max_width, line_surf.get_width()) + else: + wrapped.append((False, line_surface)) + total_height += line_surface.get_height() + line_spacing + max_width = max(max_width, line_surface.get_width()) total_height += section_spacing # espace après section max_width = max(max_width, sec_surf.get_width()) diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index c8e5c5c..b70e7fa 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -180,16 +180,21 @@ ,"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen" ,"controls_desc_confirm": "Bestätigen (z.B. A/Kreuz)" ,"controls_desc_cancel": "Abbrechen/Zurück (z.B. B/Kreis)" - ,"controls_desc_up": "Nach oben navigieren" - ,"controls_desc_down": "Nach unten navigieren" - ,"controls_desc_left": "Nach links navigieren" - ,"controls_desc_right": "Nach rechts navigieren" - ,"controls_desc_page_up": "Schnell nach oben (z.B. LB/L1)" - ,"controls_desc_page_down": "Schnell nach unten (z.B. RB/R1)" + ,"controls_desc_up": "UP ↑" + ,"controls_desc_down": "DOWN ↓" + ,"controls_desc_left": "LEFT ←" + ,"controls_desc_right": "RIGHT →" + ,"controls_desc_page_up": "Schnell nach oben (z.B. LT/L2)" + ,"controls_desc_page_down": "Schnell nach unten (z.B. RT/R2)" ,"controls_desc_history": "Verlauf öffnen (z.B. Y/Dreieck)" ,"controls_desc_clear_history": "Downloads: Mehrfachauswahl / Verlauf: Leeren (z.B. X/Quadrat)" ,"controls_desc_filter": "Filtermodus: Öffnen/Bestätigen (z.B. Select)" - ,"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LT/L2)" - ,"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RT/R2)" + ,"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LB/L1)" + ,"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RB/R1)" ,"controls_desc_start": "Pausenmenü öffnen (z.B. Start)" + ,"controls_mapping_title": "Steuerungszuordnung" + ,"controls_mapping_instruction": "Zum Bestätigen gedrückt halten:" + ,"controls_mapping_waiting": "Warte auf eine Taste oder einen Button..." + ,"controls_mapping_press": "Drücke eine Taste oder einen Button" + ,"footer_joystick": "Joystick: {0}" } \ No newline at end of file diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index 17a76e8..1e1977c 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -180,16 +180,21 @@ ,"instruction_settings_api_keys": "See detected premium provider API keys" ,"controls_desc_confirm": "Confirm (e.g. A/Cross)" ,"controls_desc_cancel": "Cancel/Back (e.g. B/Circle)" - ,"controls_desc_up": "Navigate up" - ,"controls_desc_down": "Navigate down" - ,"controls_desc_left": "Navigate left" - ,"controls_desc_right": "Navigate right" - ,"controls_desc_page_up": "Fast scroll up (e.g. LB/L1)" - ,"controls_desc_page_down": "Fast scroll down (e.g. RB/R1)" + ,"controls_desc_up": "UP ↑" + ,"controls_desc_down": "DOWN ↓" + ,"controls_desc_left": "LEFT ←" + ,"controls_desc_right": "RIGHT →" + ,"controls_desc_page_up": "Fast scroll up (e.g. LT/L2)" + ,"controls_desc_page_down": "Fast scroll down (e.g. RT/R2)" ,"controls_desc_history": "Open history (e.g. Y/Triangle)" ,"controls_desc_clear_history": "Downloads: Multi-select / History: Clear (e.g. X/Square)" ,"controls_desc_filter": "Filter mode: Open/Confirm (e.g. Select)" - ,"controls_desc_delete": "Filter mode: Delete character (e.g. LT/L2)" - ,"controls_desc_space": "Filter mode: Add space (e.g. RT/R2)" + ,"controls_desc_delete": "Filter mode: Delete character (e.g. LB/L1)" + ,"controls_desc_space": "Filter mode: Add space (e.g. RB/R1)" ,"controls_desc_start": "Open pause menu (e.g. Start)" + ,"controls_mapping_title": "Controls mapping" + ,"controls_mapping_instruction": "Hold to confirm the mapping:" + ,"controls_mapping_waiting": "Waiting for a key or button..." + ,"controls_mapping_press": "Press a key or a button" + ,"footer_joystick": "Joystick: {0}" } \ No newline at end of file diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index 990eaa7..c78503c 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -180,16 +180,21 @@ ,"instruction_settings_api_keys": "Ver claves API premium detectadas" ,"controls_desc_confirm": "Confirmar (ej. A/Cruz)" ,"controls_desc_cancel": "Cancelar/Volver (ej. B/Círculo)" - ,"controls_desc_up": "Navegar hacia arriba" - ,"controls_desc_down": "Navegar hacia abajo" - ,"controls_desc_left": "Navegar a la izquierda" - ,"controls_desc_right": "Navegar a la derecha" - ,"controls_desc_page_up": "Desplazamiento rápido - (ej. LB/L1)" - ,"controls_desc_page_down": "Desplazamiento rápido + (ej. RB/R1)" + ,"controls_desc_up": "UP ↑" + ,"controls_desc_down": "DOWN ↓" + ,"controls_desc_left": "LEFT ←" + ,"controls_desc_right": "RIGHT →" + ,"controls_desc_page_up": "Desplazamiento rápido - (ej. LT/L2)" + ,"controls_desc_page_down": "Desplazamiento rápido + (ej. RT/R2)" ,"controls_desc_history": "Abrir historial (ej. Y/Triángulo)" ,"controls_desc_clear_history": "Descargas: Selección múltiple / Historial: Limpiar (ej. X/Cuadrado)" ,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ej. Select)" - ,"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LT/L2)" - ,"controls_desc_space": "Modo filtro: Añadir espacio (ej. RT/R2)" + ,"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LB/L1)" + ,"controls_desc_space": "Modo filtro: Añadir espacio (ej. RB/R1)" ,"controls_desc_start": "Abrir menú pausa (ej. Start)" + ,"controls_mapping_title": "Asignación de controles" + ,"controls_mapping_instruction": "Mantén para confirmar la asignación:" + ,"controls_mapping_waiting": "Esperando una tecla o botón..." + ,"controls_mapping_press": "Pulsa una tecla o un botón" + ,"footer_joystick": "Joystick: {0}" } \ No newline at end of file diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index e93cd21..5efdec8 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -180,16 +180,21 @@ ,"instruction_settings_api_keys": "Voir les clés API détectées des services premium" ,"controls_desc_confirm": "Valider (ex: A/Croix)" ,"controls_desc_cancel": "Annuler/Retour (ex: B/Rond)" - ,"controls_desc_up": "Naviguer vers le haut" - ,"controls_desc_down": "Naviguer vers le bas" - ,"controls_desc_left": "Naviguer à gauche" - ,"controls_desc_right": "Naviguer à droite" - ,"controls_desc_page_up": "Défilement Rapide - (ex: LB/L1)" - ,"controls_desc_page_down": "Défilement Rapide + (ex: RB/R1)" + ,"controls_desc_up": "UP ↑" + ,"controls_desc_down": "DOWN ↓" + ,"controls_desc_left": "LEFT ←" + ,"controls_desc_right": "RIGHT →" + ,"controls_desc_page_up": "Défilement Rapide - (ex: LT/L2)" + ,"controls_desc_page_down": "Défilement Rapide + (ex: RT/R2)" ,"controls_desc_history": "Ouvrir l'historique (ex: Y/Triangle)" ,"controls_desc_clear_history": "Téléchargements : Sélection multiple / Historique : Vider (ex: X/Carré)" ,"controls_desc_filter": "Mode Filtre : Ouvrir/Valider (ex: Select)" - ,"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LT/L2)" - ,"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RT/R2)" + ,"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LB/L1)" + ,"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RB/R1)" ,"controls_desc_start": "Ouvrir le menu pause (ex: Start)" + ,"controls_mapping_title": "Configuration des contrôles" + ,"controls_mapping_instruction": "Maintenez pour confirmer l'association :" + ,"controls_mapping_waiting": "En attente d'une touche ou d'un bouton..." + ,"controls_mapping_press": "Appuyez sur une touche ou un bouton" + ,"footer_joystick": "Joystick : {0}" } \ No newline at end of file diff --git a/ports/RGSX/languages/it.json b/ports/RGSX/languages/it.json index 93f4675..04ab511 100644 --- a/ports/RGSX/languages/it.json +++ b/ports/RGSX/languages/it.json @@ -180,16 +180,21 @@ ,"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate" ,"controls_desc_confirm": "Confermare (es. A/Croce)" ,"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)" - ,"controls_desc_up": "Navigare verso l'alto" - ,"controls_desc_down": "Navigare verso il basso" - ,"controls_desc_left": "Navigare a sinistra" - ,"controls_desc_right": "Navigare a destra" - ,"controls_desc_page_up": "Scorrimento rapido su (es. LB/L1)" - ,"controls_desc_page_down": "Scorrimento rapido giù (es. RB/R1)" + ,"controls_desc_up": "UP ↑" + ,"controls_desc_down": "DOWN ↓" + ,"controls_desc_left": "LEFT ←" + ,"controls_desc_right": "RIGHT →" + ,"controls_desc_page_up": "Scorrimento rapido su (es. LT/L2)" + ,"controls_desc_page_down": "Scorrimento rapido giù (es. RT/R2)" ,"controls_desc_history": "Aprire cronologia (es. Y/Triangolo)" ,"controls_desc_clear_history": "Download: Selezione multipla / Cronologia: Svuotare (es. X/Quadrato)" ,"controls_desc_filter": "Modalità filtro: Aprire/Confermare (es. Select)" - ,"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LT/L2)" - ,"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RT/R2)" + ,"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LB/L1)" + ,"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RB/R1)" ,"controls_desc_start": "Aprire menu pausa (es. Start)" + ,"controls_mapping_title": "Mappatura controlli" + ,"controls_mapping_instruction": "Tieni premuto per confermare l'associazione:" + ,"controls_mapping_waiting": "In attesa di un tasto o pulsante..." + ,"controls_mapping_press": "Premi un tasto o un pulsante" + ,"footer_joystick": "Joystick: {0}" } \ No newline at end of file diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json index f76fea2..8cd0158 100644 --- a/ports/RGSX/languages/pt.json +++ b/ports/RGSX/languages/pt.json @@ -180,16 +180,21 @@ ,"instruction_settings_api_keys": "Ver chaves API premium detectadas" ,"controls_desc_confirm": "Confirmar (ex. A/Cruz)" ,"controls_desc_cancel": "Cancelar/Voltar (ex. B/Círculo)" - ,"controls_desc_up": "Navegar para cima" - ,"controls_desc_down": "Navegar para baixo" - ,"controls_desc_left": "Navegar para esquerda" - ,"controls_desc_right": "Navegar para direita" - ,"controls_desc_page_up": "Rolagem rápida para cima (ex. LB/L1)" - ,"controls_desc_page_down": "Rolagem rápida para baixo (ex. RB/R1)" + ,"controls_desc_up": "UP ↑" + ,"controls_desc_down": "DOWN ↓" + ,"controls_desc_left": "LEFT ←" + ,"controls_desc_right": "RIGHT →" + ,"controls_desc_page_up": "Rolagem rápida para cima (ex. LT/L2)" + ,"controls_desc_page_down": "Rolagem rápida para baixo (ex. RT/R2)" ,"controls_desc_history": "Abrir histórico (ex. Y/Triângulo)" ,"controls_desc_clear_history": "Downloads: Seleção múltipla / Histórico: Limpar (ex. X/Quadrado)" ,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ex. Select)" - ,"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LT/L2)" - ,"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RT/R2)" + ,"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LB/L1)" + ,"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RB/R1)" ,"controls_desc_start": "Abrir menu pausa (ex. Start)" + ,"controls_mapping_title": "Mapeamento de controles" + ,"controls_mapping_instruction": "Mantenha para confirmar o mapeamento:" + ,"controls_mapping_waiting": "Aguardando uma tecla ou botão..." + ,"controls_mapping_press": "Pressione uma tecla ou um botão" + ,"footer_joystick": "Joystick: {0}" } \ No newline at end of file diff --git a/pygame/controller_debug.pygame b/pygame/controller_debug.pygame index af1d7f0..cb7615e 100644 --- a/pygame/controller_debug.pygame +++ b/pygame/controller_debug.pygame @@ -2,8 +2,10 @@ import os import sys import time +import json +import re import traceback -from typing import Any, Dict, Tuple, List +from typing import Any, Dict, Tuple, List, Optional try: import pygame # type: ignore @@ -234,6 +236,116 @@ def write_log(path: str, mapping: Dict[str, Tuple[str, Any]], device_name: str) log(f"Saved mapping to: {path}") +# --- JSON preset generation --- +def sanitize_device_name(name: str) -> str: + s = name.strip().lower() + # Replace non-alphanumeric with underscore + s = re.sub(r"[^a-z0-9]+", "_", s) + s = re.sub(r"_+", "_", s).strip("_") + return s or "controller" + + +def to_json_binding(kind: str, data: Any, display: Optional[str] = None) -> Optional[Dict[str, Any]]: + if kind == "button" and isinstance(data, int): + return {"type": "button", "button": data, **({"display": display} if display else {})} + if kind == "hat" and isinstance(data, (tuple, list)) and len(data) == 2: + val = list(data) + return {"type": "hat", "value": val, **({"display": display} if display else {})} + if kind == "axis" and isinstance(data, dict): + axis = data.get("axis") + direction = data.get("direction") + if isinstance(axis, int) and direction in (-1, 1): + return {"type": "axis", "axis": axis, "direction": int(direction), **({"display": display} if display else {})} + return None + + +def build_controls_json(mapping: Dict[str, Tuple[str, Any]]) -> Dict[str, Any]: + # Map logical prompts to action keys and preferred display labels + prompt_map = { + "SOUTH_BUTTON - CONFIRM": ("confirm", "A"), + "EAST_BUTTON - CANCEL": ("cancel", "B"), + "WEST_BUTTON - CLEAR HISTORY / SELECT GAMES": ("clear_history", "X"), + "NORTH_BUTTON - HISTORY": ("history", "Y"), + "START - PAUSE": ("start", "Start"), + "SELECT - FILTER": ("filter", "Select"), + "DPAD_UP - MOVE UP": ("up", "↑"), + "DPAD_DOWN - MOVE DOWN": ("down", "↓"), + "DPAD_LEFT - MOVE LEFT": ("left", "←"), + "DPAD_RIGHT - MOVE RIGHT": ("right", "→"), + "LEFT_BUMPER - LB/L1 - Delete last char": ("delete", "LB"), + "RIGHT_BUMPER - RB/R1 - Add space": ("space", "RB"), + # Triggers per prompts: LEFT=page_up, RIGHT=page_down + "LEFT_TRIGGER - LT/L2 - Page +": ("page_up", "LT"), + "RIGHT_TRIGGER - RT/R2 - Page -": ("page_down", "RT"), + # Left stick directions (fallbacks for arrows) + "JOYSTICK_LEFT_UP - MOVE UP": ("up", "J↑"), + "JOYSTICK_LEFT_DOWN - MOVE DOWN": ("down", "J↓"), + "JOYSTICK_LEFT_LEFT - MOVE LEFT": ("left", "J←"), + "JOYSTICK_LEFT_RIGHT - MOVE RIGHT": ("right", "J→"), + } + + result: Dict[str, Any] = {} + + # First pass: take direct DPAD/face/meta/bumper/trigger bindings + for prompt, (action, disp) in prompt_map.items(): + if prompt not in mapping: + continue + kind, data = mapping[prompt] + if kind in ("ignored", "skipped"): + continue + # Prefer DPAD over JOYSTICK for directions: handle fallback later + if action in ("up", "down", "left", "right"): + if prompt.startswith("DPAD_"): + b = to_json_binding(kind, data, disp) + if b: + result[action] = b + # Joystick handled as fallback if DPAD missing + else: + b = to_json_binding(kind, data, disp) + if b: + result[action] = b + + # Second pass: fallback to joystick directions if arrows missing + fallbacks = [ + ("JOYSTICK_LEFT_UP - MOVE UP", "up", "J↑"), + ("JOYSTICK_LEFT_DOWN - MOVE DOWN", "down", "J↓"), + ("JOYSTICK_LEFT_LEFT - MOVE LEFT", "left", "J←"), + ("JOYSTICK_LEFT_RIGHT - MOVE RIGHT", "right", "J→"), + ] + for prompt, action, disp in fallbacks: + if action in result: + continue + if prompt in mapping: + kind, data = mapping[prompt] + if kind in ("ignored", "skipped"): + continue + b = to_json_binding(kind, data, disp) + if b: + result[action] = b + + return result + + +def write_controls_json(device_name: str, controls: Dict[str, Any]) -> str: + """Write the generated controls preset JSON in the same folder as this script. + + Also embeds a JSON-safe comment with the device name under the _comment key. + """ + # Same folder as the launched script + base_dir = os.path.dirname(os.path.abspath(__file__)) + fname = f"{sanitize_device_name(device_name)}_controller.json" + out_path = os.path.join(base_dir, fname) + # Include the detected device name for auto-preset matching + payload = {"device": device_name} + payload.update(controls) + try: + with open(out_path, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=4) + return out_path + except Exception: + return out_path + + def main() -> None: init_screen() js = init_joystick() @@ -253,6 +365,13 @@ def main() -> None: log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "controller_mapping.log") write_log(log_path, mapping, js.get_name()) + # Build and write ready-to-use JSON controls preset + controls = build_controls_json(mapping) + if controls: + out_json = write_controls_json(js.get_name(), controls) + log(f"Saved JSON preset to: {out_json}") + else: + log("No usable inputs captured to build a JSON preset.") log("Done. Press Q or close the window to exit.")