Tutoriel : découvrir la vue Properties d'Eclipse

Alain BERNARD

La vue Properties est très souvent utilisée dans Eclipse. Elle prend plusieurs formes, mais dans tous les cas, elle est généralement utilisée pour afficher des informations sur ce qui est actuellement sélectionné. Utiliser la vue Properties garantit une bonne modularité des plugins et ce tutoriel se propose de donner un aperçu des différentes méthodes pour contribuer à cette vue. Une connaissance en développement de plugins Eclipse est nécessaire pour les deux premières implémentations. Une connaissance de base en EMF pour le troisième exemple et d'Eclipse Sirius pour le quatrième est obligatoire. Pour une première approche de Sirius, vous pouvez consulter ce tutoriel. Ce tutoriel est basé sur Eclipse Neon. Les sources de cet exemple sont disponibles à l'adresse suivante : FTP ou HTTP.

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

- Titre : Tutoriel : découvrir la vue Properties d'Eclipse

- Auteur : Alain BERNARD

- Parution : 16 janvier 2017

- Public visé :

- Licence : Copyright ® 2017 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. Notre modèle de données

- III. La vue Properties basique

- III-A. Une histoire d'adapteurs

- III-B. La définition des adapteurs

- III-C. Les adapteurs en action

- IV. La vue Properties par onglets

- IV-A. Architecture de cette vue

- IV-B. Mise en place des extensions

- IV-C. Écriture du code

- IV-D. Test de nos vues

- V. La vue Properties basique avec EMF

- V-A. Modifier la génération EMF

- V-B. Observer le résultat

- VI. Une vue Property sans code avec Eclipse Sirius

- VI-A. Le modèle Sirius

- VI-B. La mise en pratique

- VII. Conclusion

- VIII. Liens utiles

- IX. Remerciements

I - Introduction

Vous avez sans doute déjà utilisé la vue « Properties » d'Eclipse. Souvent affichée en bas de la fenêtre, elle affiche des informations sur la sélection courante. Par exemple, sur un fichier ou un dossier, elle va afficher un certain nombre d'informations.

Cette vue permet une approche réellement modulaire de l'affichage des propriétés de vos objets : peu importe dans quelle vue ils seront manipulés et sélectionnés, la vue Properties répondra toujours présente et affichera leur contenu. Il existe plusieurs manières de construire cette vue Properties, de la plus basique à la plus complexe. Nous donnerons dans cet article une première vue sur quatre manières différentes : la vue basique, la vue par onglets, la génération via EMF et enfin une première définition de la vue Properties avec Eclipse Sirius.

II - Notre modèle de données

Évidemment, avant d'afficher des objets dans la vue Properties, il nous faut des objets. Pour cela, nous allons réaliser un modèle très simple, des compagnies aériennes (Airline) qui possèdent chacune des avions (Plane). Commencez par créer un plugin Eclipse « com.abernard.properties », qui effectuera des contributions à l'interface. L'utilisation des vues Properties nécessite une architecture 3.x, nous créerons donc un plugin de type 3.x. Commencez par créer les deux classes Airline.java et Plane.java, ainsi qu'une énumération simple Manufacturer.java.

1. package com.abernard.properties.data;

3. import java.util.ArrayList;

4. import java.util.List;

6. /**

7. * Une compagnie aerienne avec sa flotte.

8. * @author A. BERNARD

9. *

10. */

11. public class Airline {

13. private List<Plane> fleet;

14. private String oaci;

15. private String name;

17. public Airline(String oaci, String name) {

18. this.fleet = new ArrayList<Plane>();

19. this.oaci = oaci;

20. this.name = name;

21. }

23. /**

24. * @return the fleet

25. */

26. public List<Plane> getFleet() {

27. return fleet;

28. }

30. /**

31. * @param fleet the fleet to set

32. */

33. public void setFleet(List<Plane> fleet) {

34. this.fleet = fleet;

35. }

37. /**

38. * @return the oaci

39. */

40. public String getOaci() {

41. return oaci;

42. }

44. /**

45. * @param oaci the oaci to set

46. */

47. public void setOaci(String oaci) {

48. this.oaci = oaci;

49. }

51. /**

52. * @return the name

53. */

54. public String getName() {

55. return name;

56. }

58. /**

59. * @param name the name to set

60. */

61. public void setName(String name) {

62. this.name = name;

63. }

65. }

Le code OACI d'une compagnie aérienne est un code à trois lettres. Ils sont attribués par l'Organisation de l'Aviation Civile Internationale (OACI, soit ICAO en anglais) aux compagnies aériennes du monde entier.

1. package com.abernard.properties.data;

3. /**

4. * Informations a propos d'un modele d'avion dans une compagnie.

5. * @author A. BERNARD

6. *

7. */

8. public class Plane {

10. private Airline airline;

12. private String name;

14. private int count;

16. private int paxCount;

18. private int orders;

20. private Manufacturer manufacturer;

22. public Plane(Airline al, Manufacturer m, String name, int count, int pax, int orders) {

23. this.airline = al;

24. this.manufacturer = m;

25. this.name = name;

26. this.count = count;

27. this.paxCount = pax;

28. this.orders = orders;

29. }

31. /**

32. * @return the name

33. */

34. public String getName() {

35. return name;

36. }

38. /**

39. * @param name the name to set

40. */

41. public void setName(String name) {

42. this.name = name;

43. }

45. /**

46. * @return the count

47. */

48. public int getCount() {

49. return count;

50. }

52. /**

53. * @param count the count to set

54. */

55. public void setCount(int count) {

56. this.count = count;

57. }

59. /**

60. * @return the paxCount

61. */

62. public int getPaxCount() {

63. return paxCount;

64. }

66. /**

67. * @param paxCount the paxCount to set

68. */

69. public void setPaxCount(int paxCount) {

70. this.paxCount = paxCount;

71. }

73. /**

74. * @return the orders

75. */

76. public int getOrders() {

77. return orders;

78. }

80. /**

81. * @param orders the orders to set

82. */

83. public void setOrders(int orders) {

84. this.orders = orders;

85. }

87. /**

88. * @return the airline

89. */

90. public Airline getAirline() {

91. return airline;

92. }

94. /**

95. * @return the manufacturer

96. */

97. public Manufacturer getManufacturer() {

98. return manufacturer;

99. }

101. /**

102. * @param manufacturer the manufacturer to set

103. */

104. public void setManufacturer(Manufacturer manufacturer) {

105. this.manufacturer = manufacturer;

106. }

108. }

