A l'étape du billet précédant [step18] nous avons mis en place les fonctionnalité de suppression de state, d'actions et connections dans la page Graphics GEF. A ce stade nous avons des outils de la palette GEF et des actions GEF qui permettent de mettre à jour le modèle EMF via des Command GEF. Plus exactement ce sont les EditPolciy installés sur chacun des EditPart GEF qui réagissent aux outils/actions et qui interprètent les Request GEF en Command GEF. Les EditPolicy pourrait mettre à jour directement le modlèle EMF sans passer par des Command GEF. Pourquoi utiliser des Command GEF? L'editor GEF GaaphicalEditor gère en interne une stack de Command GEF (GraphicalEditor#getEditDomain()#getCommandStack()) qui dépile/empile les Command GEF executées dans l'editor via les outils des palettes et les Actions GEF.
Cette stack de Command GEF permet de gérer le undo/redo dans l'editor GEF en appelant pour chaque action GEF UndoAction/RedoAction la méthode Command#undo()/redo() de la dernière Command GEF exécutée. A ce stade nous n'avons pas implémentées ces 2 méthodes dans nos Command GEF. Dans ce billet nous allons les implémenter pour gérer le undo/redo :
Les fonctionnalités undo/redo seront accéssibles via :
Vous pouvez télécharger le projet org.example.workflow_step19.zip présenté dans ce billet.
GEF installe par défaut les actions org.eclipse.gef.ui.actions.UndoAction et org.eclipse.gef.ui.actions.RedoAction dans son registry d'actions, qui permet d'appeler via la CommandStack de l'editor, les méthodes undo/redo de la dernière Command GEF exécutée. Voici le code de la méthode UndoAction#run() :
public void run() {
getCommandStack().undo();
}
Et voici la méthode CommandStack#undo() :
public void undo() {
//Assert.isTrue(canUndo());
Command command = (Command)undoable.pop();
notifyListeners(command, PRE_UNDO);
try {
command.undo();
redoable.push(command);
notifyListeners();
} finally {
notifyListeners(command, POST_UNDO);
}
}
Avant d'implémenter les méthodes undo/redo de chacune de nos Command GEF, nous devons appeler les méthodes UndoAction#run() et RedoAction#run() par :
Modifiez le code de la méthode WorkflowContextMenuProvider#buildContextMenu(IMenuManager menu) de la classe org.example.workflow.presentation.graphical.actions.WorkflowContextMenuProvider comme suit :
public void buildContextMenu(IMenuManager menu) {
GEFActionConstants.addStandardActionGroups(menu);
IAction action = getActionRegistry().getAction( ActionFactory.UNDO.getId());
menu.appendToGroup(GEFActionConstants.GROUP_UNDO, action);
action = getActionRegistry().getAction(ActionFactory.REDO.getId());
menu.appendToGroup(GEFActionConstants.GROUP_UNDO, action);
action = getActionRegistry().getAction(ActionFactory.DELETE.getId());
if (action.isEnabled())
menu.appendToGroup(GEFActionConstants.GROUP_EDIT, action);
}
Ici nous ajoutons l'action UndoAction dans le menu contextuel de la page GEF Graphics via le code :
IAction action = getActionRegistry().getAction(ActionFactory.UNDO.getId());
menu.appendToGroup(GEFActionConstants.GROUP_UNDO, action);
Dans les exemples GEF la constante GEFActionConstants.UNDO est utilisée mais celle-ci est marquée depracated et conseille d'utiliser la constante ActionFactory.UNDO.getId().
Relancez le plugin et vous devez voir apparaître les 2 actions (désactivées) "Undo" et "Redo" dans le menu contextuel :

