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
. 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.
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 !