Afin de générer quelques données, nous créons aussi une classe « DataGenerator.java » munie d'une simple méthode statique de génération :

1. package com.abernard.properties.data;

3. import java.util.ArrayList;

4. import java.util.List;

6. /**

7. * Classe utilitaire pour generer quelques exemples.

8. * @author alain

9. *

10. */

11. public final class DataGenerator {

13. private DataGenerator() {}

15. /**

16. * Genere une liste d'{@link Airline}.

17. * @return

18. */

19. public static List<Airline> generateData() {

20. List<Airline> data = new ArrayList<Airline>();

21. Airline al = new Airline("AFR", "Air France");

22. Plane p = new Plane(al, Manufacturer.AIRBUS, "A350-900", 0, 314, 18);

23. al.getFleet().add(p);

24. p = new Plane(al, Manufacturer.AIRBUS, "A380-800", 10, 516, 2);

25. al.getFleet().add(p);

26. p = new Plane(al, Manufacturer.EMBRAER, "170", 54, 76, 8);

27. al.getFleet().add(p);

28. data.add(al);

30. al = new Airline("QTR", "Qatar Airways");

31. p = new Plane(al, Manufacturer.AIRBUS, "A330-300", 13, 305, 0);

32. al.getFleet().add(p);

33. p = new Plane(al, Manufacturer.BOEING, "B787-800", 17, 254, 13);

34. al.getFleet().add(p);

35. data.add(al);

37. al = new Airline("DLH", "Lufthansa");

38. p = new Plane(al, Manufacturer.BOEING, "B747-400", 19, 344, 0);

39. al.getFleet().add(p);

40. p = new Plane(al, Manufacturer.AIRBUS, "A340-600", 23, 346, 0);

41. al.getFleet().add(p);

42. data.add(al);

43. return data;

44. }

46. }

III - La vue Properties basique

III-A - Une histoire d'adapteurs

La vue Properties par défaut peut afficher le contenu d'objets qui implémentent l'interface IPropertySource ou IPropertySource2 pour des fonctions plus poussées. La vue écoute la sélection courante dans le workbench Eclipse, et si un objet correspondant à ce type est sélectionné, elle en affichera les propriétés. Pour autant, tous les objets Java n'implémentent pas une de ces deux interfaces ! Par exemple, les objets IFile sélectionnés dans la vue Project Explorer ne l'implémentent pas. Pas plus que les multiples objets EMF qu'on peut y afficher !

En fait, la vue Properties ne regarde pas vraiment si l'objet sélectionné implémente une de ces fameuses interfaces. Elle regarde si l'objet peut être adapté en l'une de ces interfaces ! On manipule là une notion centrale d'Eclipse : on peut enregistrer dans la plateforme des objets que l'on va appeler des adapteurs, qui vont indiquer à Eclipse : « tiens, pour cet objet A, je peux te fournir un équivalent de type B ». Dans notre cas, nous allons donc indiquer à Eclipse « tiens, pour cet objet Airline, je peux te fournir un équivalent de type IPropertySource ».

III-B - La définition des adapteurs

Pour commencer, il faut déclarer les adapteurs dans le fichier plugin.xml, en utilisant le point d'extension « org.eclipse.core.runtime.adapters ». Dans ce point d'extension, il faut déclarer des nouvelles « adapterFactory ». Chaque factory va définir un attribut de type « adaptableType » (« quel type sais-je adapter ? »), une classe pour fournir la conversion, et enfin, sous ce nœud, « l'adapter » (« vers quoi sais-je adapter »). Par exemple pour notre objet Airline, la configuration est la suivante :

Vous pouvez procéder de la même manière pour les objets Plane. De plus, rien n'empêche d'utiliser la même factory pour vos différents objets et c'est ce que nous allons voir dès à présent. Jetons un œil à la classe AirlinesAdapterFactory.java.

1. package com.abernard.properties.simple;

3. import org.eclipse.core.runtime.IAdapterFactory;

4. import org.eclipse.ui.views.properties.IPropertySource;

6. import com.abernard.properties.data.Airline;

7. import com.abernard.properties.data.Plane;

9. /**

10. * Cette classe transforme nos objets metier en {@link IPropertySource}.

11. * @author alain

12. *

13. */

14. public class AirlinesAdapterFactory implements IAdapterFactory {

16. private static final Class<?>[] TYPES = {IPropertySource.class};

18. @SuppressWarnings("rawtypes")

19. @Override

20. public Object getAdapter(Object adaptableObject, Class adapterType) {

21. if (adapterType == IPropertySource.class) {

22. if (adaptableObject instanceof Airline) {

23. return new AirlinePropertySourceAdapter((Airline)adaptableObject);

24. }

25. if (adaptableObject instanceof Plane) {

26. return new PlanePropertySourceAdapter((Plane)adaptableObject);

27. }

29. }

30. return null;

31. }

33. @Override

34. public Class<?>[] getAdapterList() {

35. return TYPES;

36. }

38. }