Pour lier les entrées de menu "Undo" et "Redo" appartenant au menu globale "Edit", nous allons procéder de la même manière que l'action DeleteAction. Pour cela modifiez la méthode GraphicalWorkkflowActionBarContributor#buildActions() de la classe org.example.workflow.presentation.graphical.actions.GraphicalWorkkflowActionBarContributor comme suit :
protected void buildActions() {
addRetargetAction(new UndoRetargetAction());
addRetargetAction(new RedoRetargetAction());
addRetargetAction(new DeleteRetargetAction());
}
Pour bénéficier du "Ctrl+Z" et "Ctrl+Y" nous n'avons rien à faire car nous avons lier les entrées de menu "Undo" et "Redo" appartenant au menu globale "Edit" qui sont elle-même lié à ces combinaisons de touches.
A ce stade les appels de CommandStack#undo() et CommandStack#redo() via le menu contextuel, le menu globale ou "Ctrl+Z", "Ctrl+Y" est opérationnel. Nous pouvons maintenant implémenter les méthodes Command#undo() / Command#redo() pour chacune de nos Command GEF. Par défaut la classe de base org.eclipse.gef.commands.Command implémente la méthode Command#redo() comme ceci :
public void redo() {
execute();
}
autrement dit elle appelle la méthode Command#execute(), ce qui dans la plupart des cas fonctionne très bien et évite à la classe qui étend org.eclipse.gef.commands.Command d'implémenter la méthode Command#redo(). La méthode Command#undo() est implémentée comme ceci :
public void undo() { }
}
autrement dit elle ne fait rien.
Pour rappel notre classe org.example.workflow.presentation.graphical.commands.CreateCommand s'occupe d'ajouter un state/action aux workflow via sa méthode CreateCommand#execute() :
@Override
public void execute() {
if (getConnectableNode() instanceof StateType) {
getWorkflow().getState().add((StateType)getConnectableNode());
}
else {
if (getConnectableNode() instanceof ActionType) {
getWorkflow().getAction().add((ActionType)getConnectableNode());
}
}
}
La méthode CreateCommand#undo() doit effectuer son inverse, autrement dit supprimer le state/action qui a été ajouté au workflow. Pour cela implémentez la méthode CreateCommand#undo() comme ceci :
@Override
public void undo() {
if (getConnectableNode() instanceof StateType) {
getWorkflow().getState().remove((StateType)getConnectableNode());
}
else {
if (getConnectableNode() instanceof ActionType) {
getWorkflow().getAction().remove((ActionType)getConnectableNode());
}
}
}
Ici nous n'avons pas besoin d'implémenter la méthode CreateCommand#redo() car elle est identique à CreateCommand#execute().
Relancez le plugin, ajoutez un state via l'ouil de création de state puis cliquez sur le menu contextuel, l'item "Undo" doit être activé :

