company:ungroup

  • Pendant l’indisponibilité de mon pc, je n’ai bien sur pas avancé autant que je l’aurais voulu. Cependant, j’en ai profité pour faire une liste des #undo/redo inexistant ou fonctionnant mal. J’ai également réalisé un petit tutorial pour expliquer comment réaliser un undo/redo pour Scribus ci dessous. N’hésitez donc pas à aider à son développement :-)

    Qu’est ce qu’un undo/redo ?

    Comme leurs noms l’indiquent, un undo permet de revenir en arrière sur une action et le redo permet de refaire une action annulée précédement.

    Comment fonctionne un undo/redo ?

    Lorsque l’on effectue une action, notre programme mémorise les informations nécessaires pour défaire et refaire une telle action. Ces informations sont mises dans une pile ainsi, lorsque l’on appuie sur CTRL + Z, la dernière action est dépilée, les informations enregistrées permettent de supprimer la dernière action et à la place cette action est ajoutée à la pile des redo. Le fonctionnement du redo est identique à celui du undo.

    Comment est-ce mis en place à travers Scribus ?

    Bien sur, tout le système de gestion des undo/redo existe déjà. Pour mettre en place une telle action, la seule chose à faire est de dire quelles sont les informations dont on a besoin dans notre pile pour pouvoir revenir en arrière et effectuer de nouveau cette action. Puis il faut écrire la fonction appellée pour effectuer ces changements.

    Concrètement prenons l’exemple simple du verrouillage de guide :

    « void ScribusDoc::lockGuides(bool isLocked)
    {
    if (GuideLock == isLocked)
    return ;
    GuideLock = isLocked ;
    if (UndoManager::undoEnabled())
    {
    QString name ;
    if (isLocked)
    name = Um::LockGuides ;
    else
    name = Um::UnlockGuides ;
    SimpleState (star)ss = new SimpleState(name, »", Um::ILockGuides) ;
    ss->set("GUIDE_LOCK", isLocked) ;
    undoManager->action(this, ss) ;
    }
    }"

    Toute la partie relative au undo se trouve dans la condition : undoEnabled(). En effet, on ne veut pas prendre en compte ces informations si le undo est désactivé. Il faut savoir que le undo désactivé peut l’être par le programme et non par l’utilisateur. Lorsqu’on presse CTRL + Z, le undo est désactivé par scribus tant que l’action de retour n’est pas effectuée. Sinon, en faisant appelle à une fonction déjà existante pour revenir en arrière, on enregistrerait une nouvelle action de undo alors que ce n’est pas le but ici. Bref, il faut toujours encadrer ces informations dans une condition UndoManager::undoEnabled().

    Ensuite, on est ici dans un cas simple, c’est à dire un cas où les seules variables qu’on a besoin de stocker sont des int, float, boolean, ... des variables standards. Je vous parlerai des autres cas plus tard. Dans ces cas là, on peut utiliser un objet SimpleState pour stocker les informations. Ses trois parametres sont le nom (celui qui apparait à côté de undo lorsque l’on clic sur édition), sa description et une image correspondant à ce undo.
    Pour connaitre la liste des noms et des images déjà existants, il faut aller voir dans undomanager.cpp. On utilise ici Um: : au lieu de UndoManager: : mais les deux sont équivalents. Si vous souhaitez ajouter un nom ou une image à ceux disponibles, c’est donc dans undomanager.cpp et undomanager.h qu’il faut aller l’ajouter.
    Ensuite, pour ajouter une valeur à retenir, il suffit d’utiliser set() comme dans l’exemple précédent. Les arguments sont le nom que l’on donne à la variable et la valeur associée. Il faut avoir au moins une variable parmi celles entrées, qui soit unique pour savoir quelle est l’action qui est ciblée lors du undo. On trouve souvent dans le code :

    "ss->set("ACTION_NAME","action_name") ;"

    Simplement pour savoir le nom de l’action même si la valeur de la variable n’a aucune importance.

    Enfin, pour que l’action soit enregistrée :

    « undoManager->action(this, ss) ; »

    la première variable doit être l’objet ciblé par le undo (pas forcément this) et le deuxième argument est l’objet contenant toutes les autres informations.

    Cette fois, l’action est donc ajoutée à la pile de undo. Il faut ensuite la prendre en considération.
    Lorsqu’on demande un undo, la fonction appellée est la fonction restore se trouvant dans l’objet qu’on a passé en premier argument de undoManager->action(). Bien souvent la fonction restore() est déjà écrite (je n’ai pas encore eu l’occasion de voir un autre cas), en s’aidant des exemples au dessus, vous devriez avoir quelque chose du genre :

    " else if (ss->contains("UNGROUP"))
    restoreUngrouping(ss, isUndo) ;
    "
    Comme je vous l’ai dit, il faut avoir une variable unique dans notre simpleState pour pouvoir detecter quelle est l’action qu’on veut traiter. Cette variable est ici « UNGROUP ». Bien souvent, l’action à effectuer fait plus de deux lignes donc, pour ne pas surcharger la fonction restore(), on crée une fonction qui se charge du restore spécifique, ici restoreUngrouping().

    Voici un cas simple du restore spécifique :
    " if (isUndo)
    GuideLock = !ss->getBool("GUIDE_LOCK") ;
    else
    GuideLock = ss->getBool("GUIDE_LOCK") ;
    "
    Pour récupérer la variable, on utilise donc get() avec comme paramètre le nom de la variable. On peut utiliser également getInt, getBool, getFloat, ... puis on effectue les actions qu’on souhaite.
    On peut aussi voir qu’il y a deux types d’action. Celles dans le cas où isUndo est vrai et le cas où c’est faux. Cela permet juste de différencier le comportement de undo (isUndo = true) et de redo (isUndo = false)

    Vous savez maintenant faire un undo.

    Comment faire lorsque l’on doit se souvenir d’un objet et pas seulement d’un int/bool/float...?

    Un autre objet que simpleState existe dans un cas comme ça : ScItemState<Class>

    Exemple :
    « ScItemState<PageItem(star)> (star)is = new ScItemState<PageItem(star)>(Um::ChangeShapeType, »", Um::IBorder) ;"

    Il prend les mêmes arguments qu’un SimpleState mais on peut, en plus, lui donner le type d’un objet qu’on veut qu’il stocke. On ne peut donc stocker qu’un seul objet mais c’est mieux que pas du tout.
    Cet objet a une methode : setItem() qui permet de lui passer cet objet. On peut toujours utiliser la methode set() pour lui passer autant de int/float/string ... que l’on veut tant qu’il ne reçois qu’un objet. Pour passer cette restriction, il faudra donc utiliser des list ou des pair.

    De cette façon, on peut traiter une grande partie des undo/redo.

    Lorsque je fais mon action, j’ai plusieurs actions ajoutées à ma pile de undo.

    En effet, si on avait une action de déplacement décomposée en un déplacement vertical et un déplacement horizontal, lorsqu’on bougerait une forme, on aurait deux actions ajoutées à notre pile de undo. Et il faut donc enlever ces actions intermédiaires une par une pour avoir un undo complet (si la liste des actions intermédiaires n’a pas dépassé la taille maximum de la pile. Dans ce cas, on ne pourra pas revenir entièrement en arrière).
    Deux solutions s’offrent à nous dans un cas comme celui ci :
    Si en effectuant tous les undo intermédiaires, on atteint bien la position de départ, on peut regrouper toutes les actions en une seule de la façon suivante :
    « //démarrer le regroupement
    UndoTransaction(star) activeTransaction = new UndoTransaction(undoManager->beginTransaction(Um::Group + »/" + Um::Selection, Um::IGroup, Um::Delete, tooltip, Um::IDelete)) ;

    //finir le regroupement
    if (activeTransaction)
    {
    activeTransaction->commit() ;
    delete activeTransaction ;
    activeTransaction = NULL ;
    }
    "
    Tout ce qui est au milieu est regroupé en une seule action. Les arguments à entrer lors de la création d’une transaction sont : la cible de l’action (selection/page/...), l’image correspondante, le nom de l’action(celui qui apparait dans le programme), une description et l’image correspondante.
    L’autre possibilité, plutôt utilisé lorsque toutes les actions intermédiaires n’ont pas une action undo dédiée, c’est de supprimer de la liste toutes les actions intermédiaires en encadrant le code de :

    « undoManager->setUndoEnabled(false) ;
    //mon code
    undoManager->setUndoEnabled(true) ; »

    puis de créer une action à ajouter à la pile de undo qui permet de prendre en compte tous les changements liés à cette action.
    Il n’y a pas à se poser la question, « est ce que lors de l’appel de setUndoEnabled(false), le undo était déjà indisponible ? Parce que dans ce cas, setUndoEnabled(true) le rendrait disponible alors qu’il ne l’était pas normalement... ». Ceci est gérer tout seul par la fonction, il suffit de bien penser. Dés que j’utilise un setUndoEnabled(false), il faut bien utiliser setUndoEnabled(true) à la fin pour que tout soit remis dans l’ordre.
    En pratique, on peut utiliser les deux techniques citées précédemment de façon combinée. C’est-à-dire, si un undo est bien géré, on garde ce undo dans la liste et on ajoute seulement ceux qui sont mal gérés puis on regroupe toutes ces actions en une seule. C’est souvent le moyen que je trouve le plus efficace mais libre à vous de faire ce qui vous parrait le mieux.

    Je crois avoir fait un petit tour des différents fonctionnements du undo. Le plus dur maintenant est de trouver et comprendre les fonctions dans lesquelles il faut ajouter le undo et de corriger les bugs que l’ont peut découvrir en faisant des testes tordus.