On observe dans cette classe une méthode « getAdapterList ». Elle donne la liste des types vers lesquels cette factory sait adapter des objets. L'autre méthode, « getAdapter », permet d'effectuer la conversion de nos objets métiers (adaptableObject) vers la classe demandée (adapterType). Dans notre cas, on voit qu'on retourne un objet AirlinePropertySourceAdapter, ou un objet PlanePropertySourceAdapter, suivant l'objet que l'on souhaite adapter. Ces deux objets implémentent l'interface « IPropertySource », donc le contrat est respecté ! Désormais, à chaque fois qu'un de nos objets sera sélectionné, la vue Properties sera capable de l'interpréter grâce à l'une de ces deux classes. Voyons maintenant concrètement ce que font ces classes.

1. package com.abernard.properties.simple;

3. import org.eclipse.ui.views.properties.IPropertyDescriptor;

4. import org.eclipse.ui.views.properties.IPropertySource2;

5. import org.eclipse.ui.views.properties.PropertyDescriptor;

6. import org.eclipse.ui.views.properties.TextPropertyDescriptor;

8. import com.abernard.properties.data.Airline;

10. /**

11. * Cette classe permet de decrire les differentes proprietes de nos objets {@link Airline}.

12. * @author alain

13. *

14. */

15. public class AirlinePropertySourceAdapter implements IPropertySource2 {

17. private static final String PROP_OACI = "oaci";

18. private static final String PROP_AL_NAME = "al_name";

20. private Airline airline;

22. public AirlinePropertySourceAdapter(Airline airline) {

23. this.airline = airline;

24. }

26. @Override

27. public Object getEditableValue() {

28. return null;

29. }

31. @Override

32. public IPropertyDescriptor[] getPropertyDescriptors() {

33. PropertyDescriptor oaciDescr = new PropertyDescriptor(PROP_OACI, "Code OACI");

34. PropertyDescriptor nameDescr = new TextPropertyDescriptor(PROP_AL_NAME, "Name");

35. return new IPropertyDescriptor[] {oaciDescr, nameDescr};

36. }

38. @Override

39. public Object getPropertyValue(Object id) {

40. switch((String)id) {

41. case PROP_AL_NAME:

42. return airline.getName();

43. case PROP_OACI:

44. return airline.getOaci();

45. }

46. return null;

47. }

49. @Override

50. public boolean isPropertySet(Object id) {

51. return true;

52. }

54. @Override

55. public void resetPropertyValue(Object id) {

56. if (id.equals(PROP_AL_NAME)) {

57. airline.setName("no_name");

58. }

59. }

61. @Override

62. public void setPropertyValue(Object id, Object value) {

63. if (id.equals(PROP_AL_NAME) && value instanceof String) {

64. airline.setName((String)value);

65. }

67. }

69. @Override

70. public boolean isPropertyResettable(Object id) {

71. return (id.equals(PROP_AL_NAME));

72. }

74. }

La méthode principale est « getPropertyDescriptors ». Elle permet de donner la liste des différentes propriétés à afficher, et l'élément graphique qui doit être utilisé pour ce faire. Par exemple, nous utilisons des « TextPropertyDescriptor » pour les valeurs de type texte, ou un « ComboBoxPropertyDescriptor » pour les différents choix d'avionneurs. Il existe différents types fournis de base :

- PropertyDescriptor : pour des propriétés non éditables ;

- ComboBoxPropertyDescriptor : pour une sélection dans une liste ;

- TextPropertyDescriptor : pour les valeurs texte ;

- ColorPropertyEditor : pour la sélection d'une couleur.

Évidemment, vous pouvez créer votre propre PropertyDescriptor pour proposer un mode d'édition spécifique, par exemple une boîte de dialogue. Notez aussi que l'on peut affecter des catégories à chaque élément, ce qui permettra de classer les propriétés dans différentes catégories. Deux autres méthodes, « getPropertyValue » et « setPropertyValue » permettent comme leur nom l'indique de récupérer les valeurs du modèle, ou de changer ces dernières. Enfin, notre classe implémente « IPropertySource2 », une extension de « IPropertySource », qui permet de réinitialiser des propriétés à leur valeur par défaut. Si la méthode « isPropertyResettable » renvoie « true », la méthode « resetPropertyValue » est appelée, permettant de restaurer une valeur par défaut.

Le mécanisme est exactement le même pour nos objets Plane :

1. package com.abernard.properties.simple;

3. import java.util.Arrays;

5. import org.eclipse.ui.views.properties.ComboBoxPropertyDescriptor;

6. import org.eclipse.ui.views.properties.IPropertyDescriptor;

7. import org.eclipse.ui.views.properties.IPropertySource2;

8. import org.eclipse.ui.views.properties.PropertyDescriptor;

9. import org.eclipse.ui.views.properties.TextPropertyDescriptor;

11. import com.abernard.properties.data.Manufacturer;

12. import com.abernard.properties.data.Plane;

14. /**

15. * Cette classe permet de decrire les differentes proprietes de nos objets {@link Plane}.

16. * @author alain

17. *

18. */

