Ajout de la Palette


La premiere chose a faire est, bien sur, d'ajouter la palette a notre editeur. Pour cela, nous allons changer la classe dont notre editeur (MyGraphicalEditor) herite : nous passons de GraphicalEditor a GraphicalEditorWithPalette (voir GraphicalEditorWithFlyoutPalette pour ceux qui preferent avoir une palette plus "interactive").
        public class MyGraphicalEditor extends GraphicalEditorWithPalette {
        // ...
        }

Ce nouvel heritage nous oblige a implementer la methode getPaletteRoot(). Celle-ci doit retourner un objet de type PaletteRoot, contenant l'arborescence de notre palette. Elle est, en general, composee de plusieurs groupes (PaletteGroup) que l'on peut separer graphiquement (PaletteSeparator), chacun de ses groupes contenants des entrees de differents types. Pour commencer, nous allons ajouter les outils de la palette les plus basiques que sont l'outil de selection et le marquee.
public class MyGraphicalEditor extends GraphicalEditorWithPalette {
                // ...
                @Override
                protected PaletteRoot getPaletteRoot() {
                        // Racine de la palette
                        PaletteRoot root = new PaletteRoot();
                       
                        // Creation d'un groupe (pour organiser un peu la palette)
                        PaletteGroup manipGroup = new PaletteGroup("Manipulation d'objets");
                        root.add(manipGroup);
                       
                        // Ajout de l'outil de selection et de l'outil de selection groupe
                        SelectionToolEntry selectionToolEntry = new SelectionToolEntry();
                        manipGroup.add(selectionToolEntry);
                        manipGroup.add(new MarqueeToolEntry());
                       
                        // Definition l'entree dans la palette qui sera utilise par defaut :
                        //      1.lors de la premiere ouverture de la palette
                        //      2.lorsqu'un element de la palette rend la main
                        root.setDefaultEntry(selectionToolEntry);
                        return root;
                }
        }

Si vous lancer l'application, vous obtiendrez un resultat comme suit:

L'outil appele marquee permet de selectionner plusieurs elements selon une region definie par l'utilisateur. C'est, notamment, pratique pour massivement deplacer, supprimer ou interagir avec un groupe d'entites (ici, les services ou les employes) sans avoir a utiliser la touche ctrl.

Ajout d'un service


Maintenant que nous avons la palette, il pourrait etre interessant de la remplir un peu et notamment d'ajouter la possiblite de creer un service graphiquement. Pour cela, nous allons d'abord creer une classe heritant de CreationFactory. Les factories sont des classes permettant selon le contexte de creer de nouveaux objets. L'avantage d'utiliser cette arborescence est que, comme nous le verrons un peu plus loin, l'appel a ses methodes est transparent et sera gere en interne par GEF.
Creeons donc notre factory, que l'on va appeler NodeCreationFactory (nom generique puisqu'on la modifiera pour qu'elle accueille la creation d'un employe un peu plus tard), heritant de CreationFactory:
        public class NodeCreationFactory implements CreationFactory
        {
                private Class<?> template;
               
                public NodeCreationFactory(Class<?> t) {
                        this.template = t;
                }
               
                @Override
                public Object getNewObject() {
                        if (template == null)
                                return null;
                        if (template == Service.class)
                        {
                                Service srv = new Service();
                                srv.setEtage(42);
                                srv.setName("Factorouf");
                                return srv;
                        }
                        return null;
                }
               
                @Override
                public Object getObjectType() {
                        return template;
                }
        }

Evidemment, libre a vous d'ameliorer la generation du nouvel objet, notamment au niveau du nom et de la gestion des etages (c'est le concept qui nous interesse plus que la finalite dans ce tuto). Maintenant que nous avons une classe qui nous permet de generer un objet presque from scratch, nous allons creer la commande qui va utilisee cette CreationFactory. Celle-ci doit memoriser les informations utiles (dans notre cas ici, le service nouvellement cre et l'entreprise a laquelle il appartient et a laquelle il va etre ajoute), notamment pour l'integration dans l'undo/redo (dans la command stack). Cette commande (qu'on va appelee ServiceCreateCommand) doit heritee de Command.
        public class ServiceCreateCommand  extends Command
        {
                private Entreprise en;
                private Service srv;
               
                public ServiceCreateCommand() {
                        super();
                        en = null;
                        srv = null;
                }
               
                public void setService(Object s) {
                        if (s instanceof Service)
                                this.srv = (Service)s;
                }
               
                public void setEntreprise(Object e) {
                        if (e instanceof Entreprise)
                                this.en = (Entreprise)e;
                }
               
                public void setLayout(Rectangle r) {
                        if (srv == null)
                                return;
                        srv.setLayout(r);
                }

                @Override
                public boolean canExecute() {
                        if (srv == null || en == null)
                                return false;
                        return true;
                }
               
                @Override
                public void execute() {
                        en.addChild(srv);
                }
               
                @Override
                public boolean canUndo() {
                        if (en == null || srv == null)
                                return false;
                        return en.contains(srv);
                }
               
                @Override
                public void undo() {
                        en.removeChild(srv);
                }
        }
