Système d'annuler-refaire par arbre (turs)

0/5 (7 avis)

Vue 5 119 fois - Téléchargée 385 fois

Description

TURS (Tree Undo Redo System) permet de gérer les annuler-refaire (ou précédent-suivant) sans perte de données avec l'utilisation d'un arbre.

Imaginons un sysème classique de précédent-suivant dans un navigateur web. Nous avons visité les sites webs suivants et nous sommes sur le dernier :

Site 1
Site 2
Site 3
Site 4 <-

On revient en arrière deux fois :

Site 1
Site 2 <-
Site 3
Site 4

Si l'on va sur un nouveau site 5, on va perdre les sites 3 et 4 :

Site 1
Site 2
Site 5 <-

Cependant, avec TURS, on utilise un arbre pour créer des branches (symbolisées par des points).

Site 1
Site 2
.
Site 3
Site 4
.
Site 5 <-

Ainsi les sites 3 et 4 sont conservés et depuis le site 2, il est possible d'accéder soit au site 5, soit aux sites 3 et 4.

Cette source peut aussi permettre, dans une certaine mesure, de s'initier aux templates en C++ ainsi qu'aux arbres (j'ai appris à les utiliser en programmant cette source).

Les fonctionnalités sont assez limitées (voir l'exemple fourni), mais il est possible d'adapter le code en modifiant directement les sources ou en héritant de la classe TURS.

Le code est commenté et documenté (la documentation est déjà générée et le fichier Doxyfile est fourni).

Attention, le zip et le code fournis sur cette page ne sont pas forcément à jour ! Pour voir les dernières sources, rendez-vous ici : https://bitbucket.org/mcc/dev/src/tip/TURS/

Source / Exemple :


#include "../../src/TURS.hh"
#include <cstdlib>
#include <vector>
#include <fstream>

void error(unsigned int line, const std::string& message) {
	std::cerr << "Ligne " << line << " : " << message << std::endl;
}

//Dans cet exemple, on lit l'entrée standard ou un fichier passé en paramètre
//afin d'exécuter des commandes pour manipuler le TURS.
int main(int argc, char** argv) {
	typedef std::vector<std::string>::const_iterator iter;
	typedef TURS<std::string> TURS_str;
	
	//Création d'un TURS de string.
	TURS_str turs;

	std::string line;
	std::vector<std::string> args;
	unsigned int i = 1;

	//On sélectionne le fichier passé en paramètre ou l'entrée standard.
	std::ifstream file;
	if(argc > 1) {
		file.open(argv[1]);
		if(!file) {
			std::cerr << "Le fichier n'existe pas : " << argv[1] << std::endl;
			return 1;
		}
	}
	std::istream& in = (argc > 1) ? file : std::cin;

	while(std::getline(in, line)) {
		//On récupère la commande.
		std::istringstream iss(line);
		std::string cmd;
		iss >> cmd;

		//On récupère les arguments.
		while(iss) {
			std::string arg;
			iss >> arg;
			args.push_back(arg);
		}

		//On exécute la commande.
		if(cmd == "action")  {
			if(args.size() > 0) {
				turs.addAction(args[0]);
			} else {
				error(i, "Argument manquant pour la commande 'action'");
			}
		} else if(cmd == "undo") {
			turs.undo();
		} else if(cmd == "redo") {
			if(args.size() > 0) {
				turs.redo(atoi(args[0].c_str()));
			} else {
				turs.redo();
			}
		} else if(cmd == "print") {
			if(args.size() > 0)  {
				for(iter it = args.begin(); it != args.end(); ++it) {
					if(*it == "$tree") {
						std::cout << turs << " ";
					} else if(*it == "$currentVal") {
						std::cout << turs.currentVal() << " ";
					} else if(*it == "$nl") {
						std::cout << std::endl;
					} else {
						std::cout << *it << " ";
					}
				}
				std::cout << std::endl;
			}
		} else if(cmd == "clear") {
			turs.clear();
		} else if(cmd == "mode") {
			if(args.size() > 0) {
				if(args[0] == "tree") {
					turs.setMode(TURS_str::TREE_MODE);
				} else if(args[0] == "normal") {
					turs.setMode(TURS_str::NORMAL_MODE);
				} else {
					error(i, "Argument invalide pour la commande 'mode'");
				}
			} else {
				error(i, "Argument manquant pour la commande 'mode'");
			}
		} else if(cmd == "quit") {
			break;
		}
		
		args.clear();
		i++;
	}

	return 0;
}

Conclusion :


J'espère que cette source pourra être utile !

Il existe peut-être des bugs, si c'est le cas, merci de me les indiquer.

Codes Sources

A voir également

Ajouter un commentaire Commentaires
cptpingu Messages postés 3836 Date d'inscription dimanche 12 décembre 2004 Statut Modérateur Dernière intervention 11 février 2023 124
23 juin 2011 à 18:30
C'est un code très intéressant et propre. C'est assez rare !
(Pas de using namespace, pas de mélange C et C++, utilisation de la STL, des templates, documentation, etc... Ça fait plaisir !).
C'est sympa aussi d'avoir une documentation. C'est encore plus sympa de l'avoir fait en Doxygen !

Au sujet du comportement du programme, il n'y a rien à redire, c'est pratique et aisément utilisable en tant que bibliothèque.

Y a des petits trucs qui me gênent un peu, mais ce n'est pas très grave:
- Inclusion de .cpp dans un .hpp. Pour séparer un code de sa définition, je conseille généralement le couple: .cc/.hh/.hxx ou .cpp/.hh/.hpp
- std::string TURSNode<T>::toString => Plutôt qu'un "std::string ret;" j'utiliserais un std::ostringstream (concaténation plus rapide qu'avec un std::string) avec un "return ret.str()".
- Dans les constructeurs, j'utiliserais la liste d'initialisation plutôt que d'affecter les valeurs dans le corps du constructeurs. Ça te permettrait d'avoir des attributs constants.
- Au lieu de addAction(T val), je mettrais plutôt: addAction(const T& val), sinon bonjour les copies en cas de gros objet ! C'est valable pour les autres aussi.
- J'éviterais les NULL au profit de 0, voir: http://0217021.free.fr/portfolio/axel.berardino/articles/null-en-cpp

Au niveau du binaire de démonstration:
- Le makefile est un faux makefile :p. Il ne tient pas compte de ce qui est déjà compilé.
- Tu compiles les *.cpp alors qu'ils sont inclus par les .hpp. Donc théoriquement, ta ligne de compil devrait être: "g++ main.cpp -o CLI"
- Tu ne mets pas de flags de sécurité: "g++ -W -Wall -Wabi -g main.cpp", tu verrais pas mal de warnings ! (Tu compares souvent des entiers signés avec des entiers non signés).
- En release un -o3 serait pas mal non plus :)
- D'après valgrind, tu as des fuites mémoires dans TURS.cpp, ligne 20 et ligne 43. Je t'invite à repasser du valgrind sur ton code.
- Au lieu de faire "char line[]", tu peux mettre un "std::string line", et faire un "while (std::getline(std::cin, line)"
- Ton binaire devrait facultativement prendre en argument un fichier. Pratique pour débugger des fuites mémoires, par exemple :p

J'ai sûrement oublié des choses. Je les ajouterais peut être plus tard.
macsou01 Messages postés 45 Date d'inscription mardi 20 mars 2007 Statut Membre Dernière intervention 28 juillet 2011
23 juin 2011 à 18:49
Merci pour toutes ces remarques intéressantes !

En effet par exemple pour la makefile j'avais bien remarqué que ça compilait même sans avoir modifié les fichiers, mais je ne savais pas d'où venait le problème.

Je vais essayer modifier ça rapidement.

Encore merci :)
cptpingu Messages postés 3836 Date d'inscription dimanche 12 décembre 2004 Statut Modérateur Dernière intervention 11 février 2023 124
23 juin 2011 à 19:00
Pour le makefile, regarde dans mes sources. Tu trouveras un exemple de makefile + configure complet (avec gestion des dépendance via g++ -MM). C'est peut être un peu "overkill" pour ce que tu veux faire, mais ça peut toujours t'intéresser :p
macsou01 Messages postés 45 Date d'inscription mardi 20 mars 2007 Statut Membre Dernière intervention 28 juillet 2011
23 juin 2011 à 20:12
J'ai modifié un peu mon code selon tes conseils. Cependant, il me reste les points suivants à effectuer :
- Gérer les sources suivant le modèle .cpp/.hh/.hpp (je n'ai pas bien compris comment faire et sur internet il n'y a pas beaucoup d'exemples clairs)
- Modifier le makefile de l'exemple pour qu'il ne compile pas à chaque fois (idem, j'ai regardé ton makefile (pour le compilateur Pascal) mais comme c'est pour un gros projet, il est un peu complexe, il faudra que j'y regarde de plus près)
- Vérifier les fuites mémoires (je verrai ça plus tard)
cptpingu Messages postés 3836 Date d'inscription dimanche 12 décembre 2004 Statut Modérateur Dernière intervention 11 février 2023 124
23 juin 2011 à 23:32
> - Gérer les sources suivant le modèle .cpp/.hh/.hpp (je n'ai pas bien compris comment faire et sur internet il n'y a pas beaucoup d'exemples clairs)

C'est pas dur:
- Dans le .hh tu mets la définition de la classe.
- Dans le .cpp le code
- Dans le .hpp (qui est inclus à la fin du .hh) tu mets le code inliné ou templaté.
En fait, c'est juste un souci de nomination que tu as, pas de principe.

> Modifier le makefile de l'exemple pour qu'il ne compile pas à chaque fois (idem, j'ai regardé ton makefile (pour le compilateur Pascal) mais comme c'est pour un gros projet, il est un peu complexe, il faudra que j'y regarde de plus près)
Attention, par convention, "Makefile" prend une majuscule.
Soit tu mets -O3 (avec une majuscule à O) qui veut dire optim maximale, soit tu mets -g (qui veut dire, pas d'optim, ajouter des informations de debug). Les deux ne sont pas compatibles (et sont carrément à l'opposé).

Un Makefile tout mini, adapté à ton projet:

# Liste des fichier cpp, ne jamais mettre *.cpp
SRC=main.cpp
# Liste des headers, ils devraient être en .hpp et .hxx
HEADER= ../../../TURS/src/TURS.cpp \
../../../TURS/src/TURS.hpp \
../../../TURS/src/TURSNode.cpp \
../../../TURS/src/TURSNode.hpp
BINDIR=.
ifdef SystemRoot
BINNAME=CLI.exe
else
BINNAME=CLI
endif

TARGET=$(BINDIR)/$(BINNAME)
OBJ=$(SRC:.cpp=.o)
CPP=g++
CXXFLAGS=-W -Wall -Wabi -O3

all: $(TARGET)

$(TARGET): $(OBJ) $(HEADER)
$(CPP) $(CXXFLAGS) $(OBJ) -o $(TARGET)

%.o: %.cpp
$(CPP) -o $@ -c $< $(CFLAGS)
clean:
rm -rf *.o

distclean: clean
rm -rf $(TARGET)

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.