19. public class PlanePropertySourceAdapter implements IPropertySource2 {

21. private Plane plane;

23. private static final String PROP_NAME = "plane.name";

24. private static final String PROP_MANUF = "plane.manuf";

25. private static final String PROP_COUNT = "plane.count";

26. private static final String PROP_ORDERS = "plane.orders";

27. private static final String PROP_PAX = "plane.pax";

32. public PlanePropertySourceAdapter(Plane plane) {

33. this.plane = plane;

34. }

36. @Override

37. public Object getEditableValue() {

38. return null;

39. }

41. @Override

42. public IPropertyDescriptor[] getPropertyDescriptors() {

43. TextPropertyDescriptor nameDescr = new TextPropertyDescriptor(PROP_NAME, "Name");

44. nameDescr.setCategory("General");

45. String[] labels = new String[Manufacturer.values().length];

46. for (int i = 0; i < labels.length; i++) {

47. labels[i] = Manufacturer.values()[i].name();

48. }

49. ComboBoxPropertyDescriptor manufDescr = new ComboBoxPropertyDescriptor(PROP_MANUF, "Manufacturer", labels);

50. manufDescr.setCategory("General");

51. TextPropertyDescriptor countDescr = new TextPropertyDescriptor(PROP_COUNT, "Count");

52. countDescr.setCategory("Specific");

53. TextPropertyDescriptor ordersDescr = new TextPropertyDescriptor(PROP_ORDERS, "Orders");

54. ordersDescr.setCategory("Specific");

55. PropertyDescriptor paxDescr = new PropertyDescriptor(PROP_PAX, "Pax");

56. paxDescr.setCategory("Specific");

57. return new IPropertyDescriptor[] {nameDescr, manufDescr, ordersDescr, countDescr, paxDescr};

58. }

60. @Override

61. public Object getPropertyValue(Object id) {

62. switch((String)id) {

63. case PROP_NAME:

64. return plane.getName();

65. case PROP_MANUF:

66. return Arrays.binarySearch(Manufacturer.values(), plane.getManufacturer());

67. case PROP_ORDERS:

68. return String.valueOf(plane.getOrders());

69. case PROP_COUNT:

70. return String.valueOf(plane.getCount());

71. case PROP_PAX:

72. return String.valueOf(plane.getPaxCount());

73. }

74. return null;

75. }

77. @Override

78. public void resetPropertyValue(Object id) {

79. switch((String)id) {

80. case PROP_ORDERS:

81. plane.setOrders(0);

82. break;

83. case PROP_COUNT:

84. plane.setCount(0);

85. break;

86. case PROP_NAME:

87. plane.setName("no_name");

88. break;

89. }

91. }

93. @Override

94. public void setPropertyValue(Object id, Object value) {

95. switch((String)id) {

96. case PROP_MANUF:

97. plane.setManufacturer(Manufacturer.values()[(int)value]);

98. break;

99. case PROP_ORDERS:

100. plane.setOrders(Integer.parseInt((String)value));

101. break;

102. case PROP_COUNT:

103. plane.setCount(Integer.parseInt((String)value));

104. break;

105. case PROP_NAME:

106. plane.setName((String)value);

107. break;

108. }

110. }

112. @Override

113. public boolean isPropertyResettable(Object id) {

114. switch((String)id) {

115. case PROP_ORDERS:

116. case PROP_COUNT:

117. case PROP_NAME:

118. return true;

119. default:

120. return false;

121. }

122. }

124. @Override

125. public boolean isPropertySet(Object id) {

126. return true;

127. }

129. }

Notre code est terminé. Notez tout de même que l'implémentation de IPropertySource2 est assez verbeuse et requiert un certain effort. Nous verrons par la suite des méthodes plus efficaces si vos objets sont complexes !

III-C - Les adapteurs en action

Pour tester notre vue Properties, nous allons avoir besoin de nos objets, affichés dans un endroit quelconque du workbench. Créons donc une vue toute simple, qui va afficher la liste de nos objets.

1. package com.abernard.properties.simple;

3. import java.util.List;

5. import org.eclipse.jface.viewers.LabelProvider;

6. import org.eclipse.jface.viewers.TreeViewer;

7. import org.eclipse.swt.SWT;

8. import org.eclipse.swt.layout.FillLayout;

9. import org.eclipse.swt.widgets.Composite;

10. import org.eclipse.ui.part.ViewPart;

12. import com.abernard.properties.data.Airline;

13. import com.abernard.properties.data.DataGenerator;

14. import com.abernard.properties.data.Plane;

16. /**

17. * Une vue basique qui affiche simplement nos donnees.

18. * @author A. BERNARD

19. *

20. */

21. public class ModelView extends ViewPart {

23. private List<Airline> data;

24. private TreeViewer viewer;

26. public ModelView() {

27. this.data = DataGenerator.generateData();

28. }

30. @Override

31. public void createPartControl(Composite parent) {

32. parent.setLayout(new FillLayout(SWT.HORIZONTAL));

34. viewer = new TreeViewer(parent, SWT.BORDER | SWT.FULL_SELECTION);

36. viewer.setContentProvider(new AirlineTreeContentProvider());

37. viewer.setLabelProvider(new LabelProvider() {

38. @Override

39. public String getText(Object element) {

40. if (element instanceof Airline) {

41. return ((Airline)element).getName() + " (" + ((Airline)element).getOaci() + ")";

42. } else if (element instanceof Plane) {

43. return ((Plane)element).getManufacturer().toString() + " " + ((Plane)element).getName();

44. }

45. return super.getText(element);

46. }

47. });

48. viewer.setInput(data);

49. // Informer le workbench a chaque selection

50. getSite().setSelectionProvider(viewer);

51. }

53. @Override

54. public void setFocus() {

55. viewer.getTree().setFocus();

56. }

58. }

La seule méthode à ne vraiment pas oublier dans cette classe est celle-ci : getSite().setSelectionProvider(viewer);. En effet, c'est grâce à elle que la sélection de nos objets sera transmise via le service de sélection au workbench, et donc pourra être interceptée par la vue Properties. Si nous lançons notre application, nous pouvons observer le résultat.

