Développement avancé avec Eclipse Zest

Alain Bernard

Cet article présente différents compléments sur la boîte à outils de visualisation de graphes sous Eclipse Zest. Il complète l'article d'introduction précédemment publié Introduction à Zest. On se propose ici d'enrichir le graphe que nous avions créé avec divers effets de mise en forme proposés par l'API de Zest. Cet article se base sur Eclipse 3.7.

Les sources de l'exemple sont disponibles à l'adresse suivante : FTP ou HTTP.

Pour toute remarque ou question sur ce tutoriel, profitez de cette discussion : Commentez

- Titre : Développement avancé avec Eclipse Zest

- Auteur : Alain Bernard

- Parution : 20 février 2012

- Mise à jour : 15 avril 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 Alain Bernard. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Changer l'algorithme d'agencement

- III. Mettre en place un zoom

- IV. Interagir avec l'utilisateur

- V. Décorer son graphe

- V-A. Mettre en forme les connexions

- V-B. Mettre en forme les nœuds

- VI. Filtrer certains éléments

- VII. Conclusion

- VIII. Liens utiles

- IX. Remerciements

I - Introduction

Dans l'article précédent, Introduction à Zest, nous avons vu comment construire un graphe, que ce soit à partir d'un modèle en utilisant les mêmes mécanismes que JFace, ou directement en créant nœuds et branches. Cet article reprend l'exemple de l'article précédent, et se propose de montrer comment enrichir le graphe de diverses manières : soit par le biais de styles personnalisés, de menus contextuels, ou encore par la mise en place d'un zoom, ou de layouts différents.

Ce tutoriel suppose que vous disposiez des connaissances de base sur les technologies suivantes :

- développement de plugins avec Eclipse ;

- utilisation de JFace.

Par ailleurs, nous repartons sur le graphe orienté "branches" de l'article d'introduction, contenu dans la vue "LinkView". Les sources sont disponibles ici (FTP) ou ici (HTTP).

II - Changer l'algorithme d'agencement

La boîte à outils Zest possède plusieurs algorithmes d'agencement pour un graphe, qui modifient la disposition initiale du graphe lors de sa construction. Ces algorithmes sont :

- GridLayoutAlgorithm : dispose les éléments sur une grille, comme le ferait un GridLayout Swing classique  ;

- HorizontalLayoutAlgorithm : dispose les éléments horizontalement, sur une seule ligne ;

- HorizontalShift : décale les éléments qui sont hors de la zone d'affichage du graphe vers la droite ;

- TreeLayoutAlgorithm : dispose les éléments selon un arbre vertical ;

- HorizontalTreeLayoutAlgorithm : effectue la même chose que le Tree LayoutAlgorithm, mais horizontalement ;

- RadialLayoutAlgorithm : place le nœud "racine" au centre et dispose les autres nœuds tout autour ;

- SpringLayoutAlgorithm : combine les éléments de manière à ce que les connexions aient approximativement la même taille et que le minimum de nœuds "dépassent" du graphe ;

- VerticalLayoutAlgorithm : dispose les éléments sur une droite verticale ;

- CompositeLayoutAlgorithm : combine différents algorithmes choisis à la construction pour le rendu du graphe. Par exemple, on peut utiliser le SpringLayout, puis HorizontalShift pour décaler les éléments qui seraient encore hors du graphe.

La plupart de ces layouts peuvent s'utiliser sans paramètres dans le constructeur, néanmoins, on peut utiliser l'option "LayoutStyles.NO_LAYOUT_NODE_RESIZING" afin que le layout ne redimensionne pas les nœuds du graphe. De même, l'option "LayoutStyles.ENFORCE_BOUNDS" force à contenir le graphe dans la zone d'affichage, sans dépassement.

Nous allons mettre en œuvre ces différents algorithmes : pour cela complétez votre vue "LinkGraph" en ajoutant un menu localisé, qui permettra de choisir le layout à utiliser. Complétez le contenu de la méthode "createPartControl()" en y ajoutant le code suivant :

LinkGraphView.java

1. @Override

