# -*- coding: iso-8859-1 -*-
from __future__ import division # 1/2 == .5 (par defaut, 1/2 == 0)
from __future__ import with_statement

#    WxGeometrie
#    Dynamic geometry, graph plotter, and more for french mathematic teachers.
#    Copyright (C) 2005-2007  Nicolas Pourcelot
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

# version unicode


from LIB import *
import matplotlib
from moteur_graphique import *

# uncomment the following to use wx rather than wxagg
# matplotlib.use('WX')
# from matplotlib.backends.backend_wx import FigureCanvasWx as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
#from matplotlib.transforms import Value, Bbox, Point as Pt
from matplotlib.backends.backend_agg import RendererAgg
from matplotlib._image import frombyte


class PseudoEvent(object):
    u"Cette classe est destinée à maintenir en vie un évènement périmé."

    _methodes = ("AltDown", "ControlDown", "ShiftDown", "GetPositionTuple",
                "RightIsDown", "RightIsUp", "LeftIsDown", "LeftIsUp", "GetWheelRotation")

    def __init__(self, event):
        self._dict = {}
        for methode in self._methodes:
            if hasattr(event, methode):
                self._dict[methode] = getattr(event, methode)()

    def __getattr__(self, name):
        try:
            return lambda:self._dict[name]
        except KeyError:
            print(u"La méthode " + name + " n'est actuellement pas définie pour les pseudo-évènements.")
            raise

    def Skip(self):
        pass



class GelAffichage(object):
    def __init__(self, canvas, geler = True, actualiser = False):
        self.canvas = canvas
        self.geler = geler
        self.actualiser = actualiser

    def __enter__(self):
        self._ancienne_valeur = self.canvas._affichage_gele
        if self.geler is not None:
            self.canvas._affichage_gele = self.geler

    def __exit__(self, type, value, traceback):
        self.canvas._affichage_gele = self._ancienne_valeur
        if self.actualiser:
            self.canvas.rafraichir_affichage()


# Garde une trace dans les logs de chaque appel de la méthode pour débogage.
@decorator
def track(meth, self, *args, **kw):
    if param.debug:
        s = "%s - Args: %s, %s" % (meth.func_name, args, kw)
        self.parent.action_effectuee(s)
    return meth(self, *args, **kw)

# Garde une trace dans les logs de chaque appel *isolé* de la méthode pour débogage.
# Par exemple, en cas de zoom avec la roulette de souris, seul le dernier zoom
# sera enregistré, pour ne pas saturer le fichier .log
@decorator
def partial_track(meth, self, *args, **kw):
    if param.debug:
        s = "%s - Args: %s, %s" % (f.func_name, args, kw)
        self.parent.action_effectuee(s, signature = f.func_name)
    return meth(self, *args, **kw)


class MiniEditeur:
    def __init__(self, parent):
        self.parent = parent
        self.objet = None


    def init(self, objet, mode = 0):
        u"""Edition d'un nouvel objet.
        mode = 0: édition du nom de l'objet
        mode = 1: édition de l'étiquette de l'objet"""
        self.close() # finalise l'éventuelle édition en cours
        self.texte = ""
        self.objet = objet
        self.old_label = objet.label()
        self.mode = mode #
        self.display()

    def display(self):
        self.objet.label(self.texte + "...")

    def cancel(self):
        self.objet.label(self.old_label)
        self.objet = None


    def ok(self):
        canvas = self.parent
        panel = canvas.parent
        if self.texte:
            try: # si possible, on change le nom de l'objet ; sinon, on change son label.
                nom = self.objet.nom
                if isinstance(self.objet, Texte) or not re.match("""[A-Za-z_][A-Za-z0-9_'"`]*$""", self.texte):
                    self.mode = 1 # label
                if self.mode:
                    self.objet.label(self.texte)
                    panel.action_effectuee(u"%s.label(%s)" %(nom, repr(self.texte)))
                else:
                    self.objet.style(label = self.old_label)
                    self.objet.renommer(self.texte, legende = wxgeo.NOM)
                    panel.action_effectuee(u"%s.renommer(%s, legende = %s)" %(nom, repr(self.texte), wxgeo.NOM))
                self.objet = None
            except RuntimeError: # on reste en mode edition
                if not param.nom_multiple:
                    self.display()
                    raise
                self.objet.label(self.texte) # par défaut, si A est réattribué à un point, il sera traité comme étiquette.
                panel.action_effectuee(u"%s.label(%s)" %(nom, repr(self.texte)))
                canvas.message(u"Attention : ce nom est déjà attribué.")
                self.objet = None
            except:
                self.display()
                raise
        else: # si aucun nom n'est rentre, [ENTREE] est un equivalent de [ECHAP].
            self.cancel()

    def key(self, key):
        if self:
            if key == wx.WXK_BACK:
                self.texte = self.texte[:-1]
                self.display()
            elif key == wx.WXK_ESCAPE:
                self.cancel()
            elif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
                self.ok()
            elif key == 10: # Ctrl + Entree (à tester sous Linux !)
                self.texte += "\n"
                self.display()
            elif key >= 32 and key <= 255 and key <> wx.WXK_DELETE:
                key = unichr(key) # a-z, A-Z, 0-9 ou _
                self.texte += key
                self.display()

    def close(self):
        u"Ferme l'éditeur. Ne renvoie pas d'erreur s'il est déjà fermé."
        if self:
            try:
                self.ok()
            except:
                self.cancel()
                print_error()


    def __nonzero__(self):
        return self.objet is not None
























class Affichage(FigureCanvas):
    def __init__(self, parent, fixe = False):
        u"Si fixe = True, l'utilisateur ne peut pas zoomer ou recadrer la fenêtre d'affichage avec la souris."
        self.parent = parent
        self.couleur_fond = self.parent.param("couleur_fond")
        self.couleur_fond_vide = [n/255. for n in self.parent.parent.GetBackgroundColour().Get()]
        self.figure = Figure(frameon=True, facecolor = self.couleur_fond)
        FigureCanvas.__init__(self, parent, -1, self.figure)
        if param.plateforme == "Linux":
            self.SetSize(wx.Size(10, 10))
        elif param.plateforme == "Windows":
            self.SetWindowStyle(wx.WANTS_CHARS)
            self.Refresh()
        self.axes = self.figure.add_axes([0, 0, 1, 1], frameon=False)
        self.axes.set_xticks([])
        self.axes.set_yticks([])
        #if self.parent.param("transformation_affine") is not None:
        #    self.transformation = matplotlib.transforms.Affine(*self.parent.param("transformation_affine"))
        #    self.figure.set_transform(self.transformation)
        #    self.axes.set_transform(self.transformation)
        #else:
        #    self.transformation = None
        if self.parent.param("transformation") is not None:
            a, b, c, d = self.parent.param("transformation")
            # CODE OBSOLETE (À RÉÉCRIRE) : numerix n'existe plus dans matplotlib 0.98+
            self.transformation = matplotlib.numerix.Matrix([[a, b], [c, d]])
        else:
            self.transformation = None

##        self.commande = self.parent.commande
##        self.historique = self.parent.historique
        self.debut_zoom = None
        # Utilisé pour zoomer avec Ctrl + Clic gauche (contiendra la position initiale)
        self.debut_select = None
        # Utilisé pour sélectionner simultanément plusieurs objets avec Alt + Clic gauche (pos. initiale)
        self.debut_shift = None
        # Utilisé pour translater le contenu de la fenêtre (position initiale)
        self.redetecter = True
        # Rechercher les objets à proximité du pointeur
        self.select = None # objet couramment sélectionné
        self.select_memoire = None
        # Objet devant être prochainement sélectionné (en cas de "litige" entre 2 objets)
        self.etiquette_selectionnee = None # étiquette couramment séléctionnée
        self.fixe = fixe
        self.interaction = None
        # Fonction à lancer au prochain clic de souris (au lieu des actions par défaut)
        self.interaction_deplacement = None
        # Fonction à lancer au prochain déplacement de souris (au lieu des actions par défaut)
        self.editeur = MiniEditeur(self) # edite les noms et etiquettes des points, textes, etc.

        # Paramètres temporaires d'affichage
        self._dessin_temporaire = False

