Parcours de l'historique par undo & redo
Avant-propos
UiNode lors des secousses, en filtrant sur le bouton gauche.UiNode.mousePressEvent() par :
def mousePressEvent(self, ev):
""" Crée la liste des court-circuits faisables sur ce node : o_node.l_shortables.
Un court-circuit est un tuple de 2 objets o_gredge : (o_gredge en entrée, o_gredge en sortie). """
if ev.button() == Qt.LeftButton:
self.o_node.set_shortables()
super().mousePressEvent(ev)
UiNode.mouseMoveEvent() par :
def mouseMoveEvent(self, ev):
if ev.buttons() & Qt.LeftButton:
self.o_node.shake()
super().mouseMoveEvent(ev)
getcwd() pose parfois des problèmes selon le contexte :Dans UiMain.open_graph(), remplacer cette ligne :
backups_folder = os.path.abspath(f"{os.getcwd()}{os.sep}..{os.sep}backups")
Par celle-ci :
backups_folder = os.path.abspath(f"{os.path.dirname(__file__)}/../backups")
Vérifier le bon fonctionnement de l'ouverture d'un graphe.
NewGraph.accept(), remplacer cette ligne :bk_folder = f"{os.getcwd()}{os.sep}..{os.sep}backups"
bk_folder = os.path.abspath(f"{os.path.dirname(__file__)}/../backups")
Vérifier le bon fonctionnement de la création d'un graphe.
CtrlScene, renommer delete_items() en remote_items()UiView.keyReleaseEvent().UiView.keyReleaseEvent() :
def keyReleaseEvent(self, ev):
super().keyReleaseEvent(ev)
key = ev.key()
if key == Qt.Key_Delete:
self.o_scene.delete_items()
if key == Qt.Key_Tab:
self.o_scene.cross_edges()
Description