2. public void createPartControl(Composite parent) {

4. //(...)

6. //Creation de la liste des layouts disponibles

7. layouts = new HashMap<String, LayoutAlgorithm>();

8. layouts.put("Grid Layout", new

9. GridLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

10. layouts.put("Horizontal Shift", new

11. HorizontalShift(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

12. layouts.put("Horizontal Layout", new

13. HorizontalLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

14. layouts.put("Horizontal Tree Layout", new

15. HorizontalTreeLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

16. layouts.put("Radial Layout", new

17. RadialLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

18. layouts.put("Spring Layout", new

19. SpringLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

20. layouts.put("Tree Layout", new

21. TreeLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

22. layouts.put("Vertical Layout", new

23. SpringLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));

24. layouts.put("Composite Layout", new CompositeLayoutAlgorithm( new

25. LayoutAlgorithm[] {new SpringLayoutAlgorithm(),

26. new HorizontalShift(LayoutStyles.NO_LAYOUT_NODE_RESIZING)}));

28. //Creation du menu de selection des layouts

29. IMenuManager menuManager = this.getViewSite().getActionBars()

30. .getMenuManager();

31. MenuManager layoutsMenu = new MenuManager("Layouts");

32. menuManager.add(layoutsMenu); // le menu de selection apparait dans le menu

33. localise

35. Action a;

36. for (final String s : layouts.keySet()) {

37. //pour chaque layout on cree un element de menu permettant de le

38. selectionner

39. a = new Action(s, IAction.AS_RADIO_BUTTON) {

40. @Override

41. public void run() {

42. viewer.setLayoutAlgorithm(layouts.get(s), true);

43. }

44. };

45. layoutsMenu.add(a); //l'item est ajoute au menu des layouts

46. //par defaut, on selectionne le Spring Layout

47. if (s.equals("Spring Layout")) {

48. a.run();

49. a.setChecked(true);

50. }

51. }

53. // (...)

55. }

La vue dispose maintenant d'un menu localisé qui permet de sélectionner le layout à utiliser. Bien évidemment, suivant le type de données représentées par le graphe, il sera plus judicieux d'utiliser tel ou tel layout.

Tree Layout

GridLayout

III - Mettre en place un zoom

Zest donne la possibilité de mettre en place très facilement un système de zoom sur le graphe. Pour cela, il faut que la vue (ou l'éditeur) implémente l'interface IZoomableWorkbenchPart. D'autre part, il faut aussi définir un menu, qui permettra à l'utilisateur de choisir son niveau de zoom. Reprenez le code de la vue "LinkGraph" et implémentez l'interface IZoomableWorkbenchPart. La méthode "getZoomableViewer()" doit retourner l'objet GraphViewer. D'autre part, ajoutez un menu dans le menu localisé de la vue, comme suit :

LinkGraphView.java

1. public class LinkGraphView extends ViewPart implements IZoomableWorkbenchPart {

3. // (...)

5. @Override

6. public void createPartControl(Composite parent) {

8. // (...)

10. //Creation d'un menu pour zoomer sur le graphe

11. //ce menu sera affiche dans le menu localise de la vue

12. IMenuManager menuManager = this.getViewSite().getActionBars()

13. .getMenuManager();

15. MenuManager zoomMenu = new MenuManager("Zoom");

16. ZoomContributionViewItem zoomItem = new ZoomContributionViewItem(this);

17. zoomMenu.add(zoomItem);

19. menuManager.add(zoomMenu);

21. // (...)

23. }

25. @Override

26. public AbstractZoomableViewer getZoomableViewer() {

27. return viewer;

28. }

29. }

Zoom

IV - Interagir avec l'utilisateur

Au-delà de la visualisation, il peut être intéressant de proposer à l'utilisateur une interaction avec le graphe, par exemple pour modifier le modèle. Dans notre cas, nous aimerions que l'utilisateur puisse ouvrir et fermer des routes. L'utilisateur doit pouvoir choisir le statut qu'il veut affecter à la route à partir d'un menu contextuel accessible depuis les branches du graphe. La première étape consiste à modifier le modèle pour rajouter dans la classe Route un attribut booléen "ouverte" et les getter/setter associés. Il faut ensuite créer le menu contextuel, au sein de notre classe LinkGraphView.java. Rajoutons le code suivant dans la méthode "createPartControl" :

LinkGraphView.java

1. @Override

2. public void createPartControl(Composite parent) {

4. // (...)

6. //Creation du menu contextuel

7. final MenuManager mgr = new MenuManager();

8. viewer.getControl().setMenu(mgr.createContextMenu(viewer.getControl()));

9. mgr.setRemoveAllWhenShown(true); //le menu est recree a chaque affichage

10. mgr.addMenuListener(new IMenuListener() {

11. @Override

12. public void menuAboutToShow(IMenuManager manager) {

13. //le contenu du menu depend de la selection courante

14. IStructuredSelection selection = (IStructuredSelection)

15. viewer.getSelection();

16. if (!selection.isEmpty() && selection.getFirstElement() != null) {

17. Object first = selection.getFirstElement();

18. if (first instanceof Route) {

19. manager.add(new OpenRoadAction((Route)first));

20. }

21. }

22. }

23. });

25. // (...)

27. }

Nous créons dans le menu une instance de "OpenRoadAction", qui permet de définir le comportement à adopter lorsque l'utilisateur la sélectionne. Cette classe est définie comme suit :

OpenRoadAction.java

1. /**

2. * Cette action permet a l'utilisateur d'ouvrir/fermer les routes

3. * directement depuis le graphe.

4. * @author A. Bernard

5. */

6. public class OpenRoadAction extends Action {

8. /**

9. * la route geree par l'action

10. */

11. private Route route;

13. /**

14. * Constructeur.

15. * Definit le texte de l'action affiche.

16. * @param r la route selectionnee

17. */

18. public OpenRoadAction(Route r) {

19. super();

20. this.route = r;

21. }

23. @Override

24. public String getText() {

25. if (route.isOuverte()) {

26. return "Fermer la route";

27. } else {

28. return "Ouvrir la route";

29. }

30. }

32. @Override

33. public void run() {

34. if (route.isOuverte()) {

35. route.setOuverte(false);

36. } else {

37. route.setOuverte(true);

38. }

39. //ne pas oublier de rafraichir le graphe

40. viewer.refresh();

41. }

42. }

On peut alors observer le résultat lors d'un clic droit sur le graphe :

Menu sur route ouverte

Menu sur route fermée

Notre graphe est maintenant interactif, mais rien ne permet de voir directement si une route est ouverte ou fermée. Nous allons donc dans la partie suivante étudier des interfaces fournies par Zest qui vont nous permettre d'afficher ces informations, en mettant en forme notre graphe.

V - Décorer son graphe

Afin de mettre en forme le graphe, il faut utiliser différentes interfaces, selon l'élément à modifier :

- l'interface "IConnectionStyleProvider" permet de modifier l'apparence des connexions ;

- l'interface "IEntityStyleProvider" permet de modifier l'apparence des entités ;

- l'interface "IEntityConnectionStyleProvider" permet de modifier l'apparence des connexions dans un graphe construit à partir de ses nœuds. En effet, rappelons-nous que dans ce cas, nous n'avons pas accès directement aux connexions.

Nous détaillons l'utilisation de ces interfaces dans les paragraphes suivants.

V-A - Mettre en forme les connexions

Dans un premier temps, nous allons retravailler sur la vue LinkGraphView : on se propose d'afficher en rouge les routes fermées et en vert les routes ouvertes. Reprenez le code de la classe LinkLabelProvider.java, et implémentez l'interface IConnectionStyleProvider. Complétez les nouvelles méthodes comme suit :

LinkLabelProvider.java

1. public class LinkLabelProvider extends LabelProvider  implements

2. IConnectionStyleProvider {

4. // (...)

6. /* **************************************************

7. *     Methodes de l'interface IConnectionStyleProvider

8. * ************************************************** */

9. @Override

10. public int getConnectionStyle(Object rel) {

11. return ZestStyles.CONNECTIONS_SOLID; //valeur par defaut

12. //Utiliser ZestStyles.CONNECTION_DIRECTED pour afficher une fleche

13. // source -> destination

14. }

16. @Override

17. public Color getColor(Object rel) {

18. //Donne la couleur de base de la connexion

19. if (rel instanceof Route) {

20. Route r = (Route) rel;

21. if (r.isOuverte()) {

22. return Display.getDefault().getSystemColor(SWT.COLOR_GREEN);

23. } else {

24. return Display.getDefault().getSystemColor(SWT.COLOR_RED);

25. }

26. } else {

27. return null; //valeur par defaut

28. }

29. }

31. @Override

32. public Color getHighlightColor(Object rel) {

33. //Donne la couleur de la connexion lorsque cette derniere est

34. selectionnee

35. if (rel instanceof Route) {

36. Route r = (Route) rel;

37. if (r.isOuverte()) {

38. return Display.getDefault().getSystemColor(SWT.COLOR_GREEN);

39. } else {

40. return Display.getDefault().getSystemColor(SWT.COLOR_RED);

41. }

42. } else {

43. return null; //valeur par defaut

44. }

45. }

47. @Override

48. public int getLineWidth(Object rel) {

49. //Donne l'epaisseur d'une connexion

50. return -1; //valeur par defaut

51. }

53. @Override

54. public IFigure getTooltip(Object entity) {

55. //Affiche un tooltip sur la connexion

56. if (entity instanceof Route) {

57. if (((Route)entity).isOuverte()) {

58. return new Label("Route ouverte");

59. } else {

60. return new Label("Route fermee");

61. }

62. } else {

63. return null;

64. }

65. }

66. }

Chaque méthode donne directement accès à la connexion en cours de construction, ce qui permet très facilement de définir le style des éléments. Nous pouvons observer le résultat sur notre graphe :

Mise en forme des connexions

Il est impossible d'utiliser cette méthode lorsque le graphe est construit à partir des nœuds. En effet, nous n'avons pas accès aux connexions lors de la construction du graphe. Pour cela, Zest propose l'interface IEntityConnectionStyleProvider. Reprenons la classe EntityLabelProvider :

EntityLabelProvider.java

1. public class EntityLabelProvider extends LabelProvider implements

2. IEntityConnectionStyleProvider {

4. // (...)

6. /* ********************************************************

7. *     Methodes de l'interface IEntityConnectionStyleProvider

8. * ******************************************************** */

10. @Override

11. public int getConnectionStyle(Object src, Object dest) {

12. return ZestStyles.CONNECTIONS_SOLID; //valeur par defaut

13. //Utiliser ZestStyles.CONNECTION_DIRECTED pour afficher une fleche

14. // source -> destination

15. }

17. @Override

18. public Color getColor(Object src, Object dest) {

19. //Donne la couleur de la connexion

20. return Display.getDefault().getSystemColor(SWT.COLOR_BLACK);

21. }

23. @Override

24. public Color getHighlightColor(Object src, Object dest) {

25. //Donne la couleur de la connexion si selectionnee

26. return Display.getDefault().getSystemColor(SWT.COLOR_MAGENTA);

27. }

29. @Override

30. public int getLineWidth(Object src, Object dest) {

31. //Donne l'epaisseur de la connexion

32. return -1;

33. }

35. @Override

36. public IFigure getTooltip(Object entity) {

37. // Donne un tooltip pour le noeud selectionne

38. return null;

39. }

41. }

Chaque méthode permet d'accéder à la connexion définie par sa source et sa destination.

Mise en forme des connexions

V-B - Mettre en forme les nœuds

Nous souhaitons maintenant afficher en rouge les villes qui ne sont plus desservies par aucune route. Reprenez la classe LinkLabelProvider.java et implémentez l'interface IEntityStyleProvider :

LinkLabelProvider.java

1. >

2. public class LinkLabelProvider extends LabelProvider  implements

3. IConnectionStyleProvider, IEntityStyleProvider {

5. // (...)

7. /* **********************************************

8. *     Methodes de l'interface IEntityStyleProvider

9. * ********************************************** */

11. @Override

12. public Color getNodeHighlightColor(Object entity) {

13. //Donne la couleur du noeud si selectionne

14. return null;

15. }

17. @Override

18. public Color getBorderColor(Object entity) {

19. //Donne la couleur de la bordure des noeud

20. return null; //valeur par defaut

21. }

23. @Override

24. public Color getBorderHighlightColor(Object entity) {

25. //Donne la couleur de la bordure du noeud selectionne

26. return null; //Valeur par defaut

27. }

29. @Override

30. public int getBorderWidth(Object entity) {

31. // Donne l'epaisseur de la bordure du noeud

32. return -1;

33. }

35. @Override

36. public Color getBackgroundColour(Object entity) {

37. //Donne la couleur de fond par defaut des noeuds

38. if (entity instanceof Ville) {

39. Ville v = (Ville) entity;

40. //On regarde si les routes qui desservent la ville sont toutes

41. // fermees ou non

42. boolean estReliee = false;

43. for (Route r : Model.INSTANCE.getRoutes()) {

44. if (r.getSource().equals(v) || r.getDestination().equals(v)) {

45. // La route consideree dessert la ville.

46. // Si elle est ouverte, la ville est accessible par au

47. // moins une route

48. if (r.isOuverte()) {

49. estReliee = true;

50. }

51. }

52. }

53. //Si la ville est reliee par au moins une route, la couleur

54. //affichee est celle par defaut, sinon le noeud est colore en rouge

55. if (estReliee) {

56. return null;

57. } else {

58. return Display.getDefault().getSystemColor(SWT.COLOR_RED);

59. }

60. } else {

61. return null;

62. }

63. }

65. @Override

66. public Color getForegroundColour(Object entity) {

67. //Donne la couleur d'avant-plan des noeuds (le texte par ex.)

68. return null; //valeur par defaut

69. }

71. @Override

72. public boolean fisheyeNode(Object entity) {

73. //Cree un zoom sur l'entite quand elle est selectionnee

74. return true;

75. }

77. }

Une nouvelle fois, chaque méthode nous permet d'accéder très facilement à l'entité en cours d'affichage. Remarquez la méthode "fisheyeNode" qui permet d'effectuer un zoom sur le nœud lorsqu'il est survolé par la souris. Observons le résultat :

Mise en forme des entités

VI - Filtrer certains éléments

Zest nous permet aussi de mettre en place des filtres sur les graphes, de la même manière que sur les viewers JFace. Créez le filtre suivant, qui nous permettra de cacher les villes qui ne sont desservies par aucune route :

EntityFilter.java

1. package com.abernard.zest.viewer;

3. import org.eclipse.jface.viewers.Viewer;

4. import org.eclipse.jface.viewers.ViewerFilter;

6. import com.abernard.zest.model.Model;

7. import com.abernard.zest.model.Route;

8. import com.abernard.zest.model.Ville;

10. /**

11. * Ce filtre permet de cacher les villes desservies par aucune route. Si

12. * la ville n'est pas reliee par des routes, elle n'est pas affichee.

13. * @author A. Bernard

14. */

15. public class EntityFilter extends ViewerFilter {

17. @Override

18. public boolean select(Viewer viewer, Object parentElement, Object element)

19. {

20. // Determine si la ville doit etre affichee, ou non

21. if (element instanceof Ville) {

22. Ville v = (Ville) element;

23. //On regarde si les routes qui desservent la ville sont toutes

24. // fermees ou non

25. boolean estReliee = false;

26. for (Route r : Model.INSTANCE.getRoutes()) {

27. if (r.getSource().equals(v) || r.getDestination().equals(v)) {

28. // La route consideree dessert la ville.

29. // Si elle est ouverte, la ville est accessible par au

30. // moins une route

31. if (r.isOuverte()) {

32. estReliee = true;

33. }

34. }

35. }

36. //Si la ville est reliee par au moins une route, la ville est

37. // affichee, sinon elle est filtree

38. return estReliee;

39. } else {

40. return true;

41. }

42. }

44. }

Ajoutez ce filtre au graphe dans la vue LinkGraphView.java :

LinkGraphView.java

1. @Override

2. public void createPartControl(Composite parent) {

4. // (...)

6. //Definition du filtre

7. viewer.addFilter(new EntityFilter());

9. // (...)

10. }

Observez maintenant le résultat lorsque l'on ferme la route qui relie Metz à Paris :

Filtre sur le graphe

VII - Conclusion

Nous avons vu dans cet article comment modifier l'affichage du graphe et améliorer son ergonomie. Au terme de ces deux articles, nous pouvons constater que Zest est un outil de visualisation de graphes puissant tout en étant pratique et facile à utiliser, et qui propose assez d'options pour représenter des graphes clairs et précis, adaptés au modèle de chaque application.
Cette boîte à outils est notamment utilisée pour l'outil de visualisation de dépendances de PDE (Plugin Developppement Environment), actuellement dans l'Incubator d'Eclipse : PDE Incubator Dependency Visualization.

VIII - Liens utiles

Pour aller plus loin, voici quelques liens utiles :

- Tutoriels sur le développement de plugins Eclipse et l'utilisation de SWT/JFace ;

- Page officielle de Zest ;

- Wiki sur Zest ;

- Snippets sur Zest.

IX - Remerciements

Je tiens à remercier pour cet article les membres de la communauté Java pour leurs remarques et leurs conseils. Un grand merci aussi à jacques_jean pour sa relecture attentive.