Cliquez sur l'item "Undo" pour annuler le state ajouté. Apuyez sur "Ctrl+Y" pour effectuer un Redo par le clavier, le state doit ré-apparaître (ceci permet de vérifier que le menu global est bien opérationnel).
Pour rappel notre classe org.example.workflow.presentation.graphical.commands.ConnectionCreateCommand s'occupe de créer une connection entre un state et une action via sa méthode ConnectionCreateCommand#execute() :
@Override
public void execute() {
if (source instanceof StateType) {
StateType state = (StateType) source;
ActionType action = (ActionType) target;
action.setFromState(state);
} else {
StateType state = (StateType) target;
ActionType action = (ActionType) source;
action.setToState(state);
}
}
La méthode ConnectionCreateCommand#undo() doit effectuer son inverse, autrement dit supprimer la connection. Pour cela implémentez la méthode ConnectionCreateCommand#undo() comme ceci :
@Override
public void undo() {
if (source instanceof StateType) {
ActionType action = (ActionType) target;
action.setFromState(null);
} else {
ActionType action = (ActionType) source;
action.setToState(null);
}
}
Ici nous n'avons pas besoin d'implémenter la méthode ConnectionCreateCommand#redo() car elle est identique à ConnectionCreateCommand#execute.
Relancez le plugin, liez un state et un action à l'aide de l'outil de création de connection puis testez que le undo/redo fonctionne correctement.
Pour rappel notre classe org.example.workflow.presentation.graphical.commands.DeleteCommand s'occupe de supprimer le state/action selectionné via sa méthode DeleteCommand#execute() :
@Override
public void execute() {
getChildren().remove(getConnectableNode());
}
La méthode DeleteCommand#undo() doit effectuer son inverse, autrement dit ajouter le state/action qui a été supprimé du workflow. Pour cela implémentez la méthode DeleteCommand#undo() comme ceci :
@Override
public void undo() {
getChildren().add(getConnectableNode());
}
Mias ceci ne suffit pas. En effet ce code fonctionne très bien pour les state/action qui ne sont pas liés par des connections. Dans le cas contraire nous perdons les informations de connections.
Pour gérer le undo correctement, il faut restaurer les connections existantes. Pour cela modifiez le code de DeleteCommand comme suit :
package org.example.workflow.presentation.graphical.commands;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.gef.commands.Command;
import org.example.workflow.model.ActionType;
import org.example.workflow.model.ConnectableNode;
import org.example.workflow.model.Connection;
import org.example.workflow.model.StateType;
import org.example.workflow.model.WorkflowType;
import org.example.workflow.model.util.ConnectionUtils;
public class DeleteCommand extends Command {
private WorkflowType workflow;
private ConnectableNode connectableNode;
private List<Connection> sourceConnections = new ArrayList<Connection>();
private List<Connection> targetConnections = new ArrayList<Connection>();
@Override
public void execute() {
getChildren().remove(getConnectableNode());
}
@Override
public void undo() {
getChildren().add(getConnectableNode());
for (Connection connection : sourceConnections) {
ConnectableNode source = connection.getSource();
ConnectableNode target = connection.getTarget();
ConnectionUtils.connect(source, target);
}
for (Connection connection : targetConnections) {
ConnectableNode source = connection.getSource();
ConnectableNode target = connection.getTarget();
ConnectionUtils.connect(source, target);
}
}
public WorkflowType getWorkflow() {
return workflow;
}
public void setWorkflow(WorkflowType workflow) {
this.workflow = workflow;
}
public ConnectableNode getConnectableNode() {
return connectableNode;
}
public void setConnectableNode(ConnectableNode connectableNode) {
this.connectableNode = connectableNode;
sourceConnections.addAll(connectableNode.getSourceConnections());
targetConnections.addAll(connectableNode.getTargetConnections());
}
protected List getChildren() {
if (connectableNode instanceof StateType)
return workflow.getState();
if (connectableNode instanceof ActionType)
return workflow.getAction();
return null;
}
}
Dans la méthode DeleteCommand#setConnectableNode(ConnectableNode connectableNode) on stocke les connections initialies sources et targets du node :
public void setConnectableNode(ConnectableNode connectableNode) {
this.connectableNode = connectableNode;
sourceConnections.addAll(connectableNode.getSourceConnections());
targetConnections.addAll(connectableNode.getTargetConnections());
}
Puis dans la méthode DeleteCommand#undo() on reconnecte les node en utilisant les connections source et target initialie :
@Override
public void undo() {
getChildren().add(getConnectableNode());
for (Connection connection : sourceConnections) {
ConnectableNode source = connection.getSource();
ConnectableNode target = connection.getTarget();
ConnectionUtils.connect(source, target);
}
for (Connection connection : targetConnections) {
ConnectableNode source = connection.getSource();
ConnectableNode target = connection.getTarget();
ConnectionUtils.connect(source, target);
}
}
Relancez le plugin et supprimez l'action a1 qui est lié aux states s1 et s2, puis effectuez un "Ctrl+Z", l'action a1 doit être à nouveau lié à s1 et s2.
Pour rappel notre classe org.example.workflow.presentation.graphical.commands.DeleteConnectionCommand s'occupe de supprimer une connection entre un state et une action via sa méthode DeleteConnectionCommand#execute() :
@Override
public void execute() {
if (source instanceof StateType) {
StateType state = (StateType) source;
ActionType action = (ActionType) target;
action.setFromState(state);
} else {
StateType state = (StateType) target;
ActionType action = (ActionType) source;
action.setToState(state);
}
}
La méthode DeleteConnectionCommand#undo() doit effectuer son inverse, autrement dit remettre à jour la connection. Pour cela implémentez la méthode DeleteConnectionCommand#undo() comme ceci :
@Override
public void undo() {
if (connection.getSource() instanceof ActionType) {
ActionType action = (ActionType) connection.getSource();
StateType state = (StateType)connection.getTarget();
action.setToState(state);
} else {
if (connection.getTarget() instanceof ActionType) {
ActionType action = (ActionType) connection.getTarget();
StateType state = (StateType)connection.getSource();
action.setFromState(state);
}
}
}
Relancez le plugin, supprimez un lien entre un state et une action à l'aide de la touche "Suppr", puis testez que le undo/redo fonctionne correctement.
Ici nous avons mis en place les fonctionnalités de Undo/Redo dans la page GEF Graphics. Les Command GEF permettent de gérer le Undo/Redo via la stack de Command. On ne modifie jamais le modèle directement on passe toujours par une Command. EMF.Edit utilise d'ailleurs le même principe. Cependant nous sommes dans un contexte de multi page et le Undo/Redo fonctionne très bien lorsque l'on reste sur une page (ex : on modifie le modèle EMF workflow via la page Graphics GEF, puis on effectue un Undo), mais pas lorsque l'on modifie une page (ex : page Graphics GEF) puis que l'on clique sur une autre page (ex : page Source ou Selection) et que l'on tente d'effectuer un Undo. Ceci s'explique par le fait que chaque page (Selection, Graphics GEF, Source XML) a sa propre stack de command. De plus les Command sont de diiférentes natures :
Pour régler ce problème, il faudrait que notre editor multi page partage la même stack de command. Mais la difficulté est que les Command sont de différentes natures. Aujourd'hui je n'ai pas encore étudié cette problématique, mais d'après ce que j'ai pu voir dans JSF Webools, il transforme toutes les Command EMF en Command GEF. Dans leur cas seul la page Source génère des Command EMF :
la classe org.eclipse.jst.jsf.facesconfig.ui.pageflow.command.EMFCommandStackGEFAdapter qui étent la classe GEF org.eclipse.gef.commands.CommandStack s'occupe de transformer les Command EMF en Command GEF via la classe org.eclipse.jst.jsf.facesconfig.ui.pageflow.command.EMFCommandGEFAdapter. Par exemple elle implémente la méthode GEF org.eclipse.gef.commands.CommandStack#getUndoCommand() comme ceci :
public Command getUndoCommand() {
if (emfCommandStack == null || emfCommandStack.getUndoCommand() == null) {
return null;
}
return new EMFCommandGEFAdapter(emfCommandStack.getUndoCommand());
}
où emfCommandStack est la stack de command EMF du modèle SSE récupérée comme ceci :
model = StructuredModelManager.getModelManager().getExistingModelForEdit(doc);
emfCommandStack = ((BasicCommandStack) this.model.getUndoManager().getCommandStack());
Un listener est ajouté à la commande de stack EMF :
emfCommandStack.addCommandStackListener(this);
pour indiquer à la commande de stack GEF EMFCommandStackGEFAdapter que celle-ci change (ajout d'une nouvelle commande GEF) et qu'elle notifie tous ses listeners GEF org.eclipse.gef.commands.CommandStackEvent qu'il y a un changement dans la stack :
public void commandStackChanged(EventObject event) {
this.notifyListeners();
}
la classe org.eclipse.jst.jsf.facesconfig.ui.pageflow.command.EMFCommandGEFAdapter étend la classe GEF org.eclipse.gef.commands.Command et attend dans son constructeur une Command EMF
org.eclipse.emf.common.command.Command. Par exemple elle implémente la méthode GEF org.eclipse.gef.commands.Command#undo() comme ceci :
public void undo() {
if (emfCommand == null) {
return;
}
emfCommand.undo();
}
où emfCommand est la Command EMF passé dans le constructeur de EMFCommandGEFAdapter.
L'editor multi page FacesConfigEditor travaille toujours avec une stack de Command de type GEF. Si la page activée est celle de GEF, c'est celle de GEF qui est utilisé, si c'est une autre page, c'est EMFCommandStackGEFAdapter qui est utilisé. FacesConfigEditor utilise la classe org.eclipse.jst.jsf.facesconfig.ui.pageflow.command.DelegatingCommandStack qui maintient la stack de Command GEF de la page active. L'état dirty qui doit se baser sur la stack de command GEF de la page active est gérée par le listener GEF org.eclipse.jst.jsf.facesconfig.ui.FacesConfigEditor.MultiPageCommandStackListener qui implémente l'interface GEF org.eclipse.gef.commands.CommandStackListener.
Je pense qu'il est plus difficile (voir impossible) de ne travailler qu'avec des Command EMF, autrement dit transformer les Command GEF en Command EMF car une Command GEF est une classe alors qu'une Command EMF est une interface.
Vous devez être identifié pour poster un commentaire.
| Lun | Mar | Mer | Jeu | Ven | Sam | Dim |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 |
Copyright © 2000-2012 - www.developpez.com