#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
**Mécanique basique de gestion des événements pygame.**
Classes utilisées par les modules **core** et **widgets**.
.. note::
V1 mécanique du type top -> bottom à faire évoluer en bottom -> top -> bottom
pour permettre la capture d'événements à différents moments du flux.
"""
# imports :
import pygame.locals
# Evite l'ajout non désiré de certains imports à la doc sphinx
__all__ = [
"CustomEventManager",
"GUIEventManager",
"CustomBaseControl",
"CustomBaseInput",
"CustomBaseButton",
]
# classes :
[docs]class CustomEventManager:
"""
Gestionnaire d'événements utilisé par composition par la GUI.
"""
# familles de touches :
LETTERS_KEYS = [
pygame.locals.K_a,
pygame.locals.K_b,
pygame.locals.K_c,
pygame.locals.K_d,
pygame.locals.K_e,
pygame.locals.K_f,
pygame.locals.K_g,
pygame.locals.K_h,
pygame.locals.K_i,
pygame.locals.K_j,
pygame.locals.K_k,
pygame.locals.K_l,
pygame.locals.K_m,
pygame.locals.K_n,
pygame.locals.K_o,
pygame.locals.K_p,
pygame.locals.K_q,
pygame.locals.K_r,
pygame.locals.K_s,
pygame.locals.K_t,
pygame.locals.K_u,
pygame.locals.K_v,
pygame.locals.K_w,
pygame.locals.K_x,
pygame.locals.K_y,
pygame.locals.K_z,
] #: Liste des touches de lettres
NUMBERS_KEYS = [
pygame.locals.K_0,
pygame.locals.K_1,
pygame.locals.K_2,
pygame.locals.K_3,
pygame.locals.K_4,
pygame.locals.K_5,
pygame.locals.K_6,
pygame.locals.K_7,
pygame.locals.K_8,
pygame.locals.K_9,
pygame.locals.K_KP0,
pygame.locals.K_KP1,
pygame.locals.K_KP2,
pygame.locals.K_KP3,
pygame.locals.K_KP4,
pygame.locals.K_KP5,
pygame.locals.K_KP6,
pygame.locals.K_KP7,
pygame.locals.K_KP8,
pygame.locals.K_KP9,
] #: Liste des touches d'entiers
PONCT_KEYS = [
pygame.locals.K_EXCLAIM,
pygame.locals.K_QUOTEDBL,
pygame.locals.K_HASH,
pygame.locals.K_DOLLAR,
pygame.locals.K_AMPERSAND,
pygame.locals.K_QUOTE,
pygame.locals.K_LEFTPAREN,
pygame.locals.K_RIGHTPAREN,
pygame.locals.K_ASTERISK,
pygame.locals.K_PLUS,
pygame.locals.K_COMMA,
pygame.locals.K_MINUS,
pygame.locals.K_PERIOD,
pygame.locals.K_SLASH,
pygame.locals.K_COLON,
pygame.locals.K_SEMICOLON,
pygame.locals.K_LESS,
pygame.locals.K_EQUALS,
pygame.locals.K_GREATER,
pygame.locals.K_QUESTION,
pygame.locals.K_AT,
pygame.locals.K_LEFTBRACKET,
pygame.locals.K_BACKSLASH,
pygame.locals.K_RIGHTBRACKET,
pygame.locals.K_CARET,
pygame.locals.K_UNDERSCORE,
pygame.locals.K_BACKQUOTE,
pygame.locals.K_KP_PERIOD,
pygame.locals.K_KP_DIVIDE,
pygame.locals.K_KP_MULTIPLY,
pygame.locals.K_KP_MINUS,
pygame.locals.K_KP_PLUS,
pygame.locals.K_KP_EQUALS,
] #: Liste des touches de ponctutaion
ARROW_KEYS = [
pygame.locals.K_UP,
pygame.locals.K_DOWN,
pygame.locals.K_RIGHT,
pygame.locals.K_LEFT,
] #: Liste des touches de flèches
COMMAND_KEYS = [
pygame.locals.K_BACKSPACE,
pygame.locals.K_TAB,
pygame.locals.K_RETURN,
pygame.locals.K_ESCAPE,
pygame.locals.K_SPACE,
pygame.locals.K_DELETE,
pygame.locals.K_KP_ENTER,
] #: Liste des touches de commande
# abonnement / désabonnement d'un contrôle
CE_REGISTER = pygame.USEREVENT + 1 #: événement abonnement de contrôle
CE_UNREGISTER = pygame.USEREVENT + 2 #: événement désabonnement de contrôle
# événement de demande de focus pour un input
CE_ASK_INPUT_FOCUS = pygame.USEREVENT + 3 #: événement demande de focus d'input
# types d'événements ciblés par un contrôle :
MOUSE_CLIC = "MOUSE_CLIC" #: événement souris
MOUSE_OVER = "MOUSE_OVER" #: événement souris
MOUSE_OUT = "MOUSE_OUT" #: événement souris
MOUSE_MOVE = "MOUSE_MOVE" #: événement souris
KEY_PRESSED = "KEY_PRESSED" #: événement touche
KEY_RELEASED = "KEY_RELEASED" #: événement touche
# méthodes
def __init__(self, Mngr):
"""
Constructeur
Args:
Mngr (GUIEventManager): interface de la GUI pygame
"""
# ref à la GUI :
self.Mngr = Mngr
# initalisations :
self._init_control_dict()
def _init_control_dict(self):
"""
Initialise le dict d'enregistrement des contrôles (boutons et inputs).
"""
self.ctrl_dict = dict()
self.ctrl_dict[CustomEventManager.MOUSE_CLIC] = list()
self.ctrl_dict[CustomEventManager.MOUSE_MOVE] = list()
self.ctrl_dict[CustomEventManager.KEY_PRESSED] = list()
self.ctrl_dict[CustomEventManager.KEY_RELEASED] = list()
self.ctrl_dict["InputControls"] = list()
self.ctrl_dict["ButtonOver"] = None
[docs] def handle_events(self):
"""
Interface avec la GUI : méthode à appeler à chaque frame d'éxécution.
Dépile les événements pygame, applique les traitements internes, informe la GUI.
"""
for e in pygame.event.get():
if e.type == CustomEventManager.CE_REGISTER:
self._register_control(e)
elif e.type == CustomEventManager.CE_UNREGISTER:
self._unregister_control(e)
elif e.type == CustomEventManager.CE_ASK_INPUT_FOCUS:
self._handle_entry_focus_event(e)
elif e.type in [
pygame.MOUSEBUTTONDOWN,
pygame.MOUSEBUTTONUP,
pygame.MOUSEMOTION,
]:
self._handle_mouse_evt(e)
elif e.type in [pygame.KEYDOWN, pygame.KEYUP]:
self._handle_key_evt(e)
elif e.type == pygame.VIDEORESIZE:
self.Mngr.on_resize_event(e)
elif e.type == pygame.QUIT:
# si on souhaite fermer avec la touche escape :
# or (e.type == pygame.KEYDOWN and e.key == pygame.K_ESCAPE):
# Rq: touche plutôt utilisée pour quitter le plein écran.
self.Mngr.on_quit_event(e)
elif e.type == pygame.ACTIVEEVENT:
self.Mngr.on_active_event(e)
def _handle_mouse_evt(self, evt):
"""
Gestion interne des événements souris
"""
ctrllist = None
if evt.type == pygame.MOUSEBUTTONDOWN and evt.button == 1:
# ie CustomEventManager.MOUSE_CLIC avec le bouton gauche
ctrllist = self.ctrl_dict[CustomEventManager.MOUSE_CLIC]
elif evt.type == pygame.MOUSEBUTTONUP and evt.button == 1:
# release soit CustomEventManager.MOUSE_OVER
ctrllist = self.ctrl_dict[CustomEventManager.MOUSE_MOVE]
elif evt.type == pygame.MOUSEMOTION:
# CustomEventManager.MOUSE_OVER ou CustomEventManager.MOUSE_OUT
ctrllist = self.ctrl_dict[CustomEventManager.MOUSE_MOVE]
if ctrllist == None:
return
# ctrl survolés ou non par la souris
colidelist = list()
noncolidelist = list()
for ctrl in ctrllist:
if ctrl.enabled and ctrl.visible:
if ctrl.globalRect.collidepoint(evt.pos):
colidelist.append(ctrl)
else:
noncolidelist.append(ctrl)
# Filtrage : on sélectionne le ctrl activé, visible et de plus grand z-index :
overctrl = None
prevoverctrl = self.ctrl_dict["ButtonOver"]
if len(colidelist) > 0:
overctrl = colidelist[0]
# contrôles survolés ou cliqués :
if evt.type == pygame.MOUSEBUTTONDOWN:
customevent = CustomEventManager.MOUSE_CLIC
else:
customevent = CustomEventManager.MOUSE_OVER
# Out du précédent :
if prevoverctrl != None and prevoverctrl != overctrl:
prevoverctrl.handle_mouse_event(CustomEventManager.MOUSE_OUT, evt)
# Over ou Clic du nouveau
if overctrl != None:
overctrl.handle_mouse_event(customevent, evt)
self.ctrl_dict["ButtonOver"] = overctrl
def _handle_entry_focus_event(self, evt):
"""
Gère l'affectation du focus parmi les éventuels CustomBaseInput
"""
entryctrl = evt.control
for ctrl in self.ctrl_dict["InputControls"]:
ctrl.focused = False
entryctrl.focused = True
def _handle_key_evt(self, evt):
"""
Gestion interne des événements clavier
"""
if evt.type == pygame.KEYUP:
customevent = CustomEventManager.KEY_RELEASED
alllist = self.ctrl_dict[CustomEventManager.KEY_RELEASED]
else:
customevent = CustomEventManager.KEY_PRESSED
alllist = self.ctrl_dict[CustomEventManager.KEY_PRESSED]
# propagation systématique : seul l'input focusé réagira
for ctrl in alllist:
if ctrl.enabled:
ctrl.handle_key_event(customevent, evt)
def _register_control(self, evt):
"""
Abonnement d'un control (via propagation d'un evt CustomEventManager.CE_REGISTER).
"""
ctrl = evt.control
# Filtrage : CustomSprite héritant de CustomBaseControl
# => tests à remplacer par une interface abstraite
if (
not isinstance(ctrl, CustomBaseControl)
or not hasattr(ctrl, "globalRect")
or not hasattr(ctrl, "visible")
or not hasattr(ctrl, "global_layer")
):
return
eventtypes = ctrl.get_event_types()
for evttype in eventtypes:
# cas particulier des inputs text :
if isinstance(ctrl, CustomBaseInput):
if ctrl not in self.ctrl_dict["InputControls"]:
self.ctrl_dict["InputControls"].append(ctrl)
# cas général :
if evttype == CustomEventManager.MOUSE_CLIC:
if ctrl not in self.ctrl_dict[evttype]:
self.ctrl_dict[evttype].append(ctrl)
elif evttype in [
CustomEventManager.MOUSE_OVER,
CustomEventManager.MOUSE_OUT,
]:
if ctrl not in self.ctrl_dict[CustomEventManager.MOUSE_MOVE]:
self.ctrl_dict[CustomEventManager.MOUSE_MOVE].append(ctrl)
elif evttype == CustomEventManager.KEY_PRESSED:
if ctrl not in self.ctrl_dict[CustomEventManager.KEY_PRESSED]:
self.ctrl_dict[CustomEventManager.KEY_PRESSED].append(ctrl)
elif evttype == CustomEventManager.KEY_RELEASED:
if ctrl not in self.ctrl_dict[CustomEventManager.KEY_RELEASED]:
self.ctrl_dict[CustomEventManager.KEY_RELEASED].append(ctrl)
def _unregister_control(self, evt):
"""
Désabonnement d'un control (via propagation d'un evt CustomEventManager.CE_UNREGISTER).
"""
ctrl = evt.control
eventtypes = ctrl.get_event_types()
for evttype in eventtypes:
# cas particulier des inputs text :
if isinstance(ctrl, CustomBaseInput):
if ctrl in self.ctrl_dict["InputControls"]:
self.ctrl_dict["InputControls"].remove(ctrl)
# cas général :
if evttype == CustomEventManager.MOUSE_CLIC:
if ctrl in self.ctrl_dict[CustomEventManager.MOUSE_CLIC]:
self.ctrl_dict[CustomEventManager.MOUSE_CLIC].remove(ctrl)
elif evttype in [
CustomEventManager.MOUSE_OVER,
CustomEventManager.MOUSE_OUT,
]:
if ctrl in self.ctrl_dict[CustomEventManager.MOUSE_MOVE]:
self.ctrl_dict[CustomEventManager.MOUSE_MOVE].remove(ctrl)
elif evttype == CustomEventManager.KEY_PRESSED:
if ctrl in self.ctrl_dict[CustomEventManager.KEY_PRESSED]:
self.ctrl_dict[CustomEventManager.KEY_PRESSED].remove(ctrl)
elif evttype == CustomEventManager.KEY_RELEASED:
if ctrl in self.ctrl_dict[CustomEventManager.KEY_RELEASED]:
self.ctrl_dict[CustomEventManager.KEY_RELEASED].remove(ctrl)
[docs]class GUIEventManager:
"""
"Interface" de la GUI utilisant par composition CustomEventManager.
"""
def __init__(self):
"""
Initialisation de la gestion des événements
"""
# Manager d'événement :
self._eventMngr = CustomEventManager(self)
[docs] def handle_events(self):
"""
Méthode à appeler à chaque frame d'exécution pour traiter les événements.
"""
self._eventMngr.handle_events()
[docs] def on_resize_event(self, event):
"""
Méthode appelée par CustomEventManager lorsque survient l'événement pygame.VIDEORESIZE
(event a pour attributs : size, w, h).
"""
# à subclasser
pass
[docs] def on_quit_event(self, event):
"""
Méthode appelée par CustomEventManager lorsque survient l'événement pygame.QUIT
(l'événement n'a aucun attribut particulier).
"""
# à subclasser
pass
[docs] def on_active_event(self, event):
"""
Méthode appelée par CustomEventManager lorsque survient l'événement pygame.ACTIVEEVENT,
indiquant si la fenêtre a ou non le focus (gain : boolean (0/1) indiquant le focus,
state : non documenté).
"""
# à subclasser
pass
[docs]class CustomBaseControl:
"""
Classe de base d'un contrôle utilisateur (bouton, entry).
A implémenter dans une succlasse héritant de CustomSprite.
"""
def __init__(self, evtdict, **kwargs):
"""
Constructeur
Args:
evtdict (dict): {"evttypes":}, avec evttypes une liste de valeurs parmi :
MOUSE_CLIC, MOUSE_OVER, MOUSE_OUT, KEY_PRESSED
"""
# spécifique :
self._enabled = True
self._focused = False
# événements écoutés :
self._eventdict = evtdict
# activation du contrôle
def _get_enabled(self):
"""Etat d'activation du contrôle."""
return self._enabled
def _set_enabled(self, val):
if bool(val) != self._enabled:
if bool(val):
self._enabled = True
else:
self._enabled = False
self.on_enable_changed()
enabled = property(
_get_enabled, _set_enabled
) #: Etat d'activation du contrôle (bool).
[docs] def on_enable_changed(self):
"""
Appelée lorsque self.enabled a été modifié.
"""
# à subclasser
pass
# focus du contrôle
def _get_focused(self):
"""Le contrôle a t'il le focus?"""
return self._focused
def _set_focused(self, val):
if bool(val):
self._focused = True
else:
self._focused = False
focused = property(_get_focused, _set_focused) #: Le contrôle a t'il le focus?
[docs] def get_event_types(self):
"""
Retourne la liste d'événements écoutés par le ctrl.
"""
return self._eventdict["evttypes"]
[docs] def register(self):
"""
Abonnement auprès du manager d'événements.
A ajouter à la méthode générique d'ajout à la displaylist
"""
if self._eventdict != None and "evttypes" in self._eventdict.keys():
args = {"control": self}
evt = pygame.event.Event(CustomEventManager.CE_REGISTER, args)
self.fire_event(evt)
[docs] def unregister(self):
"""
Désabonnement auprès du manager d'événements.
A ajouter à la méthode générique de retrait de la displaylist
"""
if self._eventdict != None and "evttypes" in self._eventdict.keys():
args = {"control": self}
evt = pygame.event.Event(CustomEventManager.CE_UNREGISTER, args)
self.fire_event(evt)
[docs] def fire_event(self, evt):
"""
Dispatch l'événement evt.
"""
pygame.event.post(evt)
[docs] def handle_mouse_event(self, customeventtype, event):
"""
Appelée par le manager d'événements
Args:
customeventtype : CustomEventManager.MOUSE_CLIC, CustomEventManager.MOUSE_OVER,
CustomEventManager.MOUSE_OUT
event : pygame.MOUSEBUTTONDOWN ou pygame.MOUSEMOTION
"""
# à subclasser
pass
[docs] def handle_key_event(self, customeventtype, event):
"""
Appelée par le manager d'événements
Args:
customeventtype : CustomEventManager.KEY_PRESSED ou CustomEventManager.KEY_RELEASED
event : pygame.KEYDOWN ou pygame.KEYUP
"""
# à subclasser
pass