Super-dictionnaires od_pkl successifs dans le temps.
dq_histos est une liste de type deque, vide au lancement de l'application.od_pkl est ajouté à cette liste, le pointeur avance en dernière place.undo-redo permet le droit à l'erreur :
undo, qui déplace le pointeur vers le début, pas à pas.redo, qui ramène le pointeur vers la fin.undo.redo.
o_ur.UndoRedo.Undo avec son raccourci-clavier Ctrl + Z.Redo avec son raccourci-clavier Ctrl + Y.
et
, revoir si nécessaire les tutos antérieurs ou visionner la vidéo.
Au lancement de l'appli, dictionnaire dq_histos vide (Undo et Redo désactivés).
Des modifications, pointeur à la fin (Redo désactivé).
Des modifications, pointeur au début (Undo désactivé)
Des modifications, pointeur à l'intérieur, ni au début ni à la fin.
Ajoutez les boutons Undo et Redo avec Qt Designer. :
actionUndo et actionRedo.tooltips sont également définis dans Qt Designer : Undo (Ctrl + Z) et Redo (Ctrl + Y)Ctrl + Z et Ctrl + Y.Fichier /pc/undo_redo.py, contenant la classe UndoRedo :
import copy
from collections import deque
class UndoRedo:
""" Instancié par CtrlScene. """
def __init__(self, o_scene):
self.o_scene = o_scene
self.pointer = 0
self.b_action = False
self.b_undo = False
self.b_redo = False
""" Les éléments de la liste dq_histo sont les super-dictionnaires od_pkl successifs dans le temps. """
self.dq_histos = deque([copy.deepcopy(self.o_scene.o_pkl.od_pkl)], maxlen=100) # Taille de l'histo = maxlen
def state(self):
""" État des icones 'Undo' et 'Redo' dans la toolbox. """
histo_max = len(self.dq_histos) - 1
self.b_undo = self.pointer > 0
self.b_redo = self.pointer < histo_max
""" État des boutons : actifs ou grisés. """
self.o_scene.o_main.actionUndo.setEnabled(self.b_undo)
self.o_scene.o_main.actionRedo.setEnabled(self.b_redo)
def add_do(self):
""" Le code appelant doit lever le flag b_action pour que le nouvel état soit ajouté à l'historique. """
if self.b_action:
""" Option : Suppression de tous les éléments après le pointeur. Décommenter pour l'activer. """
# histo_max = len(self.dq_histo) - 1
# for i in range(histo_max - self.pointer):
# self.dq_histo.pop()
""" Ajout. Si maxlen est atteint, le 1er élément (le plus ancien) est automatiquement supprimé. """
self.dq_histos.append(copy.deepcopy(self.o_scene.o_pkl.od_pkl))
""" Pointeur placé à la fin. """
self.pointer = len(self.dq_histos) - 1
self.state()
""" Histo à jour => On baisse le drapeau. """
self.b_action = False
def undo(self):
if self.b_undo:
self.pointer -= 1
self.show()
def redo(self):
if self.b_redo:
self.pointer += 1
self.show()
def show(self):
""" Le super-dictionnaire od_pkl est surchargé par l'élément pointé de l'historique. """
self.state()
self.o_scene.o_pkl.od_pkl = copy.deepcopy(self.dq_histos[self.pointer])
self.o_scene.o_pkl.backup()
self.o_scene.show_items()
Ajoutez cette déclaration dans CtrlScene.__init__() :
self.o_ur = None # Instance de UndoRedo.
Ajoutez cette affectation dans CtrlScene.setup(), ne pas oublier d'importer UndoRedo :
""" Undo-Redo. """
self.o_ur = UndoRedo(self)
- Nous allons coder de façon peu habituelle : en rectifiant le code par corrections successives ...
- ... un peu à la manière des TDD, mais avec un contrôle visuel.
- Supposons le problème résolu, et lançons l'application :
- Afficher plusieurs onglets contenant des graphes, par exemple Couleurs, Court-circuits et Croisements du tuto précédent.
- Cela nous servira de base pour cet exercice.
- Vous noterez qu'au fur et à mesure, nous découvrirons des bugs oubliés ; nous en profiterons pour les corriger.
- Si vous êtes programmeur débutant, bienvenue au club !
Undo et Redo devraient être désactivés au démarrage.add_do() est appelée lorsqu'on clique simplement sur un node, sans même le déplacer.
add_do() apparaît autant de fois qu'il y a de nodes sélectionnés, déplacés dans le graphe.dq_histos n'a pas été incrémenté, les 2 boutons restent désactivés.Redo, rien ne se passe.CtrlScene.show_items() est absente.
au lieu de celà : 
UndoRedo.state() doit être appelée, c'est elle qui fixe l'état des boutons.tab_changed()).UiMain.tab_changed(), juste avant ut.save_tabs() :
o_scene.o_ur.state()
Vérification : pour chaque onglet, les 2 boutons sont bien désactivés au démarrage.
add_do() qui ajoute un élément dans l'historique.Pkl.backup(), juste après le pickle.dump().add_do() dans Pkl.backup(), à la fin, avant le return True :
self.o_scene.o_ur.add_do() # Undo-Redo.
print() provisoire en 1ère ligne de UndoRedo.add_do() :
print('add_do')
print() a bien lieu, mais 3 problèmes apparaissent :
dq_histos n'a pas été incrémenté, les 2 boutons restent désactivés → Problème 2.3.add_do() est appelée lorsqu'on clique simplement sur un node, sans même le déplacer.UiNode.save_pos() est appelée (indirectement, via la scène) lorsqu'on clique sur un node.backup.UiNode.__init__() :
self.pos_ante = None
UiNode.init_ui() (après setPos()) :
self.pos_ante = self.o_node.pos
UiNode.save_pos() :
def save_pos(self):
x, y = self.pos().x(), self.pos().y()
b_magnetic = self.o_node.o_scene.get_param('Grille magnétique')
if b_magnetic:
mesh = int(self.o_node.o_scene.get_param('Pas de la grille', 16))
x, y = mesh * round(x/mesh), mesh * round(y/mesh)
self.setPos(x, y)
if self.pos_ante != (x, y):
self.od_pkl.write([self.s_id, 'position'], (x, y))
self.o_node.o_scene.o_pkl.backup()
self.pos_ante = (x, y) # Mémorisation de la position.
print().self.ToolTip(.....) de la méthode UiNode.init_ui().UiNode.set_title() :
def set_title(self):
title = self.o_node.get_param('Titre du node', 'Le titre')
self.setToolTip(f"{title}\n{round(self.pos().x()), round(self.pos().y())}")
self.title_item.setPlainText(title)
save_pos() doit encore être modifiée pour actualiser le tooltip.UiNode.save_pos() :
def save_pos(self):
x, y = self.pos().x(), self.pos().y()
b_magnetic = self.o_node.o_scene.get_param('Grille magnétique')
if b_magnetic:
mesh = int(self.o_node.o_scene.get_param('Pas de la grille', 16))
x, y = mesh * round(x/mesh), mesh * round(y/mesh)
self.setPos(x, y)
if self.pos_ante != (x, y):
self.od_pkl.write([self.s_id, 'position'], (x, y))
self.setToolTip(f"{self.o_node.get_param('Titre du node', 'Le titre')}\n{round(x), round(y)}")
self.o_node.o_scene.o_pkl.backup()
self.pos_ante = (x, y) # Mémorisation de la position.
print() apparaît autant de fois qu'il y a de nodes sélectionnés, déplacés dans le graphe.save_pos(), qui contient l'appel au backup global.delay() que nous avions codée dans la classe DateTime permett cela :Pkl.backup() devient :
def backup(self):
""" Méthode appelée chaque fois qu'une modification intervient dans le graphe.
- Les opérations sur disque dur sont chronophages.
- Le delay permet un anti-mitraillage : seul le dernier appel d'une répétiton est traité.
- En mode asynchrone (par défaut), la méthode est temporisée et ne retourne rien.
- En mode synchrone, la méthode est immédiate et retourne le succès : True ou False.
"""
def delayed():
try:
with open(self.fic_pkl, 'wb') as pk: # 1) Ouveture ET écrasement du fichier pkl.
pickle.dump(self.od_pkl, pk, pickle.HIGHEST_PROTOCOL) # 2) Écriture dans un fichier vierge.
except (Exception,):
return False
""" Enregistrement réussi. """
self.o_scene.o_ur.add_do() # Undo-Redo.
return True
self.dt.delay(delayed) # Exécution temporisée.
self.dt et importer la classe DateTime.Pkl.__init__() :
self.dt = DateTime()
print() n'affiche son message qu'une seule fois.dq_histos n'a pas été incrémenté, les 2 boutons restent désactivés.UndoRedo.add_do(), on s'aperçoit que le code n'est exécuté que si le flag b_action est levé.False dans __init__(), puis n'est modifié nulle part ailleurs.b_action sera levé.
CtrlScene.delete_items(), à la ligne avant le backup() :
self.o_ur.b_action = True # Ajout dans l'historique du "Undo-Redo".
CtrlScene.save_edges(), à la ligne avant le backup() :
self.o_ur.b_action = True # Ajout dans l'historique du "Undo-Redo".
UiNode.save_pos(), à la ligne avant le backup() :
self.o_scene.o_ur.b_action = True
Parameters.change() :
def change(self, _, l_params):
""" 1 - Mise à jour du dictionnaire od_real. """
o_param = l_params[0][0]
od_param = Dictionary(o_param.__dict__)
value = o_param.value()
if isinstance(value, QColor):
value = value.name()
l_keys = od_param.read(['opts', 'l_keys'])
self.od_real.write([self.o_parent.s_id] + l_keys, value)
if self.o_parent.__class__.__name__ == 'Node':
self.o_scene.o_ur.b_action = True
""" 2 - Enregistrement. """
self.o_scene.o_pkl.backup()
self.set_params()
self.o_parent.refresh(l_keys)
Redo, rien ne se passe.Undo et Redo n'ont pas encore été codés.New et Open)UiMain.set_events(), ajouter ces 2 lignes :
self.actionUndo.triggered.connect(self.undo)
self.actionRedo.triggered.connect(self.redo)
Oui mais ... les méthodes undo() et redo() n'existent pas dans UiMain. On les ajoute.
UiMain.undo() et UiMain.redo() :
def undo(self):
self.o_scene.o_ur.undo()
def redo(self):
self.o_scene.o_ur.redo()
o_scene qui est la scène en cours, mais qui n'existe pas.UiMain.__init__(), ajouter cette ligne :
self.o_scene = None
UiMain.tab_changed() :
def tab_changed(self, index=-1):
""" Hook.
:param index: Automatiquement renseigné par le connect de 'currentChanged'.
:return: NA.
"""
indx = index if index >= 0 else self.tabGraphs.currentIndex()
if indx < 0:
self.o_scene = None
return # Si aucun onglet affiché.
self.o_scene = self.tabGraphs.widget(indx)
# self.o_scene.show_params() # Devenu inutile suite à l'implémentation de CtrlScene.show_items().
self.o_scene.o_ur.state()
ut.save_tabs(self) # On restitue la fonction première de self.tabGraphs.currentChanged
Vérification :
CtrlScene.show_items() est absente. On l'ajoute.CtrlScene.show_items() :
def show_items(self):
""" Reset : suppression de tous les items. """
l_gritems = self.o_grscene.items()
for gritem in l_gritems:
self.o_grscene.removeItem(gritem)
""" Affichage des nodes mémorisés dans les paramètres. """
self.show_nodes()
""" Affichage des edges mémorisés dans les paramètres. """
self.show_edges()
""" Aucun item sélectionné => Paramètres de la scène. """
self.show_params()
show_nodes() et show_edges().undo et redo effectuent un réaffichage de la scène, il est nécessaire de tout nettoyer d'abord.reset préalable : nettoyage de toute la scène.show_params() est appelée.CtrlScene.setup(), par une seule.
""" Affichage des nodes mémorisés dans les paramètres. """
self.show_nodes()
""" Affichage des edges mémorisés dans les paramètres. """
self.show_edges()
""" Affichage des items : nodes et edges, mémorisés dans les paramètres. """
self.show_items()
A noter, la ligne show_params() commentée dans la méthode tab_changed(), un peu au dessus (Problème 3).
Si vous refusez cette option, vous devrez décommenter cette ligne.
Bonjour les codeurs !