Encore une fois, libre a vous de sophistiquer tout ca a votre bon vouloir, comme, par exemple, pour n'accepter la creation d'un nouveau service que si l'entreprise a plus de 10.000 de capital (ce qui est, evidemment, le cas de la societe Psykokwak :)). Treve de blagues car la, ca ne compile pas sans le bout de code qui suit, qui est a placer dans la classe Node et qui est une methode permettant de verifier la presence d'un Node dans son parent.
        public class Node implements IAdaptable
        {
                // ...
                public boolean contains(Node child) {
                        return children.contains(child);
                }
        }

Comme vous le savez maintenant, dans GEF, sans EditPolicy, la Commande n'est rien. Ce sur quoi nous devons nous concentrer maintenant, c'est integrer la creation de cette commande a notre EditPolicy preferee, dans notre cas AppEditLayoutPolicy.
        public class AppEditLayoutPolicy extends XYLayoutEditPolicy {
                // ... 
                protected Command getCreateCommand(CreateRequest request) {
                        if (request.getType() == REQ_CREATE && getHost() instanceof EntreprisePart) {
                                ServiceCreateCommand cmd = new ServiceCreateCommand();
                               
                                cmd.setEntreprise(getHost().getModel());
                                cmd.setService(request.getNewObject());
                               
                                Rectangle constraint = (Rectangle)getConstraintFor(request);
                                constraint.x = (constraint.x < 0) ? 0 : constraint.x;
                                constraint.y = (constraint.y < 0) ? 0 : constraint.y;
                                constraint.width = (constraint.width <= 0) ? ServiceFigure.SERVICE_FIGURE_DEFWIDTH : constraint.width;
                                constraint.height = (constraint.height <= 0) ? ServiceFigure.SERVICE_FIGURE_DEFHEIGHT : constraint.height;
                                cmd.setLayout(constraint);
                                return cmd;
                        }
                        return null;
                }
        }