On retrouve bien nos propriétés, organisées en catégories, et dans la capture ci-dessus, la liste de choix disponibles. Notez que si nous sélectionnons la propriété « Name » d'un avion, on peut cliquer sur l'icône « Restore default value » et le champ prendra alors la valeur « no_name ».

IV - La vue Properties par onglets

La méthode que nous avons vue au paragraphe précédent permet de générer simplement une petite vue Properties pour des attributs simples. Néanmoins, quand les propriétés vont devenir plus complexes, elle va vite devenir moins performante. De même, si le nombre de propriétés est très important, ou que de multiples plugins veulent contribuer une partie des propriétés pour un même objet métier, elle ne sera plus adaptée. C'est ainsi qu'est née la vue « Tabbed Properties ». Elle permet de travailler directement sur les objets métier (plus besoin d'adapters), et permet une contribution plus complexe et plus dynamique des plugins.

Nous allons voir dans les paragraphes suivants comment exploiter cette vue.

IV-A - Architecture de cette vue

La vue « Tabbed Properties » s'articule autour de trois notions essentielles. Le point d'entrée est le « propertyContributor ». Ce contributeur donne un ID et permet de déterminer quelle vue Properties sera instanciée pour tel contributeur. Cet ID sera par exemple donné par les vues ou éditeurs qui souhaiteront déclencher l'affichage des propriétés. Un contributeur va posséder une ou plusieurs « propertyCategory ». Ces catégories vont donner une organisation des onglets affichés sur la gauche de la vue. Les onglets sont la deuxième notion importante. Les onglets appartiennent à une « propertyCategory » et vont déterminer les onglets affichés à gauche (sur la capture d'écran : « General », « Documentation », « Behavior »…) Enfin, les onglets contiennent des « sections ». Ces sections sont attachées à une classe (par exemple, Airline) et vont gérer l'affichage des composants graphiques dans l'onglet. On va donc pouvoir avoir :

- un plugin qui contribue des onglets ou des sections dans les vues d'un autre plugin, uniquement en connaissant le « contributorId » ;

- des onglets qui vont pouvoir afficher les propriétés d'objets de types différents, au travers des sections ;

- la réutilisation des composants graphiques sans tracas.

IV-B - Mise en place des extensions

Pour commencer, il faut importer dans les dépendances de votre plugin, le plugin « org.eclipse.ui.views.properties.tabbed ». On va ensuite pouvoir définir notre contributeur en créant l'extension « propertyContributor » :

Notez que seul l'attribut ID est obligatoire. Il est par contre très important. Définissez ensuite au moins une category avec un ID aussi. Cette catégorie nous permettra de classer les onglets. La deuxième étape est de créer ces onglets grâce au point d'extension « propertyTabs ». Notez la réutilisation du contributorID.

