Initial commit
This commit is contained in:
608
geniusia2/core/utils/input_utils.py
Normal file
608
geniusia2/core/utils/input_utils.py
Normal file
@@ -0,0 +1,608 @@
|
||||
"""
|
||||
Utilitaires d'entrée pour exécuter des actions UI (souris, clavier, etc.).
|
||||
Support du clavier AZERTY et gestion du rollback d'actions.
|
||||
"""
|
||||
|
||||
import time
|
||||
import pyautogui
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
from enum import Enum
|
||||
|
||||
from ..logger import Logger
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
"""Types d'actions UI supportées."""
|
||||
CLICK = "click"
|
||||
TYPE = "type"
|
||||
SCROLL = "scroll"
|
||||
WAIT = "wait"
|
||||
MOVE = "move"
|
||||
DRAG = "drag"
|
||||
|
||||
|
||||
class InputUtils:
|
||||
"""
|
||||
Gestionnaire d'entrées utilisateur pour exécuter des actions UI.
|
||||
Support du clavier AZERTY et rollback d'actions.
|
||||
"""
|
||||
|
||||
def __init__(self, logger: Logger, config: Dict[str, Any]):
|
||||
"""
|
||||
Initialise les utilitaires d'entrée.
|
||||
|
||||
Args:
|
||||
logger: Logger pour journalisation
|
||||
config: Configuration globale
|
||||
"""
|
||||
self.logger = logger
|
||||
self.config = config
|
||||
|
||||
# Configuration PyAutoGUI
|
||||
pyautogui.FAILSAFE = True # Déplacer souris dans coin = arrêt
|
||||
pyautogui.PAUSE = config.get("input", {}).get("pause_between_actions", 0.1)
|
||||
|
||||
# Historique des actions pour rollback
|
||||
self.action_history = []
|
||||
|
||||
# Mapping AZERTY pour caractères spéciaux
|
||||
self.azerty_mapping = {
|
||||
'0': 'à',
|
||||
'1': '&',
|
||||
'2': 'é',
|
||||
'3': '"',
|
||||
'4': "'",
|
||||
'5': '(',
|
||||
'6': '-',
|
||||
'7': 'è',
|
||||
'8': '_',
|
||||
'9': 'ç',
|
||||
'.': ':',
|
||||
'/': '!',
|
||||
',': ';',
|
||||
';': ',',
|
||||
':': '.',
|
||||
'!': '/',
|
||||
'?': 'M', # Shift + ,
|
||||
}
|
||||
|
||||
self.logger.log_action({
|
||||
"action": "input_utils_initialized",
|
||||
"failsafe": True,
|
||||
"pause": pyautogui.PAUSE
|
||||
})
|
||||
|
||||
def click(
|
||||
self,
|
||||
x: int,
|
||||
y: int,
|
||||
button: str = "left",
|
||||
clicks: int = 1,
|
||||
interval: float = 0.0
|
||||
) -> bool:
|
||||
"""
|
||||
Effectue un clic souris à la position spécifiée.
|
||||
|
||||
Args:
|
||||
x: Coordonnée X
|
||||
y: Coordonnée Y
|
||||
button: Bouton souris ("left", "right", "middle")
|
||||
clicks: Nombre de clics
|
||||
interval: Intervalle entre clics multiples
|
||||
|
||||
Returns:
|
||||
True si succès, False sinon
|
||||
"""
|
||||
try:
|
||||
# Enregistrer position actuelle pour rollback
|
||||
current_pos = pyautogui.position()
|
||||
|
||||
# Effectuer le clic
|
||||
pyautogui.click(x, y, clicks=clicks, interval=interval, button=button)
|
||||
|
||||
# Enregistrer dans l'historique
|
||||
action_record = {
|
||||
"type": ActionType.CLICK.value,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": button,
|
||||
"clicks": clicks,
|
||||
"previous_position": current_pos,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.action_history.append(action_record)
|
||||
|
||||
self.logger.log_action({
|
||||
"action": "click_executed",
|
||||
**action_record
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log_action({
|
||||
"action": "click_failed",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"error": str(e)
|
||||
})
|
||||
return False
|
||||
|
||||
def type_text(
|
||||
self,
|
||||
text: str,
|
||||
interval: float = 0.0,
|
||||
use_azerty: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Saisit du texte au clavier.
|
||||
|
||||
Args:
|
||||
text: Texte à saisir
|
||||
interval: Intervalle entre chaque caractère
|
||||
use_azerty: Utiliser le mapping AZERTY
|
||||
|
||||
Returns:
|
||||
True si succès, False sinon
|
||||
"""
|
||||
try:
|
||||
# Convertir pour AZERTY si nécessaire
|
||||
if use_azerty:
|
||||
converted_text = self._convert_to_azerty(text)
|
||||
else:
|
||||
converted_text = text
|
||||
|
||||
# Saisir le texte
|
||||
pyautogui.write(converted_text, interval=interval)
|
||||
|
||||
# Enregistrer dans l'historique
|
||||
action_record = {
|
||||
"type": ActionType.TYPE.value,
|
||||
"text": text,
|
||||
"converted_text": converted_text,
|
||||
"length": len(text),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.action_history.append(action_record)
|
||||
|
||||
self.logger.log_action({
|
||||
"action": "text_typed",
|
||||
**action_record
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log_action({
|
||||
"action": "type_text_failed",
|
||||
"text": text[:50], # Limiter pour logs
|
||||
"error": str(e)
|
||||
})
|
||||
return False
|
||||
|
||||
def scroll(
|
||||
self,
|
||||
direction: str,
|
||||
amount: int = 3,
|
||||
x: Optional[int] = None,
|
||||
y: Optional[int] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Effectue un défilement.
|
||||
|
||||
Args:
|
||||
direction: Direction ("up", "down", "left", "right")
|
||||
amount: Quantité de défilement (nombre de "clics" de molette)
|
||||
x: Position X optionnelle
|
||||
y: Position Y optionnelle
|
||||
|
||||
Returns:
|
||||
True si succès, False sinon
|
||||
"""
|
||||
try:
|
||||
# Calculer le montant de défilement
|
||||
if direction in ["up", "right"]:
|
||||
scroll_amount = amount
|
||||
elif direction in ["down", "left"]:
|
||||
scroll_amount = -amount
|
||||
else:
|
||||
raise ValueError(f"Direction invalide: {direction}")
|
||||
|
||||
# Déplacer la souris si position spécifiée
|
||||
if x is not None and y is not None:
|
||||
pyautogui.moveTo(x, y)
|
||||
|
||||
# Effectuer le défilement
|
||||
if direction in ["up", "down"]:
|
||||
pyautogui.scroll(scroll_amount)
|
||||
else:
|
||||
pyautogui.hscroll(scroll_amount)
|
||||
|
||||
# Enregistrer dans l'historique
|
||||
action_record = {
|
||||
"type": ActionType.SCROLL.value,
|
||||
"direction": direction,
|
||||
"amount": amount,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.action_history.append(action_record)
|
||||
|
||||
self.logger.log_action({
|
||||
"action": "scroll_executed",
|
||||
**action_record
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log_action({
|
||||
"action": "scroll_failed",
|
||||
"direction": direction,
|
||||
"amount": amount,
|
||||
"error": str(e)
|
||||
})
|
||||
return False
|
||||
|
||||
def wait(self, duration: float) -> bool:
|
||||
"""
|
||||
Attend pendant une durée spécifiée.
|
||||
|
||||
Args:
|
||||
duration: Durée en secondes
|
||||
|
||||
Returns:
|
||||
True
|
||||
"""
|
||||
try:
|
||||
time.sleep(duration)
|
||||
|
||||
action_record = {
|
||||
"type": ActionType.WAIT.value,
|
||||
"duration": duration,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.action_history.append(action_record)
|
||||
|
||||
self.logger.log_action({
|
||||
"action": "wait_executed",
|
||||
**action_record
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log_action({
|
||||
"action": "wait_failed",
|
||||
"duration": duration,
|
||||
"error": str(e)
|
||||
})
|
||||
return False
|
||||
|
||||
def move(self, x: int, y: int, duration: float = 0.2) -> bool:
|
||||
"""
|
||||
Déplace la souris vers une position.
|
||||
|
||||
Args:
|
||||
x: Coordonnée X
|
||||
y: Coordonnée Y
|
||||
duration: Durée du mouvement en secondes
|
||||
|
||||
Returns:
|
||||
True si succès, False sinon
|
||||
"""
|
||||
try:
|
||||
current_pos = pyautogui.position()
|
||||
|
||||
pyautogui.moveTo(x, y, duration=duration)
|
||||
|
||||
action_record = {
|
||||
"type": ActionType.MOVE.value,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"previous_position": current_pos,
|
||||
"duration": duration,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.action_history.append(action_record)
|
||||
|
||||
self.logger.log_action({
|
||||
"action": "move_executed",
|
||||
**action_record
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log_action({
|
||||
"action": "move_failed",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"error": str(e)
|
||||
})
|
||||
return False
|
||||
|
||||
def drag(
|
||||
self,
|
||||
start_x: int,
|
||||
start_y: int,
|
||||
end_x: int,
|
||||
end_y: int,
|
||||
duration: float = 0.5,
|
||||
button: str = "left"
|
||||
) -> bool:
|
||||
"""
|
||||
Effectue un glisser-déposer.
|
||||
|
||||
Args:
|
||||
start_x: X de départ
|
||||
start_y: Y de départ
|
||||
end_x: X d'arrivée
|
||||
end_y: Y d'arrivée
|
||||
duration: Durée du glissement
|
||||
button: Bouton souris
|
||||
|
||||
Returns:
|
||||
True si succès, False sinon
|
||||
"""
|
||||
try:
|
||||
current_pos = pyautogui.position()
|
||||
|
||||
pyautogui.moveTo(start_x, start_y)
|
||||
pyautogui.drag(end_x - start_x, end_y - start_y, duration=duration, button=button)
|
||||
|
||||
action_record = {
|
||||
"type": ActionType.DRAG.value,
|
||||
"start_x": start_x,
|
||||
"start_y": start_y,
|
||||
"end_x": end_x,
|
||||
"end_y": end_y,
|
||||
"previous_position": current_pos,
|
||||
"duration": duration,
|
||||
"button": button,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.action_history.append(action_record)
|
||||
|
||||
self.logger.log_action({
|
||||
"action": "drag_executed",
|
||||
**action_record
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log_action({
|
||||
"action": "drag_failed",
|
||||
"start": (start_x, start_y),
|
||||
"end": (end_x, end_y),
|
||||
"error": str(e)
|
||||
})
|
||||
return False
|
||||
|
||||
def execute_inverse_action(self, action: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Exécute l'action inverse pour rollback.
|
||||
|
||||
Args:
|
||||
action: Action à inverser
|
||||
|
||||
Returns:
|
||||
True si succès, False sinon
|
||||
"""
|
||||
inverse = self.get_inverse_action(action)
|
||||
if not inverse:
|
||||
return False
|
||||
|
||||
action_type = inverse.get("type")
|
||||
|
||||
if action_type == ActionType.MOVE.value:
|
||||
return self.move(inverse["x"], inverse["y"], inverse.get("duration", 0.2))
|
||||
elif action_type == ActionType.SCROLL.value:
|
||||
return self.scroll(
|
||||
inverse["direction"],
|
||||
inverse["amount"],
|
||||
inverse.get("x"),
|
||||
inverse.get("y")
|
||||
)
|
||||
elif action_type == ActionType.DRAG.value:
|
||||
return self.drag(
|
||||
inverse["start_x"],
|
||||
inverse["start_y"],
|
||||
inverse["end_x"],
|
||||
inverse["end_y"],
|
||||
inverse.get("duration", 0.5),
|
||||
inverse.get("button", "left")
|
||||
)
|
||||
elif action_type == "press_key":
|
||||
# Exécuter les suppressions
|
||||
for _ in range(inverse.get("presses", 0)):
|
||||
pyautogui.press("backspace")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_inverse_action(self, action: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Génère l'action inverse pour rollback.
|
||||
|
||||
Args:
|
||||
action: Action à inverser
|
||||
|
||||
Returns:
|
||||
Action inverse ou None si non inversible
|
||||
"""
|
||||
action_type = action.get("type")
|
||||
|
||||
if action_type == ActionType.CLICK.value:
|
||||
# Un clic n'est pas vraiment inversible
|
||||
# On peut retourner à la position précédente
|
||||
prev_pos = action.get("previous_position")
|
||||
if prev_pos:
|
||||
return {
|
||||
"type": ActionType.MOVE.value,
|
||||
"x": prev_pos[0],
|
||||
"y": prev_pos[1],
|
||||
"duration": 0.2
|
||||
}
|
||||
|
||||
elif action_type == ActionType.TYPE.value:
|
||||
# Inverser la saisie = supprimer le texte
|
||||
text_length = action.get("length", 0)
|
||||
return {
|
||||
"type": "press_key",
|
||||
"key": "backspace",
|
||||
"presses": text_length
|
||||
}
|
||||
|
||||
elif action_type == ActionType.SCROLL.value:
|
||||
# Inverser le défilement
|
||||
direction = action.get("direction")
|
||||
amount = action.get("amount")
|
||||
inverse_direction = {
|
||||
"up": "down",
|
||||
"down": "up",
|
||||
"left": "right",
|
||||
"right": "left"
|
||||
}.get(direction)
|
||||
|
||||
return {
|
||||
"type": ActionType.SCROLL.value,
|
||||
"direction": inverse_direction,
|
||||
"amount": amount,
|
||||
"x": action.get("x"),
|
||||
"y": action.get("y")
|
||||
}
|
||||
|
||||
elif action_type == ActionType.MOVE.value:
|
||||
# Retourner à la position précédente
|
||||
prev_pos = action.get("previous_position")
|
||||
if prev_pos:
|
||||
return {
|
||||
"type": ActionType.MOVE.value,
|
||||
"x": prev_pos[0],
|
||||
"y": prev_pos[1],
|
||||
"duration": 0.2
|
||||
}
|
||||
|
||||
elif action_type == ActionType.DRAG.value:
|
||||
# Inverser le glissement
|
||||
return {
|
||||
"type": ActionType.DRAG.value,
|
||||
"start_x": action.get("end_x"),
|
||||
"start_y": action.get("end_y"),
|
||||
"end_x": action.get("start_x"),
|
||||
"end_y": action.get("start_y"),
|
||||
"duration": action.get("duration", 0.5),
|
||||
"button": action.get("button", "left")
|
||||
}
|
||||
|
||||
elif action_type == ActionType.WAIT.value:
|
||||
# L'attente n'a pas d'inverse
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def _convert_to_azerty(self, text: str) -> str:
|
||||
"""
|
||||
Convertit du texte pour clavier AZERTY.
|
||||
|
||||
Args:
|
||||
text: Texte à convertir
|
||||
|
||||
Returns:
|
||||
Texte converti
|
||||
"""
|
||||
# Pour l'instant, retourner tel quel
|
||||
# PyAutoGUI gère déjà le layout clavier du système
|
||||
# Cette méthode peut être étendue si nécessaire
|
||||
return text
|
||||
|
||||
def get_action_history(self, limit: int = 50) -> list:
|
||||
"""
|
||||
Retourne l'historique des actions.
|
||||
|
||||
Args:
|
||||
limit: Nombre maximum d'actions à retourner
|
||||
|
||||
Returns:
|
||||
Liste des dernières actions
|
||||
"""
|
||||
return self.action_history[-limit:]
|
||||
|
||||
def clear_history(self):
|
||||
"""Efface l'historique des actions."""
|
||||
self.action_history = []
|
||||
self.logger.log_action({
|
||||
"action": "action_history_cleared"
|
||||
})
|
||||
|
||||
def execute_action(self, action_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Exécute une action depuis un dictionnaire de données.
|
||||
|
||||
Args:
|
||||
action_data: Données de l'action à exécuter
|
||||
{
|
||||
"action_type": str,
|
||||
"bbox": (x, y, w, h),
|
||||
"parameters": dict
|
||||
}
|
||||
|
||||
Returns:
|
||||
True si succès, False sinon
|
||||
"""
|
||||
action_type = action_data.get("action_type", "").lower()
|
||||
bbox = action_data.get("bbox", (0, 0, 0, 0))
|
||||
params = action_data.get("parameters", {})
|
||||
|
||||
# Calculer le centre de la bbox pour les actions de clic
|
||||
x, y, w, h = bbox
|
||||
center_x = x + w // 2
|
||||
center_y = y + h // 2
|
||||
|
||||
if action_type == "click":
|
||||
button = params.get("button", "left")
|
||||
clicks = params.get("clicks", 1)
|
||||
return self.click(center_x, center_y, button=button, clicks=clicks)
|
||||
|
||||
elif action_type == "double_click":
|
||||
return self.click(center_x, center_y, clicks=2)
|
||||
|
||||
elif action_type == "right_click":
|
||||
return self.click(center_x, center_y, button="right")
|
||||
|
||||
elif action_type == "type":
|
||||
text = params.get("text", "")
|
||||
interval = params.get("interval", 0.0)
|
||||
return self.type_text(text, interval=interval)
|
||||
|
||||
elif action_type == "scroll":
|
||||
direction = params.get("direction", "down")
|
||||
amount = params.get("amount", 3)
|
||||
return self.scroll(direction, amount, center_x, center_y)
|
||||
|
||||
elif action_type == "wait":
|
||||
duration = params.get("duration", 1.0)
|
||||
return self.wait(duration)
|
||||
|
||||
elif action_type == "move":
|
||||
duration = params.get("duration", 0.2)
|
||||
return self.move(center_x, center_y, duration=duration)
|
||||
|
||||
elif action_type == "drag":
|
||||
end_bbox = params.get("end_bbox", bbox)
|
||||
end_x, end_y, end_w, end_h = end_bbox
|
||||
end_center_x = end_x + end_w // 2
|
||||
end_center_y = end_y + end_h // 2
|
||||
duration = params.get("duration", 0.5)
|
||||
button = params.get("button", "left")
|
||||
return self.drag(center_x, center_y, end_center_x, end_center_y, duration, button)
|
||||
|
||||
else:
|
||||
self.logger.log_action({
|
||||
"action": "unknown_action_type",
|
||||
"action_type": action_type
|
||||
})
|
||||
return False
|
||||
Reference in New Issue
Block a user