Gestion du undo / redo

UniXUniX Membre
05:49 modifié dans API AppKit #1
Salut,

Je vais implémenter le undo / redo dans mon appli.
J'ai donc consulté la doc, et j'ai à  peu près pigé le fonctionnement.

Maintenant j'ai quelques questions concernant la meilleure facon d'opérer.
Un exemple concret : j'ai un objet que je modifie au travers d'une fenêtre. Pour donner la possibilite d'annuler les modifications, je peux faireune copie de l'objet avant la modif, et lors de l'annulation, remplacer l'objet non modifié par l'objet sauvegardé...
Mais au niveau du stockage, comment faire ? J'a envisagé un tableau contenant tous ces objets et qui serait un sorte de "pile" d'objets undo ..... Ai je la bonne approche ?
Il va se poser la question de l'occuption mémoire, a force d'empiler des objets qui ne serviront peut être à  rien ...

Comment vous procédez vous, en dehors des cas simples ou une simple application de méthode peut suffire ?

Réponses

  • elfelf Membre
    05:49 modifié #2
    A moins que je me plente complètement, c'est très simple.

    Il suffit NSCoder dans ton NSDocument, et de faire du setup basic du undo-manager.
  • UniXUniX Membre
    05:49 modifié #3
    Bon, je pense que j'ai pas été trop clair ...... :-\\

    Tout d'abord une petite précision, mon appli ne fonctionne pas avec NSDocument.

    Pour essayer d'éclaircir un peu, je vais prendre un exemple.
    J'ai un objet voiture, cet objet a des variables d'instance telles que marque, modèle, photo, prix, ...
    Dans mon appli, j'ai une fenêtre qui me permet de modifier mon objet voiture. Lorsque la modification est terminée, la fenêtre se referme, et j'ajoute au undo manager une entrée du type "Annuler modification voiture".

    A cette entrée, il faut lui associer une méthode qui va permettre de faire l'opération inverse. Pour ces cas ou l'on annule une modification profonde d'un objet (on a par exemple changé la photo, la marque, l'usure des pneus, le niveau sonore ....), quelle façon de faire utilisez vous ?

    Pour moi, il y en a 2 :
    1) on a fait une copie de l'objet original, pour pouvoir remettre l'objet modifié tel que l'original
    2) on remplace carrement l'objet modifié par son original

  • schlumschlum Membre
    février 2007 modifié #4
    dans 1170192978:

    Pour moi, il y en a 2 :
    1) on a fait une copie de l'objet original, pour pouvoir remettre l'objet modifié tel que l'original
    2) on remplace carrement l'objet modifié par son original

    Il vaut mieux stocker des modifications atomique afin d'économiser de la mémoire...
    Par exemple, ton objet a des accesseurs j'imagine ; tu as parlé de photo ; prenons "- (NSImage*)photo" et "- (void)setPhoto:(NSImage*)newPhoto" (j'imagine hein, tu n'as pas forcément exactement les mêmes fonctions)

    Ce que tu vas faire, c'est enregistrer dans "setPhoto" l'action :
    [undoManager registerUndoWithTarget:self selector:@selector(setPhoto:) object:photo];
    

    ("photo" est l'ancien objet photo)

    Après, il faut écouter les notifications du UndoManager pour savoir quand rafraà®chir ton interface...
  • elfelf Membre
    05:49 modifié #5
    Oh. Je suis shocké:
    Par exemple, ton objet a des accesseurs j'imagine ; tu as parlé de photo ; prenons "- (NSImage*)getPhoto" et "- (void)setPhoto:(NSImage*)newPhoto" (j'imagine hein, tu n'as pas forcément exactement les mêmes fonctions)


    Selon les guidlines d'Objective-C les getTruc: c'est sensé être un -(void)getTruc:(NSImage**)imageAdress ou l'argument est une adresse et que la méthode écrit la valeur dans l'adresse fournie. Pour un acceseur qui retourne la valeur ça serais plutôt -(NSImage*)truc.

    Si j'ai bien tout pigé bien sûr...

    Allez j'arrète de faire mon troll et je vais jouer à  warcraft...
  • schlumschlum Membre
    05:49 modifié #6
    dans 1171310016:

    Oh. Je suis shocké:
    Par exemple, ton objet a des accesseurs j'imagine ; tu as parlé de photo ; prenons "- (NSImage*)getPhoto" et "- (void)setPhoto:(NSImage*)newPhoto" (j'imagine hein, tu n'as pas forcément exactement les mêmes fonctions)


    Selon les guidlines d'Objective-C les getTruc: c'est sensé être un -(void)getTruc:(NSImage**)imageAdress ou l'argument est une adresse et que la méthode écrit la valeur dans l'adresse fournie. Pour un acceseur qui retourne la valeur ça serais plutôt -(NSImage*)truc.

    Si j'ai bien tout pigé bien sûr...

    Allez j'arrète de faire mon troll et je vais jouer à  warcraft...

    Tu as entièrement raison, j'ai corrigé  :)
    (c'est que je suis dans le C++ au boulot en ce moment...)
  • UniXUniX Membre
    05:49 modifié #7
    Allez j'arrète de faire mon troll et je vais jouer à  warcraft...
     :)

    Bon, justement, j'étais plongé dans ce sujet cet après-midi, et je crois que j'avance sur la compréhension du sujet.

    Il y a quand même un truc que je pige pas .... et qui est l'essentiel du sujet pourtant ..... D'ou sort le Undo Manager ?
    J'ai pigé pour les vues, pour les NSDocument, mais je vois pas comment faire pour mon appli. Dans mon cas, j'ai un objet principal AppController qui contient la plupart de mes IBActions, mon NIB principal (avec la barre de menus) ...

    Moi, je me dis que pour faire un Undo sur une action exécutée par exemple par une méthode IBAction de mon AppController, il faut que je place le registerUndoWithTarget:selector:object: là  dedans. Mais comment avoir le NSUndoManager dans ce cas là  ?
  • schlumschlum Membre
    05:49 modifié #8
    Plusieurs objets ont des NSUndoManager :
    - NSDocument
    - NSResponder
    - NSManagedObjectContext
    - WebView
    - NSTextView
    (j'en oublie peut-être...)
    Tu peux également gérer toi même ton NSUndoManager...
  • UniXUniX Membre
    05:49 modifié #9
    J'en déduis donc, qu'il faut que je gère un NSUndoManager pour mon AppController, et qu'ensuite, je ferais tous mes appels à  cet objet ?

  • schlumschlum Membre
    05:49 modifié #10
    dans 1171313360:

    J'en déduis donc, qu'il faut que je gère un NSUndoManager pour mon AppController, et qu'ensuite, je ferais tous mes appels à  cet objet ?



    C'est effectivement une solution, si ton application n'est pas "NSDocument Based"  ;)

    Après, pour la gestion du menu "undo" et "redo", je te conseille d'utiliser ça :
    - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
    

    Tu l'implémentes dans le contrôleur auquel est connecté ton menu et tu fais un truc du genre :
    - (BOOL)validateMenuItem:(id&lt;NSMenuItem&gt;)menuItem<br />{<br />	SEL sel = [menuItem action];<br />	if(sel==@selector(undo:))<br />		return [undoManager canUndo];<br />	else if(sel==@selector(redo:))<br />		return [undoManager canRedo];<br />	// [...]<br />	else<br />		return YES;<br />}
    
  • UniXUniX Membre
    05:49 modifié #11
    Bon, ça y est, j'ai enfin réussi à  faire mon premier Undo ..... :adios!:

    Mais en fait, je suis pas sur d'avoir tout pigé ...
    Bon, là  j'ai défini le NSUndoManager en utilisant celui d'une classe dérivant de NSResponder, à  savoir ma fenêtre principale (NSWindow). Par contre, j'ai pas pigé, si on pouvait définir un NSUndoManager pour un objet ne dérivant pas de NSResponder ...

    La doc, dit que la responder chaine est parcourue jusqu'à  trouver un objet qui renvoie un undo manager, et ensuite la NSWindow (ce que j'ai utilisé), mais ensuite est envoyé un massage au delegate de la window pour voir s'il peut retourner un undo manager. Dans mon cas, mon instance de AppController est delegate de la fenêtre principale. Si j'implémente la méthode windowWillReturnUndoManager:, je fais comment pour renvoyer un undo manager ?


    PS : merci pour l'astuce pour afficher ou non les menu undo et redo.
  • schlumschlum Membre
    février 2007 modifié #12
    dans 1171404658:

    Bon, ça y est, j'ai enfin réussi à  faire mon premier Undo ..... :adios!:

    Mais en fait, je suis pas sur d'avoir tout pigé ...
    Bon, là  j'ai défini le NSUndoManager en utilisant celui d'une classe dérivant de NSResponder, à  savoir ma fenêtre principale (NSWindow). Par contre, j'ai pas pigé, si on pouvait définir un NSUndoManager pour un objet ne dérivant pas de NSResponder ...

    Les objets NSResponder ont déjà  un "undoManager"  ;)
    Donc si tu décides d'utiliser celui de la fenêtre, ce n'est pas la peine de le définir...

    La doc, dit que la responder chaine est parcourue jusqu'à  trouver un objet qui renvoie un undo manager, et ensuite la NSWindow (ce que j'ai utilisé), mais ensuite est envoyé un massage au delegate de la window pour voir s'il peut retourner un undo manager. Dans mon cas, mon instance de AppController est delegate de la fenêtre principale. Si j'implémente la méthode windowWillReturnUndoManager:, je fais comment pour renvoyer un undo manager ?

    - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender
    

    J'imagine...
    Ben je n'ai jamais utilisé le NSUndoManager d'une fenêtre, mais j'imagine que c'est "return [sender undoManager]".

    [Edit] En fait, n'implémente pas ça si tu utilises le NSUndoManager d'une fenêtre, car sinon il ne sera pas créé automatiquement :
    If this method is not implemented, the NSWindow object creates an NSUndoManager object for the window.


    Enfin... Moi je préfèrerais ne pas mêler la fenêtre à  tout ça et reconnecter mes menus "undo" et "redo" à  un contrôleur contenant le NSUndoManager...  ;)
  • UniXUniX Membre
    05:49 modifié #13
    En fait, on s'est pas bien compris ... :o
    dans 1171434769:
    Les objets NSResponder ont déjà  un "undoManager"  ;)
    Donc si tu décides d'utiliser celui de la fenêtre, ce n'est pas la peine de le définir...

    Oui, je me suis mal exprimé, au lieu de dire définir, j'aurais du dire utiliser.

    Enfin... Moi je préfèrerais ne pas mêler la fenêtre à  tout ça et reconnecter mes menus "undo" et "redo" à  un contrôleur contenant le NSUndoManager...  ;)

    On est d'accord ! Et c'est bien là  l'objet de ma question ! Pour l'instant, je n'ai réussi à  implémenter le undo qu'en utilisant un undo manager d'un objet de la responder chain, à  savoir la fenêtre. Mais c'est par dépit ! Je préfèrerais en avoir un défini dans mon AppController, mais je ne pige pas comment je peux lui en définir un justement !!!
  • schlumschlum Membre
    février 2007 modifié #14
    dans 1171436949:

    On est d'accord ! Et c'est bien là  l'objet de ma question ! Pour l'instant, je n'ai réussi à  implémenter le undo qu'en utilisant un undo manager d'un objet de la responder chain, à  savoir la fenêtre. Mais c'est par dépit ! Je préfèrerais en avoir un défini dans mon AppController, mais je ne pige pas comment je peux lui en définir un justement !!!


    Tout simplement en le déclarant comme variable de classe  ;)
    Tu l'initialises dans le constructeur, tu le détruits dans le destructeur, et tu définis la méthode "undo" en appelant "if([myUM canUndo]) [myUM undo];" (pareil pour "redo"), puis tu connectes les menus à  ces nouvelles méthodes...

    @interface Controller : NSObject<br />{<br />	// [...]<br />	NSUndoManager *undoManager;<br />}<br /><br />// [...];<br />- (IBAction)undo:(id)sender;<br />- (IBAction)redo:(id)sender;<br /><br />@end<br /><br />@implementation Controller<br /><br />- (id)init<br />{<br />	if (self=[super init]) {<br />		// [...]<br />		undoManager = [[NSUndoManager alloc] init];<br />	}<br />	return self;<br />}<br /><br />- (void)dealloc<br />{<br />	// [...]<br />	[undoManager release];<br />	[super dealloc];<br />}<br /><br />- (IBAction)undo:(id)sender<br />{<br />	if([undoManager canUndo])<br />		[undoManager undo];<br />}<br /><br />- (IBAction)redo:(id)sender<br />{<br />	if([undoManager canRedo])<br />		[undoManager redo];<br />}<br /><br />- (BOOL)validateMenuItem:(id&lt;NSMenuItem&gt;)menuItem<br />{<br />	SEL sel = [menuItem action];<br />	if(sel==@selector(undo:))<br />		return [undoManager canUndo];<br />	else if(sel==@selector(redo:))<br />		return [undoManager canRedo];<br />	// [...]<br />	else<br />		return YES;<br />}<br /><br />// Accesseur (pour que les autres objets puissent ajouter leurs actions &quot;undo / redo&quot;)<br />- (NSUndoManager*)undoManager<br />{<br />	return undoManager;<br />}<br /><br />@end
    
  • UniXUniX Membre
    05:49 modifié #15
    Aaaaaaaahhhhhhhhhhhh ........

    Y'a des fois, j'ai vraiment l'impression d'être con .....
    Je cherche des trucs tellement compliqués, que je ne vois même pas les choses évidentes ....!

    Merci du coup de pied aux fesses Schlum !
  • UniXUniX Membre
    05:49 modifié #16
    Bon, je reviens sur cette affaire de undo ...

    J'arrive maintenant à  réaliser des undo sur certaines opérations (j'ai laissé tomber le redo ...).
    Par exemple les ajouts d'objets (ajouter ceci ou cela), ça marche nickel.

    Il me reste un problème, c'est pour les objets dont je modifie les variables d'instances.
    J'ai un objet simple Commentaire qui contient une variable Texte.

    Dans la méthode ou je valide la modification du texte d'un commentaire, j'ai ajouté :
    NSString *texteSauve = [annotationModifiee texte];<br />// mise a jour du texte de annotationModifiee<br />[[[controlleur undoManager]prepareWithInvocationTarget:annotationModifiee]setTexte:texteSauve];<br />[[controlleur undoManager]setActionName:@&quot;Modifier annotation&quot;];
    


    Le problème, c'est que si je fais pas de retain sur texteSauve, lorsque je mets à  jour le texte de annotationModifiee, je le release, et lors du undo, ça plante .... normal.
    Mais si je fais un retain, et que je ne fais jamais de undo, je garde a vitam eternam texteSauve en mémoire ?

    C'est bien comme cela qu'il faut procéder ?
  • schlumschlum Membre
    05:49 modifié #17
    Tu pourrais dire qu'on t'a répondu sur MB afin d'éviter qu'on te réponde la même chose ici...
  • UniXUniX Membre
    05:49 modifié #18
    C'est bon, j'ai mes explications, et j'y vois maintenant clair sur le undo ....
  • MattcadamMattcadam Membre
    05:49 modifié #19
    Bonjour à  tous,

    Je suis un petit nouveau en programmation Cocoa et pour ma première application je fait un truc très simple une NSTableView dans laquelle on peut ajouter des éléments et en enlever.
    Pour perfectionner ( :)) mon appli j'ai ajouté la gestion du undo/redo qui marche grace au conseil de Schlum. Cependant je n'arrive pas à  mettre le nom de l'opération dans le menu, exemple "Undo Ajout Opération" pourtant j'ai bien mis ça :
    <br />if([undoManager isUnDoing]) {<br />&nbsp; &nbsp; [undoManager setActionName:@&quot;Ajout Operation&quot;];<br />}<br />
    


    et quand je vérifie le nom associé à  l'action avec "undoActionName" j'ai bien "Ajout Operation".

    Qu'est ce que j'aurais pu oublier?

    Merci

    Matt
  • schlumschlum Membre
    05:49 modifié #20
    Mhh, le "setActionName", il faut le faire avant un "registerUndoWithTarget" ou un "beginUndoGrouping", non ?  ???
  • MattcadamMattcadam Membre
    05:49 modifié #21

    Je n'utilise pas ces deux fonctions mais "prepareWithInvocationTarget", En fait je fais une variante d'un exemple(RaiseMan) que j'ai trouvé dans le livre d'Aaron Hillegass mais avec Cocoa Application et non Document based application.

    Je vais mettre mon code ça sera peut être plus facile de comprendre ce que j'ai fait.

    #import &quot;AppController.h&quot;<br />#import &quot;Operation.h&quot;<br /><br />@implementation AppController<br /><br />#pragma mark init/dealloc<br />- (id) init<br />{<br />	[super init];<br />	undoManager=[[NSUndoManager alloc]init];<br />	tableOperations = [[NSMutableArray alloc]init];<br /><br />	return self;<br />}<br />-(void) dealloc<br />{<br />	[self setOperations:nil];<br />	[undoManager release];<br />	[super dealloc];<br />}<br /><br />-(void)setOperations:(NSMutableArray *)anArray<br />{<br />	if(anArray == tableOperations)<br />	{<br />		return;<br />	}<br />	[anArray retain];<br />	[tableOperations release];<br />	tableOperations = anArray;<br />}<br /><br />- (void) insertObject:(Operation *)ope inTableOperationsAtIndex:(int) index<br />{<br />	NSLog(@&quot;adding %@ to %@&quot;, ope, tableOperations);<br />	<br />	//Ajout de l&#39;operation inverse a undostack<br />	[[undoManager prepareWithInvocationTarget:self] removeObjectFromTableOperationsAtIndex:index];<br />	if(![undoManager isUndoing])<br />	{<br />		[undoManager setActionName:@&quot;Ajout Opération&quot;];<br />	}<br />	//Ajout de l&#39;operation au tableau d&#39;operation<br />	[tableOperations insertObject:ope atIndex:index];<br />}<br /><br />-(void) removeObjectFromTableOperationsAtIndex:(int)index<br />{<br />	Operation *ope = [tableOperations objectAtIndex:index];<br />	NSLog(@&quot;removing %@ from %@&quot;,ope, tableOperations);<br />	<br />	//Ajout de l&#39;inverse de cette operation a undostack<br /><br />	[[undoManager prepareWithInvocationTarget:self] insertObject:ope inTableOperationsAtIndex:index];<br />	<br />	if(![undoManager isUndoing])<br />	{<br />		[undoManager setActionName:@&quot;Suppression Opération&quot;];<br />	}<br />	<br />	[tableOperations removeObjectAtIndex:index];<br />}<br /><br /><br />#pragma mark Menu Undo/Redo<br /><br />-(IBAction) undo:(id)sender<br />{<br />	if([undoManager canUndo]){<br />		[undoManager undo];<br />		<br />	}<br />}<br /><br />-(IBAction) redo:(id)sender<br />{<br />	if([undoManager canRedo]){<br />		[undoManager redo];<br /><br />	}<br />}<br /><br />//Gestion du menu undo/redo pour qu&#39;il s&#39;affiche ou non en fonction de la taille de la undostack<br /><br />- (BOOL) validateMenuItem:(NSMenuItem *)menuItem<br />{<br />	SEL sel = [menuItem action];<br />	if(sel==@selector(undo:))<br />	{<br />		<br />		return [undoManager canUndo];<br />	}<br />	else if(sel==@selector(redo:))<br />		return [undoManager canRedo];<br />	else<br />		return YES;<br />}<br /><br /><br />@end<br />
    

  • schlumschlum Membre
    05:49 modifié #22
    Faut probablement le mettre avant les "prepare..."
  • MattcadamMattcadam Membre
    avril 2009 modifié #23
    Ben non ça change rien  :'( .
    Bon en même temps c'est pas LA fonctionnalité la plus importante.
    Si quelqu'un veut tester chez lui et voir si je n'ai pas fait d'erreurs autre part je mets mon code
  • CéroceCéroce Membre, Modérateur
    05:49 modifié #24
    Je crois savoir d'où vient ton problème.

    - Dans le cas d'une appli document-based, tu n'as pas besoin de créer un NSUndoManager, tu l'obtiens par -[NSDocument undoManager].

    - Dans ton cas, celui d'une appli sans doc, la question est "À qui va-t-on réclamer le NSUndoManager ?".
    La doc d'Apple précise que lorsqu'il y a besoin d'un Undo Manager, c'est à  la fenêtre qu'il va être réclamé, ou à  son délégué (méthode windowWillReturnUndoManager:).
    Relis les messages plus haut, le même problème était évoqué. À toi de trouver comment partager le undo manager entre le délégué de la fenêtre (où tu l'auras instancié) et  ton AppController. A priori, il semble peu possible de faire ça de façon propre !


    Par ailleurs, setActionName: s'applique à  l'action sur le haut de la pile des undos. Il faut donc l'appeler après -[NSUndoManager prepareWithInvocationTarget:].
  • MattcadamMattcadam Membre
    05:49 modifié #25
    Merci. Je vais essayer de voir ça au plus vite.


Connectez-vous ou Inscrivez-vous pour répondre.