Cet onglet est attaché à une catégorie (réutilisez l'ID défini précédemment), et définit lui-même un ID. Enfin, la dernière étape est de créer les sections. Nous en créerons deux, une pour les objets Airline, une autre pour les objets Plane. Notez qu'elles sont toutes deux attachées au même onglet « General » défini juste avant.

La partie déclarative est terminée ! Il ne nous reste plus qu'à compléter le code des classes AirlinePropertySection et PlanePropertySection, définies dans le champ « class » des points d'extension.

IV-C - Écriture du code

Ces classes suivent un schéma de cycle de vie décrit dans la Javadoc de l'interface ISection :

- ISection.createControls() crée les composants graphiques ;

- ISection.setInput() donne à la section la sélection courante du workbench ;

- ISection.aboutToBeShown() lorsque l'onglet qui contient la section est activé ;

- ISection.refresh() lorsqu'un changement de sélection ou un changement d'onglet est effectué ;

- ISection.aboutToBeHidden() lorsque l'onglet qui contient la section est sur le point de ne plus être affiché ;

- ISection.dispose() lorsque la section est détruite.

Notez que l'interface ISection ne doit pas être implémentée directement, mais les classes doivent étendre la classe mère AbstractPropertySection.

Commençons par la classe AirlinePropertySection. Nous utilisons ici un FormLayout pour en montrer le fonctionnement.

1. package com.abernard.properties.tabbed;

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

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

5. import org.eclipse.swt.SWT;

6. import org.eclipse.swt.custom.CLabel;

7. import org.eclipse.swt.events.ModifyEvent;

8. import org.eclipse.swt.events.ModifyListener;

9. import org.eclipse.swt.layout.FormAttachment;

10. import org.eclipse.swt.layout.FormData;

11. import org.eclipse.swt.widgets.Composite;

12. import org.eclipse.swt.widgets.Text;

13. import org.eclipse.ui.IWorkbenchPart;

14. import org.eclipse.ui.views.properties.tabbed.AbstractPropertySection;

15. import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;

17. import com.abernard.properties.data.Airline;

19. /**

20. * La section qui affichera les proprietes d'un objet {@link Airline}.

21. * @author alain

22. *

23. */

24. public class AirlinePropertySection extends AbstractPropertySection {

26. private Text airlineName;

27. private Text oaciCode;

28. private Airline input;

30. public AirlinePropertySection() {

32. }

34. @Override

35. public void createControls(Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) {

36. super.createControls(parent, aTabbedPropertySheetPage);

38. Composite composite = getWidgetFactory()

39. .createFlatFormComposite(parent);

41. CLabel labelText = getWidgetFactory()

42. .createCLabel(composite, "Airline:"); //$NON-NLS-1$

44. airlineName = getWidgetFactory().createText(composite, ""); //$NON-NLS-1$

45. airlineName.addModifyListener(new ModifyListener() {

46. @Override

47. public void modifyText(ModifyEvent e) {

48. input.setName(airlineName.getText());

49. }

50. });

52. CLabel labelText2 = getWidgetFactory()

53. .createCLabel(composite, "OACI:"); //$NON-NLS-1$

55. oaciCode = getWidgetFactory().createText(composite, ""); //$NON-NLS-1$

56. oaciCode.addModifyListener(new ModifyListener() {

57. @Override

58. public void modifyText(ModifyEvent e) {

59. input.setOaci(oaciCode.getText());

60. }

61. });

64. FormData data = new FormData();

65. data.top = new FormAttachment(airlineName, 0, SWT.CENTER);

66. labelText.setLayoutData(data);

68. data = new FormData();

69. data.left = new FormAttachment(labelText, 5);

70. data.right = new FormAttachment(100, 0);

71. airlineName.setLayoutData(data);

73. data = new FormData();

74. data.top = new FormAttachment(oaciCode, 0, SWT.CENTER);

75. labelText2.setLayoutData(data);

77. data = new FormData();

78. data.top = new FormAttachment(airlineName, 5);

79. data.left = new FormAttachment(airlineName, 0, SWT.LEFT);

80. data.right = new FormAttachment(100, 0);

81. oaciCode.setLayoutData(data);

83. }

85. @Override

86. public void setInput(IWorkbenchPart part, ISelection selection) {

87. super.setInput(part, selection);

88. IStructuredSelection ssel = (IStructuredSelection)selection;

89. if (!ssel.isEmpty() && ssel.getFirstElement() instanceof Airline) {

90. input = (Airline)ssel.getFirstElement();

91. airlineName.setText(input.getName());

92. oaciCode.setText(input.getOaci());

93. }

95. }

96. }

Puis la classe PlanePropertySection, dans laquelle nous avons simplifié au maximum. Libre à vous d'aller plus loin !

1. package com.abernard.properties.tabbed;

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

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

5. import org.eclipse.swt.SWT;

6. import org.eclipse.swt.layout.GridData;

7. import org.eclipse.swt.layout.GridLayout;

8. import org.eclipse.swt.widgets.Composite;

9. import org.eclipse.swt.widgets.Text;

10. import org.eclipse.ui.IWorkbenchPart;

11. import org.eclipse.ui.views.properties.tabbed.AbstractPropertySection;

12. import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;

14. import com.abernard.properties.data.Plane;

16. /**

17. * La section qui affichera les proprietes d'un objet {@link Plane}.

18. * @author alain

19. *

20. */

21. public class PlanePropertySection extends AbstractPropertySection {

23. private Text planeName;

24. private Text planeManuf;

25. private Plane input;

27. public PlanePropertySection() {

28. //

29. }

31. @Override

32. public void createControls(Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) {

33. super.createControls(parent, aTabbedPropertySheetPage);

34. Composite composite = getWidgetFactory()

35. .createFlatFormComposite(parent);

36. composite.setLayout(new GridLayout(2, false));

38. getWidgetFactory().createCLabel(composite, "Name: ");

39. planeName = getWidgetFactory().createText(composite, "");

40. planeName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

42. getWidgetFactory().createCLabel(composite, "Manufacturer: ");

43. planeManuf = getWidgetFactory().createText(composite, "");

44. planeManuf.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

46. }

48. @Override

49. public void setInput(IWorkbenchPart part, ISelection selection) {

50. super.setInput(part, selection);

51. IStructuredSelection ssel = (IStructuredSelection)selection;

52. if (!ssel.isEmpty() && ssel.getFirstElement() instanceof Plane) {

53. input = (Plane)ssel.getFirstElement();

54. planeName.setText(input.getName());

55. planeManuf.setText(input.getManufacturer().toString());

56. }

57. }

59. }

Enfin, il faut adapter la vue pour déclencher l'affichage de la vue Property par onglets lors de la sélection. Il faut que cette vue implémente ITabbedPropertySheetPageContributor, et renvoie dans la méthode idoine le « contributorID ». Il faut aussi que cette vue instancie la page de Property dans la méthode getAdapter. On peut donc adapter notre vue de la manière suivante :

1. package com.abernard.properties.tabbed;

3. import java.util.List;

5. import org.eclipse.jface.viewers.LabelProvider;

6. import org.eclipse.jface.viewers.TreeViewer;

7. import org.eclipse.swt.SWT;

8. import org.eclipse.swt.layout.FillLayout;

9. import org.eclipse.swt.widgets.Composite;

10. import org.eclipse.ui.part.ViewPart;

11. import org.eclipse.ui.views.properties.IPropertySheetPage;

12. import org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor;

13. import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;

15. import com.abernard.properties.data.Airline;

16. import com.abernard.properties.data.DataGenerator;

17. import com.abernard.properties.data.Plane;

18. import com.abernard.properties.simple.AirlineTreeContentProvider;

20. /**

21. * Une vue basique qui affiche simplement nos donnees.

22. * @author A. BERNARD

23. *

24. */

25. public class ModelViewTabbed extends ViewPart implements ITabbedPropertySheetPageContributor {

27. private List<Airline> data;

28. private TreeViewer viewer;

30. public ModelViewTabbed() {

31. this.data = DataGenerator.generateData();

32. }

36. @Override

37. public void createPartControl(Composite parent) {

38. parent.setLayout(new FillLayout(SWT.HORIZONTAL));

40. viewer = new TreeViewer(parent, SWT.BORDER | SWT.FULL_SELECTION);

42. viewer.setContentProvider(new AirlineTreeContentProvider());

43. viewer.setLabelProvider(new LabelProvider() {

44. @Override

45. public String getText(Object element) {

46. if (element instanceof Airline) {

47. return ((Airline)element).getName() + " (" + ((Airline)element).getOaci() + ")";

48. } else if (element instanceof Plane) {

49. return ((Plane)element).getManufacturer().toString() + " " + ((Plane)element).getName();

50. }

51. return super.getText(element);

52. }

53. });

54. viewer.setInput(data);

55. // Informer le workbench a chaque selection

56. getSite().setSelectionProvider(viewer);

57. }

59. @Override

60. public void setFocus() {

61. viewer.getTree().setFocus();

62. }

64. @SuppressWarnings("unchecked")

65. @Override

66. public <T> T getAdapter(Class<T> adapter) {

67. if (adapter == IPropertySheetPage.class) {

68. return (T) new TabbedPropertySheetPage(this);

69. }

70. return super.getAdapter(adapter);

71. }

73. @Override

74. public String getContributorId() {

75. // Attention a bien indiquer l'ID du propertyContributor defini dans le plugin.xml !

76. return "com.abernard.properties.airlinesContributor";

77. }

79. }

IV-D - Test de nos vues

Il ne reste plus qu'à lancer l'application ! En sélectionnant les objets dans la vue, les pages de propriétés par onglets devraient s'afficher correctement.

Suivant les méthodes, et si les objets sont complexes, vous aurez sans doute constaté qu'il peut vite être fastidieux de construire ces pages de propriétés. Générer vos objets métier avec EMF vous donnera accès à des mécanismes de génération automatiques de ces vues, comme nous allons le voir dans les paragraphes suivants.

V - La vue Properties basique avec EMF

Vous avez sans doute déjà remarqué que la création d'un modèle EMF utilise intensivement la vue Properties, de même que l'éditeur généré. Il est possible de personnaliser la vue Properties générée directement depuis le fichier de génération du modèle EMF !

Pour suivre ce qui suit, il est indispensable d'avoir une connaissance de base en EMF, puis dans la création d'éditeurs Sirius pour la dernière partie. Pour aller plus rapidement, nous utiliserons les projets déjà utilisés dans le tutoriel sur Eclipse Sirius, dont les sources sont disponibles ici.

Ces projets contiennent un modèle d'aéroports, reliés entre eux par des lignes, partant d'une porte de l'aéroport d'origine, vers une porte de l'aéroport d'arrivée.

Le diagramme de classes est le suivant :

Et un exemple de modèle ouvert dans un éditeur Sirius (fichier airports.example du plugin com.abernard.airports.example) :

V-A - Modifier la génération EMF

Dans le plugin com.abernard.airports que vous avez récupéré, ouvrez le fichier .genmodel. Sélectionnez un attribut du modèle et observez le contenu de la vue Properties. Un certain nombre d'éléments font directement référence aux propriétés :

On retrouve par exemple la référence à « Property Category », qui va donc nous permettre de répartir nos propriétés en attributs. De même, le champ « Property Type » va nous permettre d'indiquer si cette propriété peut être modifiée ou non. Dans le champ « Property Filter-flags », vous pouvez indiquer si la propriété fait partie des propriétés « avancées », c'est-à-dire celles qui ne sont affichées que lorsqu'on clique sur le bouton « Show advanced properties » dans la vue Properties. Cela se fait en ajoutant la constante « org.eclipse.ui.views.properties.expert ». Par exemple, pour le champ « City » de l'objet Airport, on obtient :

On peut donc modifier directement l'agencement de nos propriétés via ce fichier, et relancer la génération du plugin « edit ».

V-B - Observer le résultat

Dans le runtime, on obtiendra alors, si on ouvre notre fichier .airports avec l'éditeur EMF par défaut :

Vous pouvez évidemment aller plus loin avec la personnalisation de cette vue Properties. Sachez par exemple que pour modifier la liste affichée pour la sélection des propriétés « Destination » et « Origin » des objets de type « Gate », il vous faudra surcharger la méthode « addDestinationPropertyDescriptor » de la classe « GateItemProvider » pour renvoyer votre propre sous-classe de ItemPropertyDescriptor, et y surcharger la méthode « getChoiceOfValues » pour effectuer vos traitements personnalisés. Ceci requiert une connaissance même basique du framework EMF.Edit, au sujet duquel vous trouverez une introduction ici.

VI - Une vue Property sans code avec Eclipse Sirius

Ceci vous permettra de gérer les propriétés dans la vue Properties basique, mais pas encore de générer une interface automatiquement, pour avoir le même type de rendu que la vue « Tabbed Properties ». C'est là qu'entre en jeu Eclipse Sirius. En effet, depuis sa version 4.0, packagée avec Eclipse Neon, l'outil de génération de modeleurs graphiques s'est enrichi de la définition déclarative des vues Properties, dans le même esprit que le reste de l'éditeur. Cette partie a vocation à donner les premières pistes et indications pour construire vos propres vues !

Ce tutoriel a été écrit avec la version 4.1.1 de Sirius, disponible ici : http://download.eclipse.org/sirius/updates/releases/4.1.1/neon

VI-A - Le modèle Sirius

Comme pour toute spécification Sirius, la première étape est de lancer un runtime Eclipse, et d'importer dans le workspace de ce runtime le projet com.abernard.airports.design pour travailler sur le fichier .odesign. Dans le même temps, on importe le projet com.abernard.airports.example pour pouvoir ouvrir le « new Airports diagram » qui y est défini. Deux méthodes existent pour générer ses vues Properties. La première et la plus simple consiste à faire confiance à Sirius. Pour cela dans le fichier .odesign, faites un clic droit sur le nœud racine de votre Viewpoint, puis sélectionnez « New > Default Properties View Description ».

Sirius va générer automatiquement des composants graphiques suivant la construction de votre modèle EMF. Comme d'habitude avec Sirius, il suffit d'ouvrir une représentation pour voir le résultat en direct :

Notez que dans le cas des références, Sirius propose directement les boutons pour associer un autre élément ou en créer un nouveau. Évidemment, cette génération automatique ne suffira pas dans la plupart des cas, nous pouvons donc construire nos vues Properties nous-mêmes !

VI-B - La mise en pratique

Dans le fichier odesign, faites un clic droit sur le nœud racine et sélectionnez « New > Properties View Description ». Deux éléments sont créés par défaut : une « Page » et un « Group ». Traduit en langage de la vue Properties, une page est un onglet (le « Tab » de la vue « TabbedProperties »), un groupe est une section. Si on clique sur l'élément Page, on observera qu'il référence le groupe créé par défaut. Chacun de ces éléments est associé à un objet de votre modèle, spécifié dans le champ « Domain Class ». Nous allons dans cet exemple créer deux onglets (donc « Page ») différents pour les objets « Airport » et « Gate ». Voilà ce que donnent leurs propriétés :

Nous avons initialisé aussi deux groupes pour afficher les propriétés.

Vous pouvez d'ores et déjà tester ces modifications. En ouvrant la représentation diagramme d'un exemple de votre modèle, vous pourrez observer ces vues Propriétés, évidemment vides.

Nous pouvons ensuite ajouter des éléments graphiques à ces deux vues. Cela se fait directement dans les éléments de type « Group », via un clic droit « New Widget > … ». Par exemple pour le nom de l'aéroport on peut créer un champ texte « New Widget > Text ».

Le premier champ est le label du champ texte, le deuxième permet de récupérer dans le modèle la valeur à afficher dans le champ texte. Le troisième permet de définir un tooltip d'aide qui sera affiché sur l'élément, et enfin le dernier permet de définir dans quelle condition le champ est éditable. Tous les champs proposent la complétion, et il faut aussi savoir que la variable « self » permet de faire référence à l'objet courant de votre modèle. En sélectionnant un aéroport dans le diagramme, vous pourrez observer que la propriété est affichée correctement.

Si vous modifiez le texte, rien ne va se passer dans le modèle. En effet, il faut indiquer à Sirius la manière dont le modèle doit être modifié. Pour cela, créez une « New Operation > Begin » sous votre champ texte dans le fichier odesign. Puis définissez une action « Set ». Ces actions sont exactement les mêmes que celles qui sont utilisées dans d'autres endroits de Sirius, elles fonctionnent à l'identique. Il faut donc indiquer la feature du modèle EMF à modifier, et la valeur à lui donner. Le point important à retenir est que la variable « newValue » vous permet de référencer la valeur entrée par l'utilisateur dans n'importe quel type de widget affiché dans vos vues Properties.

On peut tester cette opération et vérifier que le modèle se met bien à jour. Voilà une première propriété construite sans aucune ligne de code. Je vous laisse explorer les autres éléments graphiques, et nous allons dans cet article uniquement détailler encore le widget de type « référence », en prenant l'exemple des connexions entre les portes de nos aéroports. Chaque objet « Gate » possède une destination et une origine, identifiées par les features EMF « destination » et « origin ». Dans le groupe correspondant aux portes, nous pouvons donc créer deux nouveaux widgets de type « Reference ».

On y retrouve trois champs classiques pour le label, le tooltip d'aide et l'activation du champ. Deux autres sont présents : « reference owner expression » permet d'indiquer à Sirius quel objet contiendra la référence. Par défaut, ce champ vaut « var:self » donc l'objet courant. Le champ « Reference name expression » contient le nom de la feature EMF de la référence. Quand ces éléments sont renseignés, on peut observer le résultat :

Vous noterez les trois boutons présents sur la droite par défaut : ils donnent accès à l'ajout, la suppression ou la modification de la référence de manière directe. Dans le cas d'une référence multiple, deux boutons supplémentaires permettront de réagencer l'ordre des éléments. Inutile donc d'associer des opérations comme dans le cas du champ texte. Notez que si vous avez besoin d'un comportement plus spécifique, l'élément « Hyperlink » vous permet de définir une action personnalisée.

Avec ces mécanismes, vous pouvez très rapidement construire des vues Properties si vous utilisez Sirius, vous pouvez par ailleurs créer vos propres widgets personnalisés grâce à un point d'extension. Vous trouverez toutes les informations nécessaires dans la documentation officielle mentionnée en fin de cet article.

VII - Conclusion

Dans ce tutoriel, nous avons vu quatre méthodes pour construire sa vue Properties. La première, la plus simple et rapide à mettre en place conviendra très bien si on souhaite afficher des propriétés simples sur des objets peu partagés avec d'autres plugins. La deuxième est beaucoup plus flexible et extensible, et conviendra mieux à construire des vues Properties plus complexes, avec un design personnalisé. Dès que l'on entre dans le monde EMF, la génération de code nous permet de nous affranchir de la construction de la vue basique à la main. Enfin, la construction des propriétés avec Eclipse Sirius est un formidable gain de productivité, pour peu que votre modèle soit basé sur EMF et que vous utilisiez Sirius.

VIII - Liens utiles

Pour aller plus loin dans les sujets que nous venons d'aborder, voici une liste de liens utiles :

- la vue Properties « simple » ;

- plus de détails sur les Adapters dans Eclipse (à partir de la slide 78) ;

- la vue « Tabbed Properties » d'Eclipse ;

- comprendre les Layouts, particulièrement le FormLayout ;

- spécifier les propriétés dans Sirius.

IX - Remerciements

Je tiens à remercier Mickaël BARON et f-leb pour leur relecture technique et orthographique attentive.