##        self.detecter_objets_caches = False # les objets cachés à proximité sont aussi détectés
        self._affichage_gele = False
        # Ne pas utiliser directement.
        # Si on met la valeur a True, self.rafraichir_affichage() ne fait plus rien.
        # Cela permet d'effectuer un certain nombre d'actions rapidement,
        # et de n'actualiser qu'à la fin.
        # En particulier, cela sert pour charger une figure depuis un fichier .geo

##        self.regenerer_liste = True
##        # utilisé pour savoir si il faut régénérer la liste des objets de la feuille.

        self.liste_objets_en_gras = WeakList()

        self.sel = []
        # Liste ordonnée (pour la détection) des objets de la feuille actuelle.
        # Elle est remise a jour quand self.commande.compteur a changé.
        self.motion_event = None
        self.wheel_event_count = 0
        self.wheel_ctrl_event_count = 0
        self.graph = Moteur_graphique(self)
        self.parametres = [u"taille", u"gradu", u"afficher_axes", u"afficher_quadrillage", u"afficher_fleches", u"repere", u"resolution", u"origine_axes", u"utiliser_repere", u"quadrillages", u"couleur_papier_millimetre", u"liste_axes", u"orthonorme", u"grille_aimantee", u"zoom_texte", "zoom_ligne"]
        self.initialiser()

        self.Bind(wx.EVT_MOUSEWHEEL, self.EventOnWheel)
        self.Bind(wx.EVT_MOTION, self.EventOnMotion)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
        self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_CHAR, self.OnChar)
        #self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
        self._pile_instructions = [] # instructions à exécuter lorsqu'aucune autre action n'est en cours. (Idle)
        self.Bind(wx.EVT_IDLE, self.OnIdle)

        timer=wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self._actualiser_si_necessaire)
        timer.Start(150)
##    def _onSize(self, evt):
##        print "coco est content!"
###        self.rafraichir_affichage()
##        self._width, self._height = self.GetClientSize()
##        self.bitmap = wx.EmptyBitmap(self._width, self._height)
##        # Scale the displayed image (but don't update self.figsize)
##        if not self._isConfigured:
##            self._isConfigured = True
##
##        dpival = self.figure.dpi.get()
##        winch = self._width/dpival
##        hinch = self._height/dpival
##        self.figure.set_size_inches(winch, hinch)
##
##        if self._isRealized:
##            self.draw()
##        evt.Skip()

    def GetTopParent(self):
        fenetre = self
        while fenetre is not None:
            fenetre_precedente = fenetre
            fenetre = fenetre.GetParent()
        return fenetre_precedente


    def initialiser(self):
        # actions a effectuer avant de reexecuter l'historique
        for parametre in self.parametres:
            setattr(self, parametre, self.parent.param(parametre))
        #self.largeur_point = param.largeur_point[0]
##        self.rafraichir_axes = True

    def message(self, txt, lieu = 0):
        self.parent.parent.parent.message(txt, lieu) # cf. geometrie.py


##    def message_temporaire(txt = None): # A FINIR
##        self.message_temp = txt
##        if txt is None:
##            self.message(self.message_def)
##        else:
##            self.message(txt)

    def exporter(self, nom, dpi = None, zone = None):
        u"""Export de la feuille sous forme d'un fichier (png, eps, ...).
        dpi : résolution souhaitée (en dot par inch)
        zone = (xmin, xmax, ymin, ymax) : pour n'exporter qu'une partie de la feuille (png seulement)."""

        nom = str2(nom) # la méthode savefig ne gère pas l'unicode
        dpi = dpi or param.dpi_export

        # Evite d'exporter la feuille avec un nom d'objet en cours d'édition
        self.editeur.close()
        # De même, aucun objet ne doit être en gras
        self.feuille_actuelle.objets_en_gras()
        # Les objets invisibles ne doivent pas apparaitre
##        afficher_objets_caches = self.feuille_actuelle.afficher_objets_caches
##        self.feuille_actuelle.afficher_objets_caches = False
        afficher_objets_caches = self.afficher_objets_caches
        self.afficher_objets_caches = False
##        if self.select is not None:
##            self.select.affiche_en_gras(False)

        self.graph.exporter(nom = nom, dpi = dpi, zone = zone)

##        self.feuille_actuelle.afficher_objets_caches = afficher_objets_caches
        self.afficher_objets_caches = afficher_objets_caches
        self.selection_en_gras()
##        if self.select is not None:
##            self.select.affiche_en_gras(True)


    @property
    def feuille_actuelle(self):
        return self.parent.feuille_actuelle

##################################
# Les fonctions suivantes assurent la conversion pixel <-> coordonnees
##################################
# en multipliant m par coeff(0), on convertit un ecart en abcisses de m pixels en coordonnees.
# en multipliant n par coeff(1), on convertit un ecart en ordonnees de n pixels en coordonnees.
# Les fonctions qui suivent permettent la conversion d'un couple de coordonnees en pixels, et reciproquement.
# Si le mode est fixe a 1, le pixel (0,0) sera le coin inferieur gauche (plus intuitif).
# Si le mode est fixe a -1, le pixel (0,0) sera le coin superieur gauche (convention, respectee par WxPython).

    @property
    def dimensions(self):
        return self.GetSizeTuple()

    def pas(self):
        return (self.fenetre[1] - self.fenetre[0])/(self.resolution)

    ## <DESUET> ##
    def coeff(self, i): # DESUET
#        warning("Desuet. Utiliser dpix2coo (*coeff) et dcoo2pix (/coeff)")
        return (self.fenetre[1+2*i] - self.fenetre[2*i])/self.GetSizeTuple()[i]
        # Rq: une ZeroDivisionError se produit juste après avoir beaucoup réduit une fenêtre,
        # wxpython renvoit parfois (0,0) pour la taille.
        # le plus simple serait de laisser l'erreur, mais ça innonde le débugueur de messages... :-/

    def coeffs(self): # DESUET
#        warning("Desuet. Utiliser dpix2coo et dcoo2pix")
        return self.coeff(0), self.coeff(1)
    ## </DESUET> ##

##    def Xpix2coo(self, x, mode = None): # DESUET
##        warning("Desuet. Utiliser pix2coo")
##        return pylab.array(x)*self.coeff(0) + self.fenetre[0]
##
##    def Ypix2coo(self, y, mode = 1): # DESUET
##        warning("Desuet. Utiliser pix2coo")
##        return mode*pylab.array(y)*self.coeff(1) + (self.fenetre[2] if mode else self.fenetre[3])
##
##    def XYpix2coo(self, couple, mode = 1): # DESUET
##        warning("Desuet. Utiliser pix2coo")
##        u"""conversion d'un couple de pixels en coordonnées.
##
##        Si le mode est fixe a 1, le pixel (0,0) sera le coin inferieur gauche (plus intuitif).
##        Si le mode est fixe a -1, le pixel (0,0) sera le coin superieur gauche (convention, respectee par WxPython)."""
##        return (self.Xpix2coo(couple[0]), self.Ypix2coo(couple[1], mode))
##
##    def Xcoo2pix(self, x): # DESUET
##        warning("Desuet. Utiliser XYcoo2pix")
##        return (pylab.array(x) - self.fenetre[0])/(self.coeff(0))
##
##    def Ycoo2pix(self, y, mode = 1): # DESUET
##        warning("Desuet. Utiliser XYcoo2pix")
##        return mode*(pylab.array(y) - (self.fenetre[2] if mode else self.fenetre[3]))/self.coeff(1)
##
##    def XYcoo2pix(self, couple, mode = 1): # DESUET
##        warning("Desuet. Utiliser coo2pix")
##        u"""Conversion d'un couple de coordonnées en pixels.
##
##        Si le mode est fixe a 1, le pixel (0,0) sera le coin inferieur gauche (plus intuitif).
##        Si le mode est fixe a -1, le pixel (0,0) sera le coin superieur gauche (convention, respectee par WxPython)."""
##        return (self.Xcoo2pix(couple[0]), self.Ycoo2pix(couple[1], mode))

    def coo2pix(self, x, y):
        u"""Convertit des coordonnées en pixel."""
        if isinstance(x, (list, tuple)):
            x = numpy.array(x)
        if isinstance(y, (list, tuple)):
            y = numpy.array(y)
        l, h = self.dimensions
        px = l*(x - self.fenetre[0])/(self.fenetre[1] - self.fenetre[0])
        py = h*(self.fenetre[3] - y)/(self.fenetre[3] - self.fenetre[2])
        return px, py

    def pix2coo(self, px, py):
        u"""Convertit un pixel en coordonnées."""
        if isinstance(px, (list, tuple)):
            px = numpy.array(px)
        if isinstance(py, (list, tuple)):
            py = numpy.array(py)
        l, h = self.dimensions
        x = px*(self.fenetre[1] - self.fenetre[0])/l + self.fenetre[0]
        y = py*(self.fenetre[2] - self.fenetre[3])/h + self.fenetre[3]
