- updated controls mapping and reading simplified
- update translations
- move some files to reorganize folders
- add some icons to controls
This commit is contained in:
retrogamesets
2025-09-21 15:25:52 +02:00
parent 9861afb9fb
commit 9aa494a5d0
41 changed files with 1238 additions and 376 deletions
+12 -66
View File
@@ -16,7 +16,6 @@ import datetime
import subprocess import subprocess
import sys import sys
import config import config
import shutil
from display import ( from display import (
init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, 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") logger.debug("API key files ensured at startup")
except Exception as _e: except Exception as _e:
logger.warning(f"Cannot prepare API key files early: {_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) # Mise à jour de la gamelist Windows avant toute initialisation graphique (évite les conflits avec ES)
def _run_windows_gamelist_update(): def _run_windows_gamelist_update():
try: try:
@@ -198,6 +196,15 @@ for i in range(count):
joystick_names.append(j.get_name()) joystick_names.append(j.get_name())
except Exception as e: except Exception as e:
logger.debug(f"Impossible de lire le nom du joystick {i}: {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] normalized_names = [n.lower() for n in joystick_names]
if not joystick_names: if not joystick_names:
joystick_names = ["Clavier"] joystick_names = ["Clavier"]
@@ -205,73 +212,12 @@ if not joystick_names:
logger.debug("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.joystick = False
config.keyboard = True 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: 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.joystick = True
config.keyboard = False config.keyboard = False
print(f"Joysticks détectés: YES") print("Joystick détecté:", ", ".join(joystick_names))
logger.debug(f"Joysticks détectés: YES") logger.debug(f"Joysticks détectés: {joystick_names}")
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}")
@@ -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"
}
}
@@ -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"
}
}
@@ -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" }
}
@@ -1,4 +1,5 @@
{ {
"device": "RG34XX-SP Controller",
"confirm": { "confirm": {
"type": "button", "type": "button",
"button": 3, "button": 3,
+20
View File
@@ -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" }
}
@@ -1,4 +1,5 @@
{ {
"device": "Steam Deck",
"confirm": { "confirm": {
"type": "button", "type": "button",
"button": 3, "button": 3,
@@ -1,4 +1,5 @@
{ {
"device": "TRIMUI Smart Pro Controller",
"confirm": { "confirm": {
"type": "button", "type": "button",
"button": 0, "button": 0,
@@ -1,4 +1,5 @@
{ {
"device": "XBOX 360 For Windows (Controller)",
"confirm": { "confirm": {
"type": "button", "type": "button",
"button": 0, "button": 0,
@@ -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": { "confirm": {
"type": "button", "type": "button",
"button": 0, "button": 0,
@@ -9,48 +42,6 @@
"button": 1, "button": 1,
"display": "B" "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": { "history": {
"type": "button", "type": "button",
"button": 3, "button": 3,
@@ -61,6 +52,16 @@
"button": 2, "button": 2,
"display": "X" "display": "X"
}, },
"start": {
"type": "button",
"button": 7,
"display": "Start"
},
"filter": {
"type": "button",
"button": 6,
"display": "Select"
},
"delete": { "delete": {
"type": "button", "type": "button",
"button": 4, "button": 4,
@@ -70,5 +71,17 @@
"type": "button", "type": "button",
"button": 5, "button": 5,
"display": "RB" "display": "RB"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": -1,
"display": "LT"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": -1,
"display": "RT"
} }
} }
@@ -1,4 +1,5 @@
{ {
"device": "Microsoft Xbox Controller",
"confirm": { "type": "button", "button": 1, "display": "A" }, "confirm": { "type": "button", "button": 1, "display": "A" },
"cancel": { "type": "button", "button": 2, "display": "B" }, "cancel": { "type": "button", "button": 2, "display": "B" },
"clear_history": { "type": "button", "button": 3, "display": "X" }, "clear_history": { "type": "button", "button": 3, "display": "X" },
@@ -12,8 +13,5 @@
"left": { "type": "hat", "value": [-1, 0], "display": "\u2190" }, "left": { "type": "hat", "value": [-1, 0], "display": "\u2190" },
"right": { "type": "hat", "value": [1, 0], "display": "\u2192" }, "right": { "type": "hat", "value": [1, 0], "display": "\u2192" },
"page_up": { "type": "axis", "axis": 5, "direction": -1, "display": "RT" }, "page_up": { "type": "axis", "axis": 5, "direction": -1, "display": "RT" },
"page_down": { "type": "axis", "axis": 2, "direction": -1, "display": "LT" }, "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."
}
} }
@@ -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": { "confirm": {
"type": "button", "type": "button",
"button": 0, "button": 0,
@@ -9,48 +30,6 @@
"button": 1, "button": 1,
"display": "B" "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": { "history": {
"type": "button", "type": "button",
"button": 3, "button": 3,
@@ -61,14 +40,36 @@
"button": 2, "button": 2,
"display": "X" "display": "X"
}, },
"delete": { "start": {
"type": "button",
"button": 6,
"display": "Start"
},
"filter": {
"type": "button", "type": "button",
"button": 4, "button": 4,
"display": "Select"
},
"delete": {
"type": "button",
"button": 9,
"display": "LB" "display": "LB"
}, },
"space": { "space": {
"type": "button", "type": "button",
"button": 5, "button": 10,
"display": "RB" "display": "RB"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": 1,
"display": "LT"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": 1,
"display": "RT"
} }
} }
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M15,19 L49,19 A13,13 90 0,1 62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 A13,13 90 0,1 15,19 Z" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<path id="button_l" d="m15 19a13 13 0 0 0-13 13 13 13 0 0 0 13 13h34a13 13 0 0 0 13-13 13 13 0 0 0-13-13h-34zm12.804688 4.433594h4.101562v13.921875h4.289062v3.210937h-8.390624v-17.132812z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M62,19 L62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 L2,19" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<line id="outline2" x1="1" y1="19" x2="63" y2="19" stroke="#fff" stroke-width="2"/>
<path id="button_lt" d="m2 19v13a13 13 0 0 0 13 13h34a13 13 0 0 0 13-13v-13h-60zm19.939453 4.433594h4.101563v13.921875h4.289062v3.210937h-8.390625v-17.132812zm9.667969 0h10.453125v3.222656h-3.1875v13.910156h-4.078125v-13.910156h-3.1875v-3.222656z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 652 B

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M15,19 L49,19 A13,13 90 0,1 62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 A13,13 90 0,1 15,19 Z" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<path id="button_r" d="m15 19a13 13 0 0 0-13 13 13 13 0 0 0 13 13h34a13 13 0 0 0 13-13 13 13 0 0 0-13-13h-34zm12.330078 4.433594h4.546875c1.882813 0 3.273438 0.425781 4.171875 1.277344 0.898438 0.851562 1.347656 2.15625 1.347656 3.914062 0 2.039062-0.699218 3.519531-2.097656 4.441406l3.28125 7.5h-4.324219l-2.625-6.46875h-0.210937v6.46875h-4.089844v-17.132812zm4.089844 3.175781v4.324219h0.304687c0.539063 0 0.929688-0.183594 1.171875-0.550782 0.25-0.367187 0.375-0.921874 0.375-1.664062 0-0.75-0.128906-1.289062-0.386718-1.617188-0.25-0.328124-0.644532-0.492187-1.183594-0.492187h-0.28125z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 948 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M62,19 L62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 L2,19" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<line id="outline2" x1="1" y1="19" x2="63" y2="19" stroke="#fff" stroke-width="2"/>
<path id="button_rt" d="m2 19v13a13 13 0 0 0 13 13h34a13 13 0 0 0 13-13v-13h-60zm19.119141 4.433594h4.546875c1.882812 0 3.273437 0.425781 4.171875 1.277344 0.898437 0.851562 1.347656 2.15625 1.347656 3.914062 0 2.039062-0.699219 3.519531-2.097656 4.441406l3.28125 7.5h-4.324219l-2.625-6.46875h-0.210938v6.46875h-4.089843v-17.132812zm11.308593 0h10.453125v3.222656h-3.1875v13.910156h-4.078125v-13.910156h-3.1875v-3.222656zm-7.21875 3.175781v4.324219h0.304688c0.539062 0 0.929687-0.183594 1.171875-0.550782 0.25-0.367187 0.375-0.921874 0.375-1.664062 0-0.75-0.128906-1.289062-0.386719-1.617188-0.25-0.328124-0.644531-0.492187-1.183594-0.492187h-0.28125z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="m2 42a7 7 0 0 1 7-7h46a7 7 0 0 1 7 7 7 7 0 0 1-7 7h-46a7 7 0 0 1-7-7z" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<g id ="label_select" fill="#fff">
<path d="m7.5908203 26.665543q0.5175781 0 0.7910156-0.341797 0.2734375-0.351563 0.2734375-0.84961 0-0.507812-0.1464843-0.859375-0.1464844-0.361328-0.4296875-0.664062-0.2832032-0.302735-0.5371094-0.498047-0.2441406-0.205078-0.6152344-0.46875-1.2792969-0.898438-1.8945312-1.894531-0.6054688-0.996094-0.6054688-2.353516 0-1.875 1.1230469-2.96875 1.1328125-1.103516 2.9199219-1.103516t3.5253904 1.044922l-1.044922 2.509766q-0.05859-0.0293-0.302734-0.15625-0.244141-0.136719-0.3125-0.166016-0.06836-0.03906-0.283203-0.136718-0.2148439-0.107422-0.3027346-0.146485l-0.2636718-0.09766q-0.4296875-0.166016-0.8300782-0.166016-0.390625 0-0.625 0.361328-0.2246093 0.351563-0.2246093 0.859375 0 0.498047 0.1269531 0.830079 0.1269531 0.332031 0.390625 0.615234 0.4492187 0.46875 1.1035156 0.898437 1.2988282 0.878907 1.9531252 1.855469 0.664062 0.966797 0.664062 2.333985 0 2.041015-1.09375 3.134765-1.0937497 1.083985-3.1152341 1.083985-2.0117187 0-3.515625-0.84961v-3.203125q2.1289063 1.396485 3.2714844 1.396485z"/>
<path d="m21.125977 17.505386h-3.66211v2.88086h3.398438v2.65625h-3.398438v3.417968h3.66211v2.675782h-7.080079v-14.277344h7.080079z"/>
<path d="m26.838867 26.460464h3.574219v2.675782h-6.992188v-14.277344h3.417969z"/>
<path d="m39.602539 17.505386h-3.662109v2.88086h3.398437v2.65625h-3.398437v3.417968h3.662109v2.675782h-7.080078v-14.277344h7.080078z"/>
<path d="m46.643555 29.321793q-1.181641 0-2.109375-0.400391-0.917969-0.410156-1.503907-1.083984-0.585937-0.673829-0.966796-1.63086-0.69336-1.738281-0.69336-4.189453 0-1.933594 0.488281-3.564453 0.488282-1.650391 1.660157-2.714844 1.210937-1.074219 3.164062-1.074219 0.859375 0 1.650391 0.253907 0.800781 0.253906 1.728515 0.849609l-0.976562 2.412109q-0.07813-0.07813-0.380859-0.253906-0.302735-0.185547-0.556641-0.292969-0.673828-0.302734-1.259766-0.302734-0.576172 0-0.947265 0.341797-0.371094 0.332031-0.576172 0.820312-0.205078 0.488282-0.322266 1.171875-0.205078 1.103516-0.205078 2.373047 0 4.609375 2.099609 4.609375 0.917969 0 2.72461-1.035156v2.880859q-1.279297 0.830079-3.017578 0.830079z"/>
<path d="m59.680664 17.544449h-2.65625v11.591797h-3.398437v-11.591797h-2.65625v-2.685547h8.710937z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="m2 42a7 7 0 0 1 7-7h46a7 7 0 0 1 7 7 7 7 0 0 1-7 7h-46a7 7 0 0 1-7-7z" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<g id="label_start" fill="#fff">
<path d="m11.02832 26.665543q0.517578 0 0.791016-0.341797 0.273437-0.351563 0.273437-0.84961 0-0.507812-0.146484-0.859375-0.146484-0.361328-0.429687-0.664062-0.283204-0.302735-0.53711-0.498047-0.24414-0.205078-0.615234-0.46875-1.2792971-0.898438-1.8945314-1.894531-0.6054688-0.996094-0.6054688-2.353516 0-1.875 1.1230469-2.96875 1.1328123-1.103516 2.9199223-1.103516 1.787109 0 3.52539 1.044922l-1.044922 2.509766q-0.05859-0.0293-0.302734-0.15625-0.244141-0.136719-0.3125-0.166016-0.06836-0.03906-0.283203-0.136718-0.214844-0.107422-0.302735-0.146485l-0.263671-0.09766q-0.429688-0.166016-0.830079-0.166016-0.390625 0-0.625 0.361328-0.224609 0.351563-0.224609 0.859375 0 0.498047 0.126953 0.830079 0.126953 0.332031 0.390625 0.615234 0.449219 0.46875 1.103516 0.898437 1.298828 0.878907 1.953125 1.855469 0.664062 0.966797 0.664062 2.333985 0 2.041015-1.09375 3.134765-1.09375 1.083985-3.115234 1.083985-2.0117188 0-3.5156251-0.84961v-3.203125q2.1289063 1.396485 3.2714841 1.396485z"/>
<path d="m25.149414 17.544449h-2.65625v11.591797h-3.398437v-11.591797h-2.65625v-2.685547h8.710937z"/>
<path d="m36.760742 29.136246h-3.4375l-0.791015-3.496094h-2.88086l-0.791015 3.496094h-3.427735l3.544922-14.335938h4.228516zm-4.814453-6.201172-0.849609-3.759766q-0.15625 0.751953-0.830078 3.759766z"/>
<path d="m38.108398 14.858902h3.789063q2.353516 0 3.476562 1.064453 1.123047 1.064453 1.123047 3.261719 0 2.548828-1.748047 3.701172l2.734375 6.25h-3.603515l-2.1875-5.390625h-0.175781v5.390625h-3.408204zm3.408204 2.646484v3.603516h0.253906q0.673828 0 0.976562-0.458984 0.3125-0.458985 0.3125-1.386719 0-0.9375-0.322265-1.347656-0.3125-0.410157-0.986328-0.410157z"/>
<path d="m56.243164 17.544449h-2.65625v11.591797h-3.398437v-11.591797h-2.65625v-2.685547h8.710937z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="m 50,22 c -5.522847,0 -10,4.477153 -10,10 0,5.522847 4.477153,10 10,10 5.522847,0 10,-4.477153 10,-10 0,-5.522847 -4.477153,-10 -10,-10 z"
id="button_east" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="M 32,4 C 26.477153,4 22,8.4771525 22,14 22,19.522847 26.477153,24 32,24 37.522847,24 42,19.522847 42,14 42,8.4771525 37.522847,4 32,4 Z"
id="button_north" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="m 32,40 c -5.522847,0 -10,4.477153 -10,10 0,5.522847 4.477153,10 10,10 5.522847,0 10,-4.477153 10,-10 0,-5.522847 -4.477153,-10 -10,-10 z"
id="button_south" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="M 14,22 C 8.4771525,22 4,26.477153 4,32 4,37.522847 8.4771525,42 14,42 19.522847,42 24,37.522847 24,32 24,26.477153 19.522847,22 14,22 Z"
id="button_west" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+6 -5
View File
@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore pygame = None # type: ignore
# Version actuelle de l'application # Version actuelle de l'application
app_version = "2.2.2.5" app_version = "2.2.2.6"
def get_application_root(): 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") OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "games.zip")
#CHEMINS DES EXECUTABLES #CHEMINS DES EXECUTABLES
UNRAR_EXE = os.path.join(APP_FOLDER,"assets", "unrar.exe") UNRAR_EXE = os.path.join(APP_FOLDER,"assets","progs","unrar.exe")
XDVDFS_EXE = os.path.join(APP_FOLDER,"assets", "xdvdfs.exe") XDVDFS_EXE = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs.exe")
XDVDFS_LINUX = os.path.join(APP_FOLDER,"assets", "xdvdfs") XDVDFS_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs")
if not HEADLESS: if not HEADLESS:
# Print des chemins pour debug # Print des chemins pour debug
@@ -191,6 +191,7 @@ trimui_controller = False
generic_controller = False generic_controller = False
xbox_elite_controller = False # Flag spécifique manette Xbox Elite xbox_elite_controller = False # Flag spécifique manette Xbox Elite
anbernic_rg35xx_controller = False # Flag spécifique Anbernic RG3xxx 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) --- # --- Filtre plateformes (UI) ---
selected_filter_index = 0 # index dans la liste visible triée selected_filter_index = 0 # index dans la liste visible triée
@@ -253,7 +254,7 @@ def init_font():
search_size = 48 search_size = 48
small_size = 28 small_size = 28
if fam == "pixel": 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)) f = pygame.font.Font(path, int(base_size * font_scale))
t = pygame.font.Font(path, int(title_size * font_scale)) t = pygame.font.Font(path, int(title_size * font_scale))
s = pygame.font.Font(path, int(search_size * font_scale)) s = pygame.font.Font(path, int(search_size * font_scale))
+51 -29
View File
@@ -2,6 +2,7 @@ import pygame # type: ignore
import shutil import shutil
import asyncio import asyncio
import json import json
import re
import os import os
import config import config
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE 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 # 2) Préréglages sans copie si aucun fichier utilisateur
try: try:
candidates = [] # --- Auto-match par nom de périphérique détecté ---
# Si aucun contrôleur détecté, privilégier le préréglage clavier def _sanitize(s: str) -> str:
if not getattr(config, 'joystick', False) or getattr(config, 'keyboard', False): s = (s or "").strip().lower()
candidates.append('keyboard.json') s = re.sub(r"[^a-z0-9]+", "_", s)
# Déterminer les préréglages disponibles selon les flags détectés au démarrage s = re.sub(r"_+", "_", s).strip("_")
if getattr(config, 'steam_controller', False): return s
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')
for fname in candidates: 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) 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): 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) data = json.load(f)
if isinstance(data, dict) and data: 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 return data
except Exception as e: except Exception as e:
logging.getLogger(__name__).warning(f"Échec du chargement des contrôles préréglés: {e}") logging.getLogger(__name__).warning(f"Échec du chargement des contrôles préréglés: {e}")
# 3) Fallback clavier par défaut # 3) Fallback: si joystick présent mais aucun préréglage trouvé, retourner {} pour déclencher le remap
logging.getLogger(__name__).info("Aucun fichier utilisateur ou préréglage trouvé, utilisation des contrôles par défaut") 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() return default_config.copy()
except Exception as e: except Exception as e:
+192 -23
View File
@@ -1,12 +1,21 @@
import pygame # type: ignore import pygame # type: ignore
import json import json
import os import os
import io
import logging import logging
import config import config
import language import language
from config import CONTROLS_CONFIG_PATH from config import CONTROLS_CONFIG_PATH
from display import draw_gradient from display import draw_gradient
import xml.etree.ElementTree as ET 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__) logger = logging.getLogger(__name__)
@@ -282,9 +291,77 @@ MOUSE_BUTTON_NAMES = {
# Durée de maintien pour valider une entrée (en millisecondes) # Durée de maintien pour valider une entrée (en millisecondes)
HOLD_DURATION = 1000 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) 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): def load_controls_config(path=CONTROLS_CONFIG_PATH):
"""Charge la configuration des contrôles depuis controls.json""" """Charge la configuration des contrôles depuis controls.json"""
try: try:
@@ -347,9 +424,64 @@ def get_readable_input_name(event):
return "Inconnu" 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): def map_controls(screen):
"""Interface de mappage des contrôles avec maintien de 3 secondes""" """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_action_index = 0
current_input = None current_input = None
input_held_time = 0 input_held_time = 0
@@ -357,6 +489,7 @@ def map_controls(screen):
last_frame_time = pygame.time.get_ticks() last_frame_time = pygame.time.get_ticks()
config.needs_redraw = True config.needs_redraw = True
last_joyhat_time = 0 last_joyhat_time = 0
next_input_allowed_time = 0 # timestamp until which new inputs are ignored after accept
# État des entrées maintenues # État des entrées maintenues
held_keys = set() held_keys = set()
@@ -424,6 +557,9 @@ def map_controls(screen):
# Détection des nouvelles entrées # Détection des nouvelles entrées
if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN): 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 event.type == pygame.JOYHATMOTION:
if (current_time - last_joyhat_time) < JOYHAT_DEBOUNCE: if (current_time - last_joyhat_time) < JOYHAT_DEBOUNCE:
continue continue
@@ -485,36 +621,41 @@ def map_controls(screen):
# Sauvegarder avec la structure attendue par controls.py # Sauvegarder avec la structure attendue par controls.py
if current_input["type"] == "key": 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] = { controls_config[action_name] = {
"type": "key", "type": "key",
"key": current_input["value"], "key": current_input["value"],
"display": last_input_name "display": disp
} }
elif current_input["type"] == "button": 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] = { controls_config[action_name] = {
"type": "button", "type": "button",
"button": current_input["value"], "button": current_input["value"],
"display": last_input_name "display": disp
} }
elif current_input["type"] == "axis": elif current_input["type"] == "axis":
axis, direction = current_input["value"] axis, direction = current_input["value"]
disp = get_preferred_display_for_action(action_name, "axis", (axis, direction)) or last_input_name
controls_config[action_name] = { controls_config[action_name] = {
"type": "axis", "type": "axis",
"axis": axis, "axis": axis,
"direction": direction, "direction": direction,
"display": last_input_name "display": disp
} }
elif current_input["type"] == "hat": 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] = { controls_config[action_name] = {
"type": "hat", "type": "hat",
"value": current_input["value"], "value": current_input["value"],
"display": last_input_name "display": disp
} }
elif current_input["type"] == "mouse": 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] = { controls_config[action_name] = {
"type": "mouse", "type": "mouse",
"button": current_input["value"], "button": current_input["value"],
"display": last_input_name "display": disp
} }
logger.debug(f"Contrôle mappé: {action_name} -> {controls_config[action_name]}") logger.debug(f"Contrôle mappé: {action_name} -> {controls_config[action_name]}")
@@ -523,6 +664,8 @@ def map_controls(screen):
input_held_time = 0 input_held_time = 0
last_input_name = None last_input_name = None
config.needs_redraw = True 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 # Réinitialiser les entrées maintenues
held_keys.clear() held_keys.clear()
@@ -552,6 +695,8 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr
border_radius = 24 border_radius = 24
border_width = 4 border_width = 4
shadow_offset = 8 shadow_offset = 8
bar_height = 25
min_bar_inner_width = 200 # largeur minimale utile de la barre
# Titre principal (traduction) # Titre principal (traduction)
title_text = language.get_text("controls_mapping_title", "Configuration des contrôles") 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)) title_rect = title_surface.get_rect(center=(config.screen_width // 2, 80))
screen.blit(title_surface, title_rect) 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) # Instructions (traduction)
instruction_text = language.get_text("controls_mapping_instruction", "Maintenez pendant 3s pour configurer :") instruction_text = language.get_text("controls_mapping_instruction", "Maintenez pendant 3s pour configurer :")
description_text = action.get('description', '') 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_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() input_width, input_height = input_surface.get_size()
# Dimensions de la popup # Dimensions de la popup (s'adapte au contenu et inclut la barre)
text_width = max(instruction_width, description_width, input_width) icon_width, icon_height = (icon_surface.get_size() if icon_surface else (0, 0))
text_height = instruction_height + description_height + input_height + 2 * padding_between inner_text_width = max(instruction_width, description_width, input_width, min_bar_inner_width, icon_width)
popup_width = text_width + 2 * padding_horizontal inner_text_height = 0
popup_height = text_height + 40 + 2 * padding_vertical # +40 pour la barre de progression 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_x = (config.screen_width - popup_width) // 2
popup_y = (config.screen_height - popup_height) // 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 # Bordure blanche
pygame.draw.rect(screen, (255, 255, 255), popup_rect, border_width, border_radius=border_radius) 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 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) screen.blit(instruction_surface, instruction_rect)
start_y += instruction_height + padding_between 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) screen.blit(description_surface, description_rect)
start_y += description_height + padding_between 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) screen.blit(input_surface, input_rect)
start_y += input_height + padding_between start_y += input_height + padding_between
# Barre de progression pour le maintien # Barre de progression pour le maintien (adaptée à la largeur intérieure de la popup)
bar_width = 300 bar_x = popup_x + padding_horizontal
bar_height = 25 bar_y = start_y
bar_x = (config.screen_width - bar_width) // 2 bar_width = popup_width - 2 * padding_horizontal
bar_y = start_y + 20
pygame.draw.rect(screen, (50, 50, 50), (bar_x, bar_y, bar_width, bar_height)) pygame.draw.rect(screen, (50, 50, 50), (bar_x, bar_y, bar_width, bar_height))
progress_width = bar_width * hold_progress 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, (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) 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: if hold_progress > 0:
progress_text = f"{int(hold_progress * 100)}%" progress_text = f"{int(hold_progress * 100)}%"
progress_surface = config.small_font.render(progress_text, True, (255, 255, 255)) 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) screen.blit(progress_surface, progress_rect)
+189 -16
View File
@@ -1,4 +1,8 @@
import pygame # type: ignore import pygame # type: ignore
import os
import io
import config import config
from utils import truncate_text_middle, wrap_text, load_system_image, truncate_text_end from utils import truncate_text_middle, wrap_text, load_system_image, truncate_text_end
import logging import logging
@@ -10,6 +14,145 @@ logger = logging.getLogger(__name__)
OVERLAY = None # Initialisé dans init_display() 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 # Couleurs modernes pour le thème
THEME_COLORS = { THEME_COLORS = {
# Fond des lignes sélectionnées # Fond des lignes sélectionnées
@@ -1271,6 +1414,24 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
start_text = _("controls_action_start") start_text = _("controls_action_start")
control_text = f"RGSX v{config.app_version} - {start_button} : {start_text}" 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 # Ajouter le nom de la musique si disponible
if config.current_music_name and config.music_popup_start_time > 0: if config.current_music_name and config.music_popup_start_time > 0:
current_time = pygame.time.get_ticks() / 1000 current_time = pygame.time.get_ticks() / 1000
@@ -1995,25 +2156,25 @@ def draw_filter_platforms_menu(screen):
# Menu aide contrôles # Menu aide contrôles
def draw_controls_help(screen, previous_state): def draw_controls_help(screen, previous_state):
"""Affiche la liste des contrôles (aide) avec mise en page adaptative.""" """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 = { control_categories = {
_("controls_category_navigation"): [ _("controls_category_navigation"): [
f"{get_control_display('up', '')} {get_control_display('down', '')} {get_control_display('left', '')} {get_control_display('right', '')} : {_('controls_navigation')}", ("icons", ["up", "down", "left", "right"], 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", ["page_up", "page_down"], f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {_('controls_pages')}"),
], ],
_("controls_category_main_actions"): [ _("controls_category_main_actions"): [
f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}", ("icons", ["confirm"], f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}"),
f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}", ("icons", ["cancel"], f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}"),
f"{get_control_display('start', 'Start')} : {_('controls_action_start')}", ("icons", ["start"], f"{get_control_display('start', 'Start')} : {_('controls_action_start')}"),
], ],
_("controls_category_downloads"): [ _("controls_category_downloads"): [
f"{get_control_display('history', 'Y')} : {_('controls_action_history')}", ("icons", ["history"], f"{get_control_display('history', 'Y')} : {_('controls_action_history')}"),
f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}", ("icons", ["clear_history"], f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}"),
], ],
_("controls_category_search"): [ _("controls_category_search"): [
f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}", ("icons", ["filter"], f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}"),
f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}", ("icons", ["delete"], f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}"),
f"{get_control_display('space', 'Espace')} : {_('controls_action_space')}", ("icons", ["space"], f"{get_control_display('space', 'Espace')} : {_('controls_action_space')}"),
], ],
} }
@@ -2062,8 +2223,19 @@ def draw_controls_help(screen, previous_state):
total_height += sec_surf.get_height() + line_spacing total_height += sec_surf.get_height() + line_spacing
for raw_line in lines: for raw_line in lines:
# Wrap par mots # Deux formats possibles:
words = raw_line.split() # - 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 = "" cur = ""
for word in words: for word in words:
test = (cur + " " + word).strip() test = (cur + " " + word).strip()
@@ -2072,9 +2244,6 @@ def draw_controls_help(screen, previous_state):
else: else:
if cur: if cur:
line_surf = font.render(cur, True, THEME_COLORS["text"]) line_surf = font.render(cur, True, THEME_COLORS["text"])
wrapped.append((False, line_surf)) wrapped.append((False, line_surf))
total_height += line_surf.get_height() + line_spacing total_height += line_surf.get_height() + line_spacing
max_width = max(max_width, line_surf.get_width()) max_width = max(max_width, line_surf.get_width())
@@ -2084,6 +2253,10 @@ def draw_controls_help(screen, previous_state):
wrapped.append((False, line_surf)) wrapped.append((False, line_surf))
total_height += line_surf.get_height() + line_spacing total_height += line_surf.get_height() + line_spacing
max_width = max(max_width, line_surf.get_width()) 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 total_height += section_spacing # espace après section
max_width = max(max_width, sec_surf.get_width()) max_width = max(max_width, sec_surf.get_width())
+13 -8
View File
@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen" ,"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen"
,"controls_desc_confirm": "Bestätigen (z.B. A/Kreuz)" ,"controls_desc_confirm": "Bestätigen (z.B. A/Kreuz)"
,"controls_desc_cancel": "Abbrechen/Zurück (z.B. B/Kreis)" ,"controls_desc_cancel": "Abbrechen/Zurück (z.B. B/Kreis)"
,"controls_desc_up": "Nach oben navigieren" ,"controls_desc_up": "UP ↑"
,"controls_desc_down": "Nach unten navigieren" ,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "Nach links navigieren" ,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "Nach rechts navigieren" ,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Schnell nach oben (z.B. LB/L1)" ,"controls_desc_page_up": "Schnell nach oben (z.B. LT/L2)"
,"controls_desc_page_down": "Schnell nach unten (z.B. RB/R1)" ,"controls_desc_page_down": "Schnell nach unten (z.B. RT/R2)"
,"controls_desc_history": "Verlauf öffnen (z.B. Y/Dreieck)" ,"controls_desc_history": "Verlauf öffnen (z.B. Y/Dreieck)"
,"controls_desc_clear_history": "Downloads: Mehrfachauswahl / Verlauf: Leeren (z.B. X/Quadrat)" ,"controls_desc_clear_history": "Downloads: Mehrfachauswahl / Verlauf: Leeren (z.B. X/Quadrat)"
,"controls_desc_filter": "Filtermodus: Öffnen/Bestätigen (z.B. Select)" ,"controls_desc_filter": "Filtermodus: Öffnen/Bestätigen (z.B. Select)"
,"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LT/L2)" ,"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LB/L1)"
,"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RT/R2)" ,"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RB/R1)"
,"controls_desc_start": "Pausenmenü öffnen (z.B. Start)" ,"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}"
} }
+13 -8
View File
@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "See detected premium provider API keys" ,"instruction_settings_api_keys": "See detected premium provider API keys"
,"controls_desc_confirm": "Confirm (e.g. A/Cross)" ,"controls_desc_confirm": "Confirm (e.g. A/Cross)"
,"controls_desc_cancel": "Cancel/Back (e.g. B/Circle)" ,"controls_desc_cancel": "Cancel/Back (e.g. B/Circle)"
,"controls_desc_up": "Navigate up" ,"controls_desc_up": "UP ↑"
,"controls_desc_down": "Navigate down" ,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "Navigate left" ,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "Navigate right" ,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Fast scroll up (e.g. LB/L1)" ,"controls_desc_page_up": "Fast scroll up (e.g. LT/L2)"
,"controls_desc_page_down": "Fast scroll down (e.g. RB/R1)" ,"controls_desc_page_down": "Fast scroll down (e.g. RT/R2)"
,"controls_desc_history": "Open history (e.g. Y/Triangle)" ,"controls_desc_history": "Open history (e.g. Y/Triangle)"
,"controls_desc_clear_history": "Downloads: Multi-select / History: Clear (e.g. X/Square)" ,"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_filter": "Filter mode: Open/Confirm (e.g. Select)"
,"controls_desc_delete": "Filter mode: Delete character (e.g. LT/L2)" ,"controls_desc_delete": "Filter mode: Delete character (e.g. LB/L1)"
,"controls_desc_space": "Filter mode: Add space (e.g. RT/R2)" ,"controls_desc_space": "Filter mode: Add space (e.g. RB/R1)"
,"controls_desc_start": "Open pause menu (e.g. Start)" ,"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}"
} }
+13 -8
View File
@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Ver claves API premium detectadas" ,"instruction_settings_api_keys": "Ver claves API premium detectadas"
,"controls_desc_confirm": "Confirmar (ej. A/Cruz)" ,"controls_desc_confirm": "Confirmar (ej. A/Cruz)"
,"controls_desc_cancel": "Cancelar/Volver (ej. B/Círculo)" ,"controls_desc_cancel": "Cancelar/Volver (ej. B/Círculo)"
,"controls_desc_up": "Navegar hacia arriba" ,"controls_desc_up": "UP ↑"
,"controls_desc_down": "Navegar hacia abajo" ,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "Navegar a la izquierda" ,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "Navegar a la derecha" ,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Desplazamiento rápido - (ej. LB/L1)" ,"controls_desc_page_up": "Desplazamiento rápido - (ej. LT/L2)"
,"controls_desc_page_down": "Desplazamiento rápido + (ej. RB/R1)" ,"controls_desc_page_down": "Desplazamiento rápido + (ej. RT/R2)"
,"controls_desc_history": "Abrir historial (ej. Y/Triángulo)" ,"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_clear_history": "Descargas: Selección múltiple / Historial: Limpiar (ej. X/Cuadrado)"
,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ej. Select)" ,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ej. Select)"
,"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LT/L2)" ,"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LB/L1)"
,"controls_desc_space": "Modo filtro: Añadir espacio (ej. RT/R2)" ,"controls_desc_space": "Modo filtro: Añadir espacio (ej. RB/R1)"
,"controls_desc_start": "Abrir menú pausa (ej. Start)" ,"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}"
} }
+13 -8
View File
@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Voir les clés API détectées des services premium" ,"instruction_settings_api_keys": "Voir les clés API détectées des services premium"
,"controls_desc_confirm": "Valider (ex: A/Croix)" ,"controls_desc_confirm": "Valider (ex: A/Croix)"
,"controls_desc_cancel": "Annuler/Retour (ex: B/Rond)" ,"controls_desc_cancel": "Annuler/Retour (ex: B/Rond)"
,"controls_desc_up": "Naviguer vers le haut" ,"controls_desc_up": "UP ↑"
,"controls_desc_down": "Naviguer vers le bas" ,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "Naviguer à gauche" ,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "Naviguer à droite" ,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Défilement Rapide - (ex: LB/L1)" ,"controls_desc_page_up": "Défilement Rapide - (ex: LT/L2)"
,"controls_desc_page_down": "Défilement Rapide + (ex: RB/R1)" ,"controls_desc_page_down": "Défilement Rapide + (ex: RT/R2)"
,"controls_desc_history": "Ouvrir l'historique (ex: Y/Triangle)" ,"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_clear_history": "Téléchargements : Sélection multiple / Historique : Vider (ex: X/Carré)"
,"controls_desc_filter": "Mode Filtre : Ouvrir/Valider (ex: Select)" ,"controls_desc_filter": "Mode Filtre : Ouvrir/Valider (ex: Select)"
,"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LT/L2)" ,"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LB/L1)"
,"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RT/R2)" ,"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RB/R1)"
,"controls_desc_start": "Ouvrir le menu pause (ex: Start)" ,"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}"
} }
+13 -8
View File
@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate" ,"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate"
,"controls_desc_confirm": "Confermare (es. A/Croce)" ,"controls_desc_confirm": "Confermare (es. A/Croce)"
,"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)" ,"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)"
,"controls_desc_up": "Navigare verso l'alto" ,"controls_desc_up": "UP ↑"
,"controls_desc_down": "Navigare verso il basso" ,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "Navigare a sinistra" ,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "Navigare a destra" ,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Scorrimento rapido su (es. LB/L1)" ,"controls_desc_page_up": "Scorrimento rapido su (es. LT/L2)"
,"controls_desc_page_down": "Scorrimento rapido giù (es. RB/R1)" ,"controls_desc_page_down": "Scorrimento rapido giù (es. RT/R2)"
,"controls_desc_history": "Aprire cronologia (es. Y/Triangolo)" ,"controls_desc_history": "Aprire cronologia (es. Y/Triangolo)"
,"controls_desc_clear_history": "Download: Selezione multipla / Cronologia: Svuotare (es. X/Quadrato)" ,"controls_desc_clear_history": "Download: Selezione multipla / Cronologia: Svuotare (es. X/Quadrato)"
,"controls_desc_filter": "Modalità filtro: Aprire/Confermare (es. Select)" ,"controls_desc_filter": "Modalità filtro: Aprire/Confermare (es. Select)"
,"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LT/L2)" ,"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LB/L1)"
,"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RT/R2)" ,"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RB/R1)"
,"controls_desc_start": "Aprire menu pausa (es. Start)" ,"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}"
} }
+13 -8
View File
@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Ver chaves API premium detectadas" ,"instruction_settings_api_keys": "Ver chaves API premium detectadas"
,"controls_desc_confirm": "Confirmar (ex. A/Cruz)" ,"controls_desc_confirm": "Confirmar (ex. A/Cruz)"
,"controls_desc_cancel": "Cancelar/Voltar (ex. B/Círculo)" ,"controls_desc_cancel": "Cancelar/Voltar (ex. B/Círculo)"
,"controls_desc_up": "Navegar para cima" ,"controls_desc_up": "UP ↑"
,"controls_desc_down": "Navegar para baixo" ,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "Navegar para esquerda" ,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "Navegar para direita" ,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Rolagem rápida para cima (ex. LB/L1)" ,"controls_desc_page_up": "Rolagem rápida para cima (ex. LT/L2)"
,"controls_desc_page_down": "Rolagem rápida para baixo (ex. RB/R1)" ,"controls_desc_page_down": "Rolagem rápida para baixo (ex. RT/R2)"
,"controls_desc_history": "Abrir histórico (ex. Y/Triângulo)" ,"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_clear_history": "Downloads: Seleção múltipla / Histórico: Limpar (ex. X/Quadrado)"
,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ex. Select)" ,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ex. Select)"
,"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LT/L2)" ,"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LB/L1)"
,"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RT/R2)" ,"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RB/R1)"
,"controls_desc_start": "Abrir menu pausa (ex. Start)" ,"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}"
} }
+120 -1
View File
@@ -2,8 +2,10 @@
import os import os
import sys import sys
import time import time
import json
import re
import traceback import traceback
from typing import Any, Dict, Tuple, List from typing import Any, Dict, Tuple, List, Optional
try: try:
import pygame # type: ignore 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}") 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: def main() -> None:
init_screen() init_screen()
js = init_joystick() 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") log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "controller_mapping.log")
write_log(log_path, mapping, js.get_name()) 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.") log("Done. Press Q or close the window to exit.")