Ici, si le type de la requete est bien une requete de creation (en realite, ce test n'est, a priori, pas vraiment necessaire puisque nous sommes deja dans getCreateCommand()) et si l'EditPart cible est bien un EntreprisePart, alors nous creeons la commande et la remplissons des informations utiles. Au passage, les plus reactifs d'entre vous auront noter qu'il nous faut ajouter les deux constantes de taille dans la classe ServiceFigure (relatif aux services et a leur representation donc c'est bien ici que ca doit etre).

        public class ServiceFigure extends Figure {
                public static final int SERVICE_FIGURE_DEFWIDTH = 250;
                public static final int SERVICE_FIGURE_DEFHEIGHT = 150;
                // ...
        }

Il faut ensuite verifier que l'EditPolicy que nous venons de modifier est bien utiliser par les EditPart dans lesquels nous voulons pouvoir deposer un nouvel element. Dans notre cas, c'est EntreprisePart; en effet, on ne veux pas pouvoir creer un service qui couvrirait un autre service (tout du moins, dont le point superieur gauche serait dans l'espace d'un autre service pour etre precis). Dans notre cas, cest deja fait, il ne reste plus qu'a ajouter l'entree dans la palette.
        public class MyGraphicalEditor extends GraphicalEditorWithPalette
        {
                // ...
                protected PaletteRoot getPaletteRoot()
                {
                        // ...
                        PaletteSeparator sep2 = new PaletteSeparator();
                        root.add(sep2);

                        PaletteGroup instGroup = new PaletteGroup("Creation d'elemnts");
                        root.add(instGroup);
                       
                        instGroup.add(new CreationToolEntry("Service", "Creation d'un service type",
                                        new NodeCreationFactory(Service.class),
                                        null, null));
                        // ...   
                        root.setDefaultEntry(selectionToolEntry);
                        return root;
                }
        }

On definit donc une nouvelle entree dans la palette (que l'on place dans un nouveau groupe dans la palette d'ailleurs), a laquelle on donne, dans l'ordre : un nom, une description, une instance de la factory qui permettra de generer les objets puis les ImageDescriptor vers les icones respectivement pour les petites et grandes vues. Il ne reste plus qu'a tester tout ca !



Ajout d'un employe dans un service


On peux creer un nouveau service dans notre entreprise mais ca serait interessant d'y rajouter des employes. On va reproduire globalement la meme procedure que pour l'etape d'avant.
Tout d'abord, on va modifier notre CreationFactory afin qu'elle puisse nous produire des objets de type Employe. On procede comme ceci:
        public class NodeCreationFactory implements CreationFactory
        {
                // ...
                public Object getNewObject() {
                        // ...
                        else if (template == Employe.class) {
                                Employe emp = new Employe();
                                emp.setPrenom("Halle");
                                emp.setName("Berry");
                                return emp;
                        }
                        return null;
                }
        }

Nous allons maintenant definir une nouvelle commande, que l'on va appeler EmployeCreateCommand, qui correspond concretement a l'ajout de l'employe dans un service precis.
        public class EmployeCreateCommand extends Command
        {
                private Service srv;
                private Employe emp;
               
                public EmployeCreateCommand() {
                        super();
                        srv = null;
                        emp = null;
                }
               
                public void setService(Object s) {
                        if (s instanceof Service)
                                this.srv = (Service)s;
                }
               
                public void setEmploye(Object e) {
                        if (e instanceof Employe)
                                this.emp = (Employe)e;
                }
               
                public void setLayout(Rectangle r) {
                        if (emp == null)
                                return;
                        emp.setLayout(r);
                }
               
                @Override
                public boolean canExecute() {
                        if (srv == null || emp == null)
                                return false;
                        return true;
                }
               
                @Override
                public void execute() {
                        srv.addChild(emp);
                }
               
                @Override
                public boolean canUndo() {
                        if (srv == null || emp == null)
                                return false;
                        return srv.contains(emp);
                }
               
                @Override
                public void undo() {
                        srv.removeChild(emp);
                }
        }

Dans le concept, pas beaucoup de changement, on memorise les informations utiles pour pouvoir faire du undo/redo et on ajoute l'employe dans le service specifie. Il faut que nous construisons cette nouvelle commande, lorsque le contexte est correct.

        public class AppEditLayoutPolicy extends XYLayoutEditPolicy
        {
                // ...
                @Override
                protected Command getCreateCommand(CreateRequest request) {
                        if (request.getType() == REQ_CREATE && getHost() instanceof EntreprisePart) {
                                // ...
                        }
                        else if (request.getType() == REQ_CREATE && getHost() instanceof ServicePart) {
                                EmployeCreateCommand cmd = new EmployeCreateCommand();
                               
                                cmd.setService(getHost().getModel());
                                cmd.setEmploye(request.getNewObject());
                               
                                Rectangle constraint = (Rectangle)getConstraintFor(request);
                                constraint.x = (constraint.x < 0) ? 0 : constraint.x;
                                constraint.y = (constraint.y < 0) ? 0 : constraint.y;
                                constraint.width = (constraint.width <= 0) ? EmployeFigure.EMPLOYE_FIGURE_DEFWIDTH :  constraint.width;
                                constraint.height = (constraint.height <= 0) ? EmployeFigure.EMPLOYE_FIGURE_DEFHEIGHT : constraint.height;
                                cmd.setLayout(constraint);
                                return cmd;               
                        }
                        return null;
                }
        }

Encore une fois, on ajoute au passage dans EmployeFigure nos valeurs par defaut et histoire d'homogeneiser le tout, on les utilse aussi lors de la creation de notre graphe. La boucle est donc maintenant bouclee ! Il ne reste plus qu'a ajouter une entree dans la palette qui utilisera tout ce beau code. C'est parti !
        public class MyGraphicalEditor extends GraphicalEditorWithPalette {
                // ...
                protected PaletteRoot getPaletteRoot() {
                        // ...
                        PaletteGroup instGroup = new PaletteGroup("Creation d'elemnts");
                        root.add(instGroup);
                       
                        instGroup.add(new CreationToolEntry("Service", "Creation d'un service type",
                                        new NodeCreationFactory(Service.class),
                                        AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/services-low.png"),
                                        AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/services-high.png")));

                        instGroup.add(new CreationToolEntry("Employe", "Creation d'un employe model",
                                        new NodeCreationFactory(Employe.class),
                                        AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/employe-low.png"),
                                        AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/employe-high.png")));
                        // ...
                }
        }

Au passage, on ajoute des petites icones a nos deux entrees : l'une pour notre entree de creation de service, l'autre pour celle des employes. On recupere un ImageDescriptor grace l'identifiant du plugin et le chemin relatif de nos icones dans celui-ci. Si vous preferez la palette interactive (FlyoutPalette), vous aurez acces a certaines configurations sympathiques et notamment celle qui utilisera la grande version des icones.

Ma foie, tout ca est bien sympatique, il ne reste plus qu'a tester l'ensemble et prendre une petite pause avant la prochaine partie du tutorial. Enjoy :)


Vous pouvez telecharger l'archive de ce tutorial ICI.
Tutorial rédigé par Romain MESON.