#        print x,  y,  -x,  -y
        return x, y

    def dcoo2pix(self, dx, dy):
        u"""Convertit un déplacement exprimé en coordonnées en un déplacement en pixels."""
        l, h = self.dimensions
        dpx = l*dx/(self.fenetre[1] - self.fenetre[0])
        dpy = h*dy/(self.fenetre[2] - self.fenetre[3])
        return dpx, dpy

    def dpix2coo(self, dpx, dpy):
        u"""Convertit un déplacement exprimé en pixels en un déplacement exprimé en coordonnées."""
        l, h = self.dimensions
        dx = dpx*(self.fenetre[1] - self.fenetre[0])/l
        dy = dpy*(self.fenetre[3] - self.fenetre[2])/h
        return dx, dy


#    Gestion de l'affichage
#################################


##    def repaint(self, event = None): # Utilise pour l'evt RESIZE
##        if event: event.Skip() # en particulier, il faut que self.draw() soit appliqué
####        if self.orthonorme:
####            self.orthonormer(rafraichir = False)
##        self.rafraichir_axes = True
##        self.refresh()

##    def ecran_blanc(self): # affiche un ecran blanc essentiellement quand il n'y a pas de feuille
##        self.clear(axes = False)
##        self.draw()
##        #self.ClearBackground()

##    def affiche(self):
##        if self.feuille_actuelle is None:
##            self.ecran_blanc()
##        else:
##            self.feuille_actuelle.affiche()
##            self.rafraichir_affichage()

##    def refresh(self):
##        u"""Rafraichir la fenêtre, et redessiner les objets sensibles au réglage de la fenêtre.
##
##        Désuet."""
##        if self.feuille_actuelle:
##            self.feuille_actuelle.refresh(tous_les_objets = False)
##        warning("Utilisation a eviter desormais.")

##    def clear(self):
##        self.graph.fast_clear()
##
##    effacer = clear

##    def pixel_plot(self, px, py, couleur = "k", **kw):
##        x, y = self.pix2coo(px, py)
##        return self.dessiner_ligne(x, y, couleur = couleur, **kw)


##    # À déplacer avec les autres évènements une fois la refactorisation du code terminée
##    def actualiser(self):
##        u"Tout redessiner (à utiliser lors d'un changement de la fenêtre)."
####        self.parent._affiche()
##        self.rafraichir_affichage(rafraichir_axes = True)



##    @full_trace


    def rafraichir_affichage(self, dessin_temporaire = False, rafraichir_axes = None):
        if rafraichir_axes is not None:
            self.feuille_actuelle._repere_modifie = rafraichir_axes
        self.feuille_actuelle.affichage_perime()
        self._dessin_temporaire = dessin_temporaire

    def _actualiser_si_necessaire(self, event = None, _n=[0]):
#        _n[0] += 1
        if self.feuille_actuelle._affichage_a_actualiser:
#            print _n[0], u"Affichage actualisé."
            self._actualiser()

    def _actualiser(self, _n = [0]):
        # Le code suivant est à activer uniquement pour le débogage de l'affichage:
        # <DEBUG>
        if param.debug:
            s = "Actualisation"
            if self.feuille_actuelle._repere_modifie:
                s += " complete"
            print s + str(_n) + ": " + self.parent.__titre__
            _n[0] += 1
        # </DEBUG>
        self.graph.dessiner(dessin_temporaire = self._dessin_temporaire,
                        rafraichir_axes = self.feuille_actuelle._repere_modifie)
        self.feuille_actuelle._repere_modifie = False
        self.feuille_actuelle._affichage_a_actualiser = False
        self._dessin_temporaire = False

            #wx.YieldIfNeeded()


#   Alias
######################

    def dessiner_ligne(self, *args, **kw):
        return self.graph.ajouter_ligne(*args, **kw)

    def dessiner_polygone(self, *args, **kw):
        return self.graph.ajouter_polygone(*args, **kw)

    def dessiner_texte(self, *args, **kw):
        return self.graph.ajouter_texte(*args, **kw)

    def dessiner_arc(self, *args, **kw):
        return self.graph.ajouter_arc(*args, **kw)

    def dessiner_point(self, *args, **kw):
        return self.graph.ajouter_point(*args, **kw)


    def ligne(self, *args, **kw):
        return self.graph.ligne(*args, **kw)

    def polygone(self,  *args, **kw):
        return self.graph.polygone(*args, **kw)

    def texte(self, *args, **kw):
        return self.graph.texte(*args, **kw)

    def arc(self, *args, **kw):
        return self.graph.arc(*args, **kw)

    def point(self, *args, **kw):
        return self.graph.point(*args, **kw)

    def dessiner(self, objet):
        self.graph.ajouter(objet)






#    Reglage des parametres d'affichage
##########################################
# Gestion des variables d'environnement liées à l'affichage.
# A standardiser.


##    _liste_parametres_repere = ("quadrillages", "affiche_quadrillage", "affiche_axes",
##                                    "affiche_fleches", "repere", "gradu", "utiliser_repere",
##                                    "liste_axes", "orthonorme", "fenetre",
##                                    )



    def geler_affichage(self, geler = True, actualiser = False):
        u"""À utiliser au sein d'un contexte 'with':
        with self.geler_affichage():
            ...
        """
        return GelAffichage(self, geler = geler, actualiser = actualiser)

    @property
    def affichage_gele(self):
        return self._affichage_gele

    def saturation(self, i):
        return self.zoom_ligne*self.coeff(i)/self.gradu[i]

    # Parametres booléens gérés par une entrée du menu
    for _nom_, _doc_ in (
    ('afficher_axes', u"Afficher ou non les axes."),
    ('afficher_quadrillage', u"Afficher ou non le(s) quadrillage(s)."),
    ('orthonorme', u"Afficher la figure dans un repère toujours orthonormé."),
    ('afficher_objets_caches', u"Indique si les objets cachés sont affichés ou non."),
    ('grille_aimantee', u"Indique si les points doivent se placer sur le quadrillage."),
    ):
        exec_('''@track
def gerer_parametre_%(_nom_)s(self, afficher = None):
    """%(_doc_)s"""
    if afficher is not None:
        if isinstance(afficher, bool):
            self.%(_nom_)s = afficher
        else:
            self.%(_nom_)s = not self.%(_nom_)s
        self.rafraichir_affichage()
    assert isinstance(self.%(_nom_)s, bool), '%(_nom_)s: ' + repr(self.%(_nom_)s)
    return self.%(_nom_)s''' %locals(), globals(), locals())

    # Paramètres gérés directement par la feuille
    for _nom_ in Feuille._parametres_repere:
        exec_('''assert "%(_nom_)s" not in locals(), "Erreur: %(_nom_)s est deja defini !"
@property2
def %(_nom_)s(self, valeur = 'X7#H4>u87-Kt/y;tr7"'):
    if valeur == 'X7#H4>u87-Kt/y;tr7"':
        return self.feuille_actuelle.%(_nom_)s
    self.feuille_actuelle.%(_nom_)s = valeur''' %locals(), globals(), locals())

    del _nom_, _doc_









#      Gestion du zoom, etc...
########################################




    def _get_fenetre(self):
        if self.orthonorme:
            w, h = self.GetSizeTuple()
            fenetre = self.feuille_actuelle.fenetre
            coeff0 = (fenetre[1] - fenetre[0])/w
            coeff1 = (fenetre[3] - fenetre[2])/h
            xmin, xmax, ymin, ymax = fenetre
            xcoeff = (coeff1/coeff0 if coeff0 < coeff1 else 1)
            ycoeff = (1 if coeff0 < coeff1 else coeff0/coeff1)
            x, y, rx, ry = (xmin+xmax)/2., (ymin+ymax)/2., (xmax-xmin)/2., (ymax-ymin)/2.
            return x - xcoeff*rx, x + xcoeff*rx, y - ycoeff*ry, y + ycoeff*ry
        return self.feuille_actuelle.fenetre
##        if hasattr(self, 'ratio'): # À ÉCRIRE
##            if self.ratio is not None: # ratio est le
##                xcoeff = (self.coeff(1)/(self.ratio*self.coeff(0)) if self.ratio*self.coeff(0) < self.coeff(1) else 1)
##                ycoeff = (1 if (self.ratio*self.coeff(0)) < self.coeff(1) else (self.ratio*self.coeff(0))/self.coeff(1))
##                xmin, xmax, ymin, ymax = self.fenetre
##                x, y, rx, ry = (xmin+xmax)/2., (ymin+ymax)/2., (xmax-xmin)/2., (ymax-ymin)/2.
##                self._changer_fenetre(x - xcoeff*rx, x + xcoeff*rx, y - ycoeff*ry, y + ycoeff*ry)




    def _set_fenetre(self, xmin_xmax_ymin_ymax):
        self.feuille_actuelle.fenetre = xmin_xmax_ymin_ymax
##        self.rafraichir_affichage()

##    def _changer_fenetre(self, xmin, xmax, ymin, ymax):
##        u"Commande de bas niveau pour modifier la fenêtre sans rafraichir l'affichage."
##        self.feuille_actuelle._fenetre = xmin, xmax, ymin, ymax
##        self.rafraichir_axes = True

    fenetre = property(_get_fenetre, _set_fenetre)

    def synchroniser_fenetre(self):
        u"""Détecte la fenêtre d'affichage et l'enregistre.
        Ce peut être utile si l'on utilise une commande de haut niveau de matplolib,
        qui calcule automatiquement la meilleure fenêtre d'affichage."""
        if param.old_matplotlib:
            self.fenetre = self.axes.viewLim.intervalx().get_bounds() + self.axes.viewLim.intervaly().get_bounds()
        else:
            xmin, xmax = self.axes.viewLim.intervalx
            ymin, ymax = self.axes.viewLim.intervaly
            self.fenetre = xmin, xmax, ymin, ymax

    def zoomer(self, coeff):
        xmin, xmax, ymin, ymax = self.fenetre
        x, y, rx, ry = (xmin+xmax)/2., (ymin+ymax)/2., (xmax-xmin)/2., (ymax-ymin)/2.
        self.fenetre = x - rx/coeff, x + rx/coeff, y - ry/coeff, y + ry/coeff

    def zoom_in(self, event = None):
        self.zoomer(param.zoom_in)

    def zoom_out(self, event = None):
        self.zoomer(param.zoom_out)


    @track
    def zoom_auto(self, event = None):
##        a = self.fenetre
        print "okokokok3"
##        for i in xrange(5):
##
        for i in xrange(2):
            #self.graph._actualiser()
            time.sleep(1)
            self._zoom_auto()

##        compteur = 0
##        condition = True
##        while condition:
##            compteur += 1
##            self._zoom_auto()
            # à cause du texte dont la taille est indépendante de l'échelle, les calculs sont faussés.
            # Mais en réitérant le procédé, l'affichage converge rapidement (A OPTIMISER ?).
##            b = a
##            a = self.fenetre
##            erreur_x = abs(a[0] - b[0]) + abs(a[1] - b[1])
##            erreur_y = abs(a[2] - b[2]) + abs(a[3] - b[3])
            #dx = a[1] - a[0] # xmax - xmin
            #dy = a[3] - a[2] # ymax - ymin
            # l'erreur doit être inférieure à 1 pixel:
##            condition = erreur_x/self.coeff(0) > 1 or erreur_y/self.coeff(1) > 1
##            if compteur > 25:
##                print u"Échec du zoom automatique."
##                break



    @track
    def zoom_auto(self, event = None):
        fenetre_initiale = a = self.fenetre
        compteur = 0
        condition = True
        while condition:
            self.graph._regler_fenetre()
#            self._actualiser()
            compteur += 1
            self._zoom_auto()
            # à cause du texte dont la taille est indépendante de l'échelle, les calculs sont faussés.
            # Mais en réitérant le procédé, l'affichage converge rapidement (A OPTIMISER ?).
            b = a
            a = self.fenetre
            erreur_x = abs(a[0] - b[0]) + abs(a[1] - b[1])
            erreur_y = abs(a[2] - b[2]) + abs(a[3] - b[3])
            #dx = a[1] - a[0] # xmax - xmin
            #dy = a[3] - a[2] # ymax - ymin
            # l'erreur doit être inférieure à 1 pixel:
            condition = erreur_x/self.coeff(0) > 1 or erreur_y/self.coeff(1) > 1
            if compteur > 25:
                self.message(u"Échec du zoom automatique.")
                self.fenetre = fenetre_initiale
#                self.graph._regler_fenetre()
                break

    def _zoom_auto(self):
        actuelle = self.feuille_actuelle
        xxyy = zip(*([obj.espace_vital for obj in actuelle.liste_objets(False)
                      if obj.espace_vital is not None]
                      + [obj.etiquette.espace_vital for obj in actuelle.liste_objets(False)
                         if obj.etiquette is not None and obj.etiquette.espace_vital is not None]))
        print 'xxyy', xxyy
        if xxyy:
            # 'None' indique que l'objet ne fournit pas d'indication de dimension
            # pour les abscisses ou pour les ordonnées.
            _ajuster_xmin = _ajuster_xmax = True
            xmins = [x for x in xxyy[0] if x is not None]
            if xmins:
                xmin = min(xmins)
            else:
                xmin = self.fenetre[0]
                _ajuster_xmin = False
            xmaxs = [x for x in xxyy[1] if x is not None]
            if xmaxs:
                xmax = max(xmaxs)
            else:
                xmax = self.fenetre[1]
                _ajuster_xmax = False
            ymin = min(y for y in xxyy[2] if y is not None)
            ymax = max(y for y in xxyy[3] if y is not None)
            dx = xmax - xmin
            dy = ymax - ymin
            # des valeurs trop proches pour xmin et xmax risquent de faire planter l'affichage.
            if dx < 100*param.tolerance:
                r = (self.fenetre[1] - self.fenetre[0])/2
                xmin -= r
                xmax += r
            else:
                if _ajuster_xmin:
                    xmin -= .05*dx
                if _ajuster_xmax:
                    xmax += .05*dx

            if dy < 100*param.tolerance: # idem pour ymin et ymax
                r = abs(self.fenetre[3] - self.fenetre[2])/2
                ymin -= r
                ymax += r
            else:
                ymin -= .05*dy
                ymax += .05*dy

            self.fenetre = xmin, xmax, ymin, ymax
            if param.debug:
                print "ZOOM AUTO :", xmin, xmax, ymin, ymax,  xxyy


    @track
    def orthonormer(self, event = None, mode = 1):
        u"""
        mode 0 : on orthonormalise le repère en restreignant la vue.
        mode 1 : on orthonormalise le repère en élargissant la vue."""
        if mode:
            xcoeff = self.coeff(1)/self.coeff(0) if self.coeff(0) < self.coeff(1) else 1
            ycoeff = 1 if self.coeff(0) < self.coeff(1) else self.coeff(0)/self.coeff(1)
        else:
            xcoeff = self.coeff(1)/self.coeff(0) if self.coeff(0) > self.coeff(1) else 1
            ycoeff = 1 if self.coeff(0) > self.coeff(1) else self.coeff(0)/self.coeff(1)
        xmin, xmax, ymin, ymax = self.fenetre
        x, y, rx, ry = (xmin+xmax)/2., (ymin+ymax)/2., (xmax-xmin)/2., (ymax-ymin)/2.
        self.fenetre = x - xcoeff*rx, x + xcoeff*rx, y - ycoeff*ry, y + ycoeff*ry

    # < Zooms concernant uniquement la taille des objets >

    def zoom_text(self, event = None, valeur = 100):
        self.zoom_texte = valeur/100
##        self.rafraichir_affichage()

    def zoom_line(self, event = None, valeur = 100):
        self.zoom_ligne = valeur/100
##        self.rafraichir_affichage()

    def zoom_normal(self, event = None):
        self.zoom_texte = 1
        self.zoom_ligne = 1
##        self.rafraichir_affichage()

    def zoom_large(self, event = None):
        self.zoom_texte = 1.2
        self.zoom_ligne = 1.4
##        self.rafraichir_affichage()

    def zoom_videoprojecteur(self, event = None):
        self.zoom_texte = 1.6
        self.zoom_ligne = 2
##        self.rafraichir_affichage()

    def zoom_videoprojecteur_large(self, event = None):
        self.zoom_texte = 2.2
        self.zoom_ligne = 3
##        self.rafraichir_affichage()

    # </ Fin des zooms sur la taille des objets >


    @track
    def repere_Oij(self, event = None):
        self.repere = ('O', 'i', 'j')

    @track
    def repere_OIJ(self, event = None):
        self.repere = ('O', 'I', 'J')

    @track
    def repere_011(self, event = None):
        ux, uy = self.gradu
        self.repere = ('0', str(ux), str(uy))

##    @track
##    def reperage(self, origine, x, y):
##        self.repere = (origine, x, y)
##        self.parent.action_effectuee('changement_repere')
##        self.rafraichir_affichage()

##    @track
##    def graduations(self, xgradu, ygradu):
##        self.gradu = xgradu, ygradu
##        self.rafraichir_affichage()

##    @track
##    def reperage_et_graduations(self, origine, x, y, xgradu, ygradu):
##        self.repere = (origine, x, y)
##        self.gradu = xgradu, ygradu
##        self.parent.action_effectuee('changement_repere_graduations')
##        self.rafraichir_affichage()

##    @track
##    def modifier_quadrillage(self, *quadrillages):
##        self.quadrillages = quadrillages
##        self.parent.action_effectuee('changement_quadrillage')
##        self.rafraichir_affichage()

    @track
    def quadrillage_millimetre(self, event = None):
        self.quadrillages = (  ((1, 1), ':', 1, 'k'),
                            ((0.5, 0.5), '-', 0.25, 'k'),
                            ((0.1, 0.1), '-', 0.1, 'k'),
                            )

    @track
    def quadrillage_millimetre_colore(self, event = None, couleur = None):
        if couleur is None:
            couleur = self.couleur_papier_millimetre
        self.quadrillages = (  ((1, 1), ':', 1, couleur),
                                    ((0.5, 0.5), '-', 0.25, couleur),
                                    ((0.1, 0.1), '-', 0.1, couleur),
                                    )

    @track
    def quadrillage_demigraduation(self, event = None):
        ux, uy = self.gradu
        self.quadrillages = (  ((ux, uy), ':', 1, 'k'),
                                    ((ux/2, uy/2), '-', 0.25, 'k'),
                                    )

    @track
    def quadrillage_demigraduation_colore(self, event = None, couleur = None):
        ux, uy = self.gradu
        if couleur is None:
            couleur = self.couleur_papier_millimetre
        self.quadrillages = (  ((ux, uy), ':', 1, couleur),
                                    ((ux/2, uy/2), '-', 0.25, couleur),
                                    )

    def quadrillage_defaut(self, event = None):
        self.quadrillages = self.parent.param("quadrillages", defaut = True)


# Evenements concernant directement la feuille
################################

    def coder(self, event):
        self.executer(u"coder()")

    def decoder(self, event):
        self.executer(u"effacer_codage()")

##    def codage(self, booleen):
##        actuelle = self.feuille_actuelle
##        if actuelle is not None:
##            if booleen:
##                actuelle.coder()
##            else:
##                actuelle.effacer_codage()

    def effacer_traces(self, event):
        self.feuille_actuelle.effacer_traces()

##    def relier(self, point, x = True, y = True):
##        if x:
##            self.feuille_actuelle.relier_axe_x(point)
##        if y:
##            self.feuille_actuelle.relier_axe_y(point)


    def executer(self, commande, parser = False):
        u"""Exécute une commande dans la feuille.

        NB: le parser n'est *PAS* activé par défaut, par souci de rapidité."""
        self.feuille_actuelle.executer(commande, parser = parser)


#    Gestion des evenements (essentiellement la souris).
###############################





    def coordonnees(self, event, dx = 0, dy = 0):
        u"""Renvoie les coordonnées correspondant à l'évènement, converties en coordonnées de la feuille.
        Si [Maj] est enfoncée, les coordonnées sont arrondies à la graduation la plus proche.
        dx et dy correspondent au décalage entre les coordonnées de l'objet, et le point où on l'a saisit.
        (Par exemple, un texte n'est pas forcément saisi au niveau de son point d'ancrage).
        """
        x, y = self.pix2coo(*event.GetPositionTuple())
        if event.ShiftDown() or self.grille_aimantee:
            a, b = self.gradu
            return a*round((x + dx)/a), b*round((y + dy)/b)
        else:
            return float(x + dx), float(y + dy)



    def interagir(self, fonction, aide = None, fonction_bis = None):
        u"""Permet l'interaction du canevas avec un module externe.

        A chaque clic de souris, la fonction indiquée est appelée, avec
        un certains nombre de paramètres comme arguments :
        objet(s) à proximité, position en coordonnées et en pixels...

        Une aide est éventuellement affichée dans la barre d'état de la fenêtre principale.

        fonction_bis est éventuellement utilisée lors de chaque déplacement de la souris.
        Elle reçoit en argument la position en pixels uniquement (pour ne pas alourdir le traitement).

        Exemple de récupération des arguments :
        si un module appelle self.canvas.interagir(ma_fonction, u"Cliquez n'importe où.", ma_2eme_fonction),
        le module doit définir :

        def ma_fonction(self, **kw):
            pixel = kw["pixel"]
            position = kw["position"]
            objet_selectionne = kw["selection"]
            autres_objets_a_proximite = kw["autres"]
            print u"clic!"

        def ma_2eme_fonction(self, **kw):
            pixel = kw["pixel"]
            print u"ça bouge !"

        """

        self.interaction = fonction
        self.interaction_deplacement = fonction_bis
        if fonction:
            self.SetCursor(wx.StockCursor(wx.CURSOR_QUESTION_ARROW))
        else:
            self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
        if aide is not None:
            self.message(aide)


    def signal(self, event = None):
        if self.interaction:
            pixel = event.GetPositionTuple()
            self.detecter(pixel)
            objet = self.interaction(selection = self.select, autres = self.selections, position = self.coordonnees(event), pixel = pixel)
            self.detecter(pixel)


    def deplacable(self, obj): # indique si un objet est deplacable
        return isinstance(obj, Objet) and obj._deplacable

    def pointable(self, obj): # indique si un objet possede des coordonnees
        return isinstance(obj, Objet) and obj._pointable


    def infos(self):
        self.message(self.select.info if self.select is not None else '')

    def selection_en_gras(self):
        self.feuille_actuelle.objets_en_gras(self.select, *self.liste_objets_en_gras)

    def detecter(self, position = None):
        u"""Détecte les objets à proximité de la position indiquée.
        Typiquement, on utilise self.detecter(event.GetPositionTuple())."""

        self.redetecter = False
        self.debut_zoom = None
        actuelle = self.feuille_actuelle # feuille courante

        if not self.affichage_gele:
            if position is None:
                position = self.ScreenToClient(wx.GetMousePosition())
            if param.afficher_coordonnees: # affichage des coordonnees dans la barre d'etat
                self.message(str(self.pix2coo(*position)), 1)
            x, y = position
            # on place les objets 'modifiables' en premier (points libres, glisseurs, textes)
            self.sel = actuelle.liste_objets(tri = True)
            # liste des objets pres du pointeur de la souris :
            self.selections = []
            for obj in self.sel:
                try:
                    if obj.distance_inf(x, y, param.precision_selection):
                        self.selections.append(obj)
                except:
                    print_error()
                    self.message(u"Erreur: les coordonnées de %s sont incalculables." %obj.nom)
            proximite = len(self.selections)
            if proximite:
                self.select = self.selections[0]
                if self.select_memoire and self.select_memoire in self.selections:
                    self.select = self.select_memoire
                if self.interaction:
                    self.SetCursor(wx.StockCursor(wx.CURSOR_QUESTION_ARROW))
                else:
                    if self.deplacable(self.select):
                        self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
                    else:
                        self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
                self.infos()
            else:
                if self.interaction:
                    self.SetCursor(wx.StockCursor(wx.CURSOR_QUESTION_ARROW))
                else:
                    self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
                self.message("")
                self.select = None
            self.selection_en_gras()


    def OnIdle(self, event):
        u"Instructions à exécuter si aucun processus n'est en cours."
        if self.motion_event is not None:
            try:
                self.OnMotion(self.motion_event)
            finally:
                self.motion_event = None
        elif self.wheel_event_count != 0:
            try:
                self.OnWheel(self.wheel_event_count)
            finally:
                self.wheel_event_count = 0
        elif self.wheel_ctrl_event_count != 0:
            try:
                self.OnCtrlWheel(self.wheel_ctrl_event_count)
            finally:
                self.wheel_ctrl_event_count = 0
        elif self._pile_instructions:
            try:
                fonction, args, kwargs = self._pile_instructions[-1]
                fonction(*args, **kwargs)
            finally:
                self._pile_instructions.pop()
        self._actualiser_si_necessaire()


    def execute_on_idle(self, fonction, *args, **kwargs):
        u"Exécuter une fois qu'aucun processus n'est actif."
        self._pile_instructions.append((fonction, args, kwargs))

    def EventOnWheel(self, event):
        if event.ControlDown():
            if event.GetWheelRotation() > 0:
                self.wheel_ctrl_event_count += 1
            else:
                self.wheel_ctrl_event_count -= 1
        else:
            if event.GetWheelRotation() > 0:
                self.wheel_event_count += 1
            else:
                self.wheel_event_count -= 1


    def OnWheel(self, wheel_event_count):
        u"Gestion du zoom par la roulette de la souris."
        if self.fixe: return
        if wheel_event_count > 0:
            self.zoomer(param.zoom_in**wheel_event_count)
            self.parent.action_effectuee("zoom_in", signature = 'zoom_in')
#            self.zoom_in()
        else:
#            self.zoom_out()
            self.zoomer(param.zoom_out**(-wheel_event_count))
            self.parent.action_effectuee("zoom_out", signature = 'zoom_out')


    def OnCtrlWheel(self, wheel_ctrl_event_count):
        u"Gestion du zoom du texte par la roulette de la souris."
        if wheel_ctrl_event_count > 0:
            self.zoom_texte *= param.zoom_texte_in**wheel_ctrl_event_count
            self.zoom_ligne *= param.zoom_ligne_in**wheel_ctrl_event_count
        else:
            self.zoom_texte *= param.zoom_texte_out**(-wheel_ctrl_event_count)
            self.zoom_ligne *= param.zoom_ligne_out**(-wheel_ctrl_event_count)
##        self.rafraichir_affichage()


    def EventOnMotion(self, event):
        self.motion_event = PseudoEvent(event)
        if event.LeftIsDown() and not self.HasCapture():
            self.CaptureMouse()

##    def EventOnMotion(self, event):
##        self.motion_event = PseudoEvent(event)
##        if event.LeftIsDown() or self.feuille_actuelle.objet_temporaire:
##            if not self.HasCapture():
##                self.CaptureMouse()
##        elif self.HasCapture():
##            self.ReleaseMouse()

    def OnMotion(self, event):
        #if self.FindFocus() in (self.parent, self.parent.parent, self.parent.parent.parent):
        if self.GetTopParent().IsEnabled() and (event.LeftIsDown() or event.RightIsDown()):
            self.SetFocus()
        if self.redetecter:
            self.detecter()
        actuelle = self.feuille_actuelle

        if actuelle.objet_temporaire(): # Utilisé pour la prévisualisation d'objets, avant leur construction avec la souris
            actuelle.point_temporaire().coordonnees = self.coordonnees(event)

        if event.LeftIsDown():# or self.interaction:
            self.editeur.close()
            if event.ControlDown(): # selection d'un zone pour zoomer
                if event.AltDown(): # selection simultanée de tous les objets d'une zone
                    self.selection_zone(event.GetPositionTuple())

                elif not self.fixe:
                    self.gestion_zoombox(event.GetPositionTuple())

            elif event.AltDown(): # deplacement de l'etiquette d'un objet
                self.debut_zoom = None
                x, y = event.GetPositionTuple()
                if self.etiquette_selectionnee is None:
                    for objet in actuelle.liste_objets(False):
                        if objet.etiquette is not None:
                            try:
                                if objet.etiquette.distance_inf(x, y, param.precision_selection):
                                    self.etiquette_selectionnee = objet.etiquette
                                    x, y = self.etiquette_selectionnee.coordonnees
                                    x1, y1 = self.coordonnees(event)
                                    self.decalage_coordonnees = x - x1, y - y1
                                    break
                            except:
                                print_error()
                                self.message(u"Erreur: impossible de trouver l'étiquette de %s.." %objet.nom)
                if self.etiquette_selectionnee:
                    self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
                    self.etiquette_selectionnee(*self.coordonnees(event, *self.decalage_coordonnees))


            elif self.select is not None and not self.interaction: # deplacement d'un objet avec la souris
                self.debut_zoom = None
                if self.deplacable(self.select):
                    self.select(*self.coordonnees(event, *self.decalage_coordonnees))
                self.infos()

        elif event.RightIsDown() and self.debut_shift and not self.fixe: # deplacement de la feuille
            self.SetCursor(wx.StockCursor(wx.CURSOR_SIZING))
            self.fin_shift = self.pix2coo(*event.GetPositionTuple())
            translation = pylab.array(self.fin_shift) - pylab.array(self.debut_shift)
            self.fenetre = self.fenetre[0] - translation[0], self.fenetre[1] - translation[0], self.fenetre[2] - translation[1], self.fenetre[3] - translation[1]
            if self.select is not None:
                self.select = None
                self.selection_en_gras()

        elif self.interaction_deplacement is not None:
            self.interaction_deplacement(pixel = event.GetPositionTuple())

        elif not event.ControlDown():   # detection des objets a proximite du pointeur
            self.detecter(event.GetPositionTuple())



    def gestion_zoombox(self, pixel):
        x, y = pixel
        xmax, ymax = self.GetSize()
        x = max(min(x, xmax), 0)
        y = max(min(y, ymax), 0)
        self.fin_zoom = self.pix2coo(x, y)
        self.debut_zoom = self.debut_zoom or self.fin_zoom
        (x0, y0), (x1, y1) = self.debut_zoom, self.fin_zoom
        if self.orthonorme:
            if ymax*abs(x0 - x1) > xmax*abs(y0 - y1):
                y1 = y0 + ymax/xmax*abs(x0 - x1)*cmp(y1, y0)
            else:
                x1 = x0 + xmax/ymax*abs(y0 - y1)*cmp(x1, x0)
            self.fin_zoom = (x1, y1)
            #if param.bouger_curseur:  # ou comment rendre fou l'utilisateur... ;)
            #    self.WarpPointer(*self.XYcoo2pix((x1, y1), -1))
        self.dessiner_polygone([x0,x0,x1,x1], [y0,y1,y1,y0], facecolor='c', edgecolor='c', alpha = .1)
        self.dessiner_ligne([x0,x0,x1,x1,x0], [y0,y1,y1,y0,y0], 'c', alpha = 1)

        self.rafraichir_affichage(dessin_temporaire = True) # pour ne pas tout rafraichir


    def selection_zone(self, pixel):
        x, y = pixel
        xmax, ymax = self.GetSize()
        x = max(min(x, xmax), 0)
        y = max(min(y, ymax), 0)
        self.fin_select = self.pix2coo(x, y)
        self.debut_select = self.debut_select or self.fin_select
        (x0, y0), (x1, y1) = self.debut_select, self.fin_select
        self.dessiner_polygone([x0,x0,x1,x1], [y0,y1,y1,y0], facecolor='y', edgecolor='y',alpha = .1)
        self.dessiner_ligne([x0,x0,x1,x1,x0], [y0,y1,y1,y0,y0], 'g', linestyle = ":", alpha = 1)

        self.rafraichir_affichage(dessin_temporaire = True) # pour ne pas tout rafraichir







    def OnSelect(self, x0, x1, y0, y1):
        x0, x1 = min(x0, x1), max(x0, x1)
        y0, y1 = min(y0, y1), max(y0, y1)
        objets_dans_la_zone = []
        for objet in self.feuille_actuelle.liste_objets():
            espace = objet.espace_vital
            if espace is not None:
                xmin, xmax, ymin, ymax = espace
                if x0 <= xmin <= xmax <= x1 and y0 <= ymin <= ymax <= y1:
                    objets_dans_la_zone.append(objet)
        self.feuille_actuelle.objets_en_gras(*objets_dans_la_zone)

        def exporte():
            actuelle = self.feuille_actuelle # feuille de travail courante
            if actuelle.sauvegarde["export"]:
                dir, fichier = os.path.split(actuelle.sauvegarde["export"]) # on exporte sous le même nom qu'avant par défaut
            elif actuelle.sauvegarde["nom"]:
                fichier = actuelle.sauvegarde["nom"] # le nom par defaut est le nom de sauvegarde
                dir = actuelle.sauvegarde["repertoire"]
            else:
                fichier = ""
                if param.rep_export is None:
                    dir = param.repertoire
                else:
                    dir = param.rep_export

            dlg = wx.FileDialog(
                self, message = u"Exporter la sélection en png", defaultDir=dir,
                defaultFile=fichier, wildcard = u"PNG - Portable Network Graphic (.png)|*.png", style=wx.SAVE | wx.OVERWRITE_PROMPT | wx.CHANGE_DIR
                )

            if dlg.ShowModal() == wx.ID_OK:
                param.rep_export = os.getcwd()
                if param.rep_open is None:
                    param.rep_open = param.rep_save
                if param.rep_save is None:
                    param.rep_save = param.rep_export
                os.chdir(param.repertoire)
##                self.geler_affichage = True
                self.selection_en_gras()
##                for objet in objets_dans_la_zone:
##                    objet.affiche_en_gras(False)
##                self.geler_affichage = False
##                self.parent.affiche()
                path = dlg.GetPath()
                self.exporter(nom = path, zone = (x0, x1, y0, y1))
                actuelle.sauvegarde["export"] = path

        if objets_dans_la_zone:
            dlg = wx.SingleChoiceDialog(self, u"Appliquer à la sélection :", u"Sélection",
                        [u"Supprimer les objets", u"Masquer les objets", u"Editer les objets", u"Exporter la zone comme image"],
                        wx.CHOICEDLG_STYLE)

            if dlg.ShowModal() == wx.ID_OK:
                choix = dlg.GetSelection()
                if choix == 0:
                    with self.geler_affichage(actualiser = True):
                        for objet in objets_dans_la_zone:
                            self.executer(u"del %s" %objet.nom)
                elif choix == 1:
                    with self.geler_affichage(actualiser = True):
                        for objet in objets_dans_la_zone:
                            self.executer(u"%s.cacher()" %objet.nom)
                elif choix == 2:
                    from GUI.proprietes_objets import Proprietes
                    win = Proprietes(self, objets_dans_la_zone)
                    win.Show(True)
                elif choix == 3:
                    exporte()
                    dlg.Destroy()

            dlg.Destroy()

            with self.geler_affichage():
##            for objet in objets_dans_la_zone:
##                objet.affiche_en_gras(False)
                self.selection_en_gras()
        else:
            exporte()
        self.rafraichir_affichage()


    def OnLeftDown(self, event):
        # Patch pour l'utilisation avec un dispositif de pointage absolu (tablette graphique ou TNI)
        self.detecter(event.GetPositionTuple())
        if self.HasCapture():
            self.ReleaseMouse()
        self.SetFocus()
        if self.deplacable(self.select):
            x, y = self.select.coordonnees
            x1, y1 = self.coordonnees(event)
            self.decalage_coordonnees = x - x1, y - y1


    def OnLeftUp(self, event):
        if self.HasCapture():
            self.ReleaseMouse()

        if self.etiquette_selectionnee:
            x, y = self.etiquette_selectionnee.coordonnees
            self.parent.action_effectuee(u"%s.etiquette(%s, %s)" %(self.etiquette_selectionnee.parent.nom, x, y))
            self.etiquette_selectionnee = None
            return

        if self.debut_zoom and not self.fixe:
            #self.ReleaseMouse()
            try:
                if event.ControlDown():
                    (x0, y0), (x1, y1) = self.debut_zoom, self.fin_zoom
                    self.executer("fenetre = " + str((x0, x1, y0, y1)))
                else:
                    self.rafraichir_affichage()
            finally:
                self.debut_zoom = None

        elif self.debut_select and not self.fixe:
            #self.ReleaseMouse()
            try:
                if event.ControlDown() and event.AltDown():
                    (x0, y0), (x1, y1) = self.debut_select, self.fin_select
                    self.OnSelect(x0, x1, y0, y1)
                else:
                    self.rafraichir_affichage()
            finally:
                self.debut_select = None

        elif self.interaction:
            self.signal(event)
            self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))

        elif self.deplacable(self.select):
            self.parent.action_effectuee(self.select.nom + str(self.select.coordonnees))
            # pas super precis : il se peut qu'on relache le bouton sans avoir deplace le point.
            # ca fait un enregistrement inutile dans l'historique...



    def OnRightDown(self, event):
        self.editeur.close()
        self.detecter(event.GetPositionTuple())

        if self.select is not None and not event.ControlDown():
            menu = wx.Menu()
            # Contournement d'un bug de wxGtk
            if wx.Platform == '__WXGTK__':
                menuitem = menu.Append(wx.NewId(), u" \u2714 " + self.select.nom_complet)
                menu.AppendSeparator()
            else:
                menu.SetTitle(self.select.nom_complet)
            selections = [obj for obj in self.selections if obj is not self.select] # autres objets a proximite
            n = len(selections)
            ids = [wx.NewId() for i in xrange(n + 7)]
            for i in range(n):
                def select(event, obj = selections[i]):
##                    select.affiche_en_gras(False)
                    self.select = self.select_memoire = obj
                    self.selection_en_gras()
                menu.Append(ids[i], u"Sélectionner " + selections[i].nom_complet)
                menu.Bind(wx.EVT_MENU, select, id = ids[i])
            if n:
                menu.AppendSeparator()

            menu.Append(ids[n], u"Supprimer")
            def supprimer(event, select = self.select):
                self.executer(u"%s.supprimer()" %select.nom)
            menu.Bind(wx.EVT_MENU, supprimer, id = ids[n])

            if self.select.style("visible"):
                chaine = u"Masquer"
            else:
                chaine = u"Afficher"
            menu.Append(ids[n + 1], chaine)
            def masquer(event, select = self.select):
                self.executer(u"%s.style(visible = %s)" %(select.nom, not self.select.style("visible")))
            menu.Bind(wx.EVT_MENU, masquer, id = ids[n + 1])

            menu.Append(ids[n + 2], u"Renommer")
            def renommer(event, select = self.select):
                dlg = wx.TextEntryDialog(self, u"Note: pour personnaliser davantage le texte de l'objet,\nchoisissez \"Texte associé\" dans le menu de l'objet.", u"Renommer l'objet", select.nom_corrige)
                test = True
                while test:
                    test = dlg.ShowModal()
                    if test == wx.ID_OK:
                        try:    # on renomme, et on met l'affichage de la légende en mode "Nom"
                            self.executer(u"%s.renommer(%s, legende = %s)" %(select.nom, repr(dlg.GetValue()), wxgeo.NOM))
                            break
                        except:
                            print_error()

                    if test == wx.ID_CANCEL:
                        break
                dlg.Destroy()
            menu.Bind(wx.EVT_MENU, renommer, id = ids[n + 2])

            msg = u"Éditer le texte" if isinstance(self.select, Texte_generique) else u"Texte associé"
            menu.Append(ids[n + 3], msg)
            def etiquette(event, select = self.select):
                old_style = select.style().copy()
                old_label = select.style(u"label")
                if old_label is None:   # le style label n'existe pas pour l'objet
                    return
                #dlg = wx.TextEntryDialog(self, "Note: le code LATEX doit etre entre $$. Ex: $\\alpha$", "Changer l'etiquette de l'objet", old_label, style = wx.TE_MULTILINE )
                #dlg.FitInside()
                dlg = wx.Dialog(self, -1, u"Changer la légende de l'objet (texte quelconque)")
                sizer = wx.BoxSizer(wx.VERTICAL)
                sizer.Add(wx.StaticText(dlg, -1, u"Note: le code LATEX doit etre entre $$. Ex: $\\alpha$"), 0, wx.ALIGN_LEFT|wx.ALL, 5)
                dlg.text = wx.TextCtrl(dlg, -1, old_label, size=wx.Size(300,50), style = wx.TE_MULTILINE)
                sizer.Add(dlg.text, 0, wx.ALIGN_LEFT|wx.ALL, 5)
                dlg.cb = wx.CheckBox(dlg, -1, u"Interpréter la formule")
                dlg.cb.SetValue(select.style(u"legende") == wxgeo.FORMULE)
                sizer.Add(dlg.cb, 0, wx.ALIGN_LEFT|wx.ALL, 5)
                line = wx.StaticLine(dlg, -1, size=(20,-1), style=wx.LI_HORIZONTAL)
                sizer.Add(line, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.TOP, 5)
                box = wx.BoxSizer(wx.HORIZONTAL)
                btn = wx.Button(dlg, wx.ID_OK)
                #btn.SetDefault()
                box.Add(btn, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
                btn = wx.Button(dlg, wx.ID_CANCEL, u" Annuler ")
                box.Add(btn, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
                sizer.Add(box, 0, wx.ALIGN_RIGHT|wx.ALL, 5)
                dlg.SetSizer(sizer)
                sizer.Fit(dlg)
                dlg.CenterOnScreen()
                test = True
                while test:
                    test = dlg.ShowModal()
                    if test == wx.ID_OK:
                        try:
                            self.executer(u"%s.label(%s, %s)" %(select.nom, repr(dlg.text.GetValue()), dlg.cb.GetValue()))
                            break
                        except:
                            select.style(**old_style)
                            #~ print "Ancien style", old_style
                            print_error()
                    if test == wx.ID_CANCEL:
                        #select.label(old_label)
                        #~ select.style(**old_style)
                        break
                dlg.Destroy()
            menu.Bind(wx.EVT_MENU, etiquette, id = ids[n + 3])

            if self.select.label():
                chaine = u"Masquer"
            else:
                chaine = u"Afficher"
            menu.Append(ids[n + 4], chaine + u" nom/texte")
            def masquer_nom(event, select = self.select):
                if self.select.label():
                    mode = wxgeo.RIEN
                else:
                    if self.select.style(u"label"):
                        mode = wxgeo.TEXTE
                    else:
                        mode = wxgeo.NOM
                self.executer(u"%s.style(legende = %s)" %(select.nom, mode))
            menu.Bind(wx.EVT_MENU, masquer_nom, id = ids[n + 4])

            menu.AppendSeparator()

            menu.Append(ids[n + 5], u"Redéfinir")
            def redefinir(event, select = self.select):
                nom = select.nom
                dlg = wx.TextEntryDialog(self, u"Exemple: transformez une droite en segment.", u"Redéfinir l'objet", select._definition())
                test = True
                while test:
                    test = dlg.ShowModal()
                    if test == wx.ID_OK:
                        try:    # on redéfinit l'objet
                            self.executer(u"%s.redefinir(%s)" %(nom, repr(dlg.GetValue())))
                            break
                        except:
                            print_error()

                    if test == wx.ID_CANCEL:
                        break
                dlg.Destroy()
            menu.Bind(wx.EVT_MENU, redefinir, id = ids[n + 5])

            menu.AppendSeparator()

            if isinstance(self.select, Point_generique):
                ids_relier = [wx.NewId(), wx.NewId(), wx.NewId()]
                relier = wx.Menu()
                relier.Append(ids_relier[0], u"aux axes")
                relier.Append(ids_relier[1], u"à l'axe des abscisses")
                relier.Append(ids_relier[2], u"à l'axe des ordonnées")


                def relier0(event, self = self, select = self.select):
                    self.executer(u"%s.relier_axes()" %select.nom)
                relier.Bind(wx.EVT_MENU, relier0, id = ids_relier[0])

                def relier1(event, self = self, select = self.select):
                    self.executer(u"%s.relier_axe_x()" %select.nom)
                relier.Bind(wx.EVT_MENU, relier1, id = ids_relier[1])

                def relier2(event, self = self, select = self.select):
                    self.executer(u"%s.relier_axe_y()" %select.nom)
                relier.Bind(wx.EVT_MENU, relier2, id = ids_relier[2])

                menu.AppendMenu(wx.NewId(), u"Relier le point", relier)


                menu.AppendSeparator()

            menu.Append(ids[n + 6], u"Propriétés")
            def proprietes(event, select = self.select):
                from GUI.proprietes_objets import Proprietes
                win = Proprietes(self, [select])
                win.Show(True)
            menu.Bind(wx.EVT_MENU, proprietes, id = ids[n + 6])

            self.PopupMenu(menu)
            menu.Destroy()
            if self.select is not None:
##                self.select.affiche_en_gras(False)
                self.select = None
                self.selection_en_gras()
            self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
        elif not self.fixe:
            self.debut_shift = self.pix2coo(*event.GetPositionTuple())
            self.SetCursor(wx.StockCursor(wx.CURSOR_SIZING))
            if not self.HasCapture():
                self.CaptureMouse()




    def OnRightUp(self, event):
        if self.HasCapture():
            self.ReleaseMouse()

        if self.fixe: return

        self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))

        if self.debut_shift:
            self.debut_shift= None
            self.parent.action_effectuee(u"fenetre = " + str(self.fenetre))


    def editer(self, mode = 0):
        if self.select is not None:
            self.editeur.init(self.select, mode)
        else:
            self.editeur.close()


    def OnChar(self, event):
        if self.redetecter:
            self.detecter()

        key = event.GetKeyCode()
        debug(u"key: ", key)
        if key == wx.WXK_DELETE and self.select:
            if event.ShiftDown():
                self.executer(u"%s.cacher()" %self.select.nom)
            else:
                self.executer(u"%s.supprimer()" %self.select.nom)
        if (key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER) and self.editeur.objet is not self.select:
            self.editer(event.ShiftDown())
        elif self.editeur:
            self.editeur.key(key)

        if key == wx.WXK_ESCAPE and self.interaction:
            print "ESCAPE !"
            self.interaction(special = "ESC")

        if key < 256:debug(unichr(key))
        event.Skip()



    def OnPaint(self, event):
        self.graph.restaurer_dessin()
        if param.plateforme == "Windows":
            event.Skip()


    def OnSize(self, event):
##        if param.plateforme == "Windows":
##             self.execute_on_idle(self.rafraichir_affichage, rafraichir_axes = True)
        self.rafraichir_affichage(rafraichir_axes = True)
        event.Skip()

    def OnLeave(self, event):
        self.execute_on_idle(self.feuille_actuelle.objets_en_gras)


