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.
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
package
com.abernard.properties.data;
import
java.util.ArrayList;
import
java.util.List;
/**
* Une compagnie aerienne avec sa flotte.
*
@author
A. BERNARD
*
*/
public
class
Airline {
private
List<
Plane>
fleet;
private
String oaci;
private
String name;
public
Airline
(
String oaci, String name) {
this
.fleet =
new
ArrayList<
Plane>(
);
this
.oaci =
oaci;
this
.name =
name;
}
/**
*
@return
the fleet
*/
public
List<
Plane>
getFleet
(
) {
return
fleet;
}
/**
*
@param
fleet
the fleet to set
*/
public
void
setFleet
(
List<
Plane>
fleet) {
this
.fleet =
fleet;
}
/**
*
@return
the oaci
*/
public
String getOaci
(
) {
return
oaci;
}
/**
*
@param
oaci
the oaci to set
*/
public
void
setOaci
(
String oaci) {
this
.oaci =
oaci;
}
/**
*
@return
the name
*/
public
String getName
(
) {
return
name;
}
/**
*
@param
name
the name to set
*/
public
void
setName
(
String name) {
this
.name =
name;
}
}
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.
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
package
com.abernard.properties.data;
/**
* Informations a propos d'un modele d'avion dans une compagnie.
*
@author
A. BERNARD
*
*/
public
class
Plane {
private
Airline airline;
private
String name;
private
int
count;
private
int
paxCount;
private
int
orders;
private
Manufacturer manufacturer;
public
Plane
(
Airline al, Manufacturer m, String name, int
count, int
pax, int
orders) {
this
.airline =
al;
this
.manufacturer =
m;
this
.name =
name;
this
.count =
count;
this
.paxCount =
pax;
this
.orders =
orders;
}
/**
*
@return
the name
*/
public
String getName
(
) {
return
name;
}
/**
*
@param
name
the name to set
*/
public
void
setName
(
String name) {
this
.name =
name;
}
/**
*
@return
the count
*/
public
int
getCount
(
) {
return
count;
}
/**
*
@param
count
the count to set
*/
public
void
setCount
(
int
count) {
this
.count =
count;
}
/**
*
@return
the paxCount
*/
public
int
getPaxCount
(
) {
return
paxCount;
}
/**
*
@param
paxCount
the paxCount to set
*/
public
void
setPaxCount
(
int
paxCount) {
this
.paxCount =
paxCount;
}
/**
*
@return
the orders
*/
public
int
getOrders
(
) {
return
orders;
}
/**
*
@param
orders
the orders to set
*/
public
void
setOrders
(
int
orders) {
this
.orders =
orders;
}
/**
*
@return
the airline
*/
public
Airline getAirline
(
) {
return
airline;
}
/**
*
@return
the manufacturer
*/
public
Manufacturer getManufacturer
(
) {
return
manufacturer;
}
/**
*
@param
manufacturer
the manufacturer to set
*/
public
void
setManufacturer
(
Manufacturer manufacturer) {
this
.manufacturer =
manufacturer;
}
}
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 :
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
package
com.abernard.properties.data;
import
java.util.ArrayList;
import
java.util.List;
/**
* Classe utilitaire pour generer quelques exemples.
*
@author
alain
*
*/
public
final
class
DataGenerator {
private
DataGenerator
(
) {}
/**
* Genere une liste d'
{@link
Airline
}
.
*
@return
*/
public
static
List<
Airline>
generateData
(
) {
List<
Airline>
data =
new
ArrayList<
Airline>(
);
Airline al =
new
Airline
(
"AFR"
, "Air France"
);
Plane p =
new
Plane
(
al, Manufacturer.AIRBUS, "A350-900"
, 0
, 314
, 18
);
al.getFleet
(
).add
(
p);
p =
new
Plane
(
al, Manufacturer.AIRBUS, "A380-800"
, 10
, 516
, 2
);
al.getFleet
(
).add
(
p);
p =
new
Plane
(
al, Manufacturer.EMBRAER, "170"
, 54
, 76
, 8
);
al.getFleet
(
).add
(
p);
data.add
(
al);
al =
new
Airline
(
"QTR"
, "Qatar Airways"
);
p =
new
Plane
(
al, Manufacturer.AIRBUS, "A330-300"
, 13
, 305
, 0
);
al.getFleet
(
).add
(
p);
p =
new
Plane
(
al, Manufacturer.BOEING, "B787-800"
, 17
, 254
, 13
);
al.getFleet
(
).add
(
p);
data.add
(
al);
al =
new
Airline
(
"DLH"
, "Lufthansa"
);
p =
new
Plane
(
al, Manufacturer.BOEING, "B747-400"
, 19
, 344
, 0
);
al.getFleet
(
).add
(
p);
p =
new
Plane
(
al, Manufacturer.AIRBUS, "A340-600"
, 23
, 346
, 0
);
al.getFleet
(
).add
(
p);
data.add
(
al);
return
data;
}
}
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.
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
package
com.abernard.properties.simple;
import
org.eclipse.core.runtime.IAdapterFactory;
import
org.eclipse.ui.views.properties.IPropertySource;
import
com.abernard.properties.data.Airline;
import
com.abernard.properties.data.Plane;
/**
* Cette classe transforme nos objets metier en
{@link
IPropertySource
}
.
*
@author
alain
*
*/
public
class
AirlinesAdapterFactory implements
IAdapterFactory {
private
static
final
Class<
?>
[] TYPES =
{
IPropertySource.class
}
;
@SuppressWarnings
(
"rawtypes"
)
@Override
public
Object getAdapter
(
Object adaptableObject, Class adapterType) {
if
(
adapterType ==
IPropertySource.class
) {
if
(
adaptableObject instanceof
Airline) {
return
new
AirlinePropertySourceAdapter
((
Airline)adaptableObject);
}
if
(
adaptableObject instanceof
Plane) {
return
new
PlanePropertySourceAdapter
((
Plane)adaptableObject);
}
}
return
null
;
}
@Override
public
Class<
?>
[] getAdapterList
(
) {
return
TYPES;
}
}
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.
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
package
com.abernard.properties.simple;
import
org.eclipse.ui.views.properties.IPropertyDescriptor;
import
org.eclipse.ui.views.properties.IPropertySource2;
import
org.eclipse.ui.views.properties.PropertyDescriptor;
import
org.eclipse.ui.views.properties.TextPropertyDescriptor;
import
com.abernard.properties.data.Airline;
/**
* Cette classe permet de decrire les differentes proprietes de nos objets
{@link
Airline
}
.
*
@author
alain
*
*/
public
class
AirlinePropertySourceAdapter implements
IPropertySource2 {
private
static
final
String PROP_OACI =
"oaci"
;
private
static
final
String PROP_AL_NAME =
"al_name"
;
private
Airline airline;
public
AirlinePropertySourceAdapter
(
Airline airline) {
this
.airline =
airline;
}
@Override
public
Object getEditableValue
(
) {
return
null
;
}
@Override
public
IPropertyDescriptor[] getPropertyDescriptors
(
) {
PropertyDescriptor oaciDescr =
new
PropertyDescriptor
(
PROP_OACI, "Code OACI"
);
PropertyDescriptor nameDescr =
new
TextPropertyDescriptor
(
PROP_AL_NAME, "Name"
);
return
new
IPropertyDescriptor[] {
oaciDescr, nameDescr}
;
}
@Override
public
Object getPropertyValue
(
Object id) {
switch
((
String)id) {
case
PROP_AL_NAME:
return
airline.getName
(
);
case
PROP_OACI:
return
airline.getOaci
(
);
}
return
null
;
}
@Override
public
boolean
isPropertySet
(
Object id) {
return
true
;
}
@Override
public
void
resetPropertyValue
(
Object id) {
if
(
id.equals
(
PROP_AL_NAME)) {
airline.setName
(
"no_name"
);
}
}
@Override
public
void
setPropertyValue
(
Object id, Object value) {
if
(
id.equals
(
PROP_AL_NAME) &&
value instanceof
String) {
airline.setName
((
String)value);
}
}
@Override
public
boolean
isPropertyResettable
(
Object id) {
return
(
id.equals
(
PROP_AL_NAME));
}
}
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 :
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
package
com.abernard.properties.simple;
import
java.util.Arrays;
import
org.eclipse.ui.views.properties.ComboBoxPropertyDescriptor;
import
org.eclipse.ui.views.properties.IPropertyDescriptor;
import
org.eclipse.ui.views.properties.IPropertySource2;
import
org.eclipse.ui.views.properties.PropertyDescriptor;
import
org.eclipse.ui.views.properties.TextPropertyDescriptor;
import
com.abernard.properties.data.Manufacturer;
import
com.abernard.properties.data.Plane;
/**
* Cette classe permet de decrire les differentes proprietes de nos objets
{@link
Plane
}
.
*
@author
alain
*
*/
public
class
PlanePropertySourceAdapter implements
IPropertySource2 {
private
Plane plane;
private
static
final
String PROP_NAME =
"plane.name"
;
private
static
final
String PROP_MANUF =
"plane.manuf"
;
private
static
final
String PROP_COUNT =
"plane.count"
;
private
static
final
String PROP_ORDERS =
"plane.orders"
;
private
static
final
String PROP_PAX =
"plane.pax"
;
public
PlanePropertySourceAdapter
(
Plane plane) {
this
.plane =
plane;
}
@Override
public
Object getEditableValue
(
) {
return
null
;
}
@Override
public
IPropertyDescriptor[] getPropertyDescriptors
(
) {
TextPropertyDescriptor nameDescr =
new
TextPropertyDescriptor
(
PROP_NAME, "Name"
);
nameDescr.setCategory
(
"General"
);
String[] labels =
new
String[Manufacturer.values
(
).length];
for
(
int
i =
0
; i <
labels.length; i++
) {
labels[i] =
Manufacturer.values
(
)[i].name
(
);
}
ComboBoxPropertyDescriptor manufDescr =
new
ComboBoxPropertyDescriptor
(
PROP_MANUF, "Manufacturer"
, labels);
manufDescr.setCategory
(
"General"
);
TextPropertyDescriptor countDescr =
new
TextPropertyDescriptor
(
PROP_COUNT, "Count"
);
countDescr.setCategory
(
"Specific"
);
TextPropertyDescriptor ordersDescr =
new
TextPropertyDescriptor
(
PROP_ORDERS, "Orders"
);
ordersDescr.setCategory
(
"Specific"
);
PropertyDescriptor paxDescr =
new
PropertyDescriptor
(
PROP_PAX, "Pax"
);
paxDescr.setCategory
(
"Specific"
);
return
new
IPropertyDescriptor[] {
nameDescr, manufDescr, ordersDescr, countDescr, paxDescr}
;
}
@Override
public
Object getPropertyValue
(
Object id) {
switch
((
String)id) {
case
PROP_NAME:
return
plane.getName
(
);
case
PROP_MANUF:
return
Arrays.binarySearch
(
Manufacturer.values
(
), plane.getManufacturer
(
));
case
PROP_ORDERS:
return
String.valueOf
(
plane.getOrders
(
));
case
PROP_COUNT:
return
String.valueOf
(
plane.getCount
(
));
case
PROP_PAX:
return
String.valueOf
(
plane.getPaxCount
(
));
}
return
null
;
}
@Override
public
void
resetPropertyValue
(
Object id) {
switch
((
String)id) {
case
PROP_ORDERS:
plane.setOrders
(
0
);
break
;
case
PROP_COUNT:
plane.setCount
(
0
);
break
;
case
PROP_NAME:
plane.setName
(
"no_name"
);
break
;
}
}
@Override
public
void
setPropertyValue
(
Object id, Object value) {
switch
((
String)id) {
case
PROP_MANUF:
plane.setManufacturer
(
Manufacturer.values
(
)[(
int
)value]);
break
;
case
PROP_ORDERS:
plane.setOrders
(
Integer.parseInt
((
String)value));
break
;
case
PROP_COUNT:
plane.setCount
(
Integer.parseInt
((
String)value));
break
;
case
PROP_NAME:
plane.setName
((
String)value);
break
;
}
}
@Override
public
boolean
isPropertyResettable
(
Object id) {
switch
((
String)id) {
case
PROP_ORDERS:
case
PROP_COUNT:
case
PROP_NAME:
return
true
;
default
:
return
false
;
}
}
@Override
public
boolean
isPropertySet
(
Object id) {
return
true
;
}
}
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.
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
package
com.abernard.properties.simple;
import
java.util.List;
import
org.eclipse.jface.viewers.LabelProvider;
import
org.eclipse.jface.viewers.TreeViewer;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.layout.FillLayout;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.ui.part.ViewPart;
import
com.abernard.properties.data.Airline;
import
com.abernard.properties.data.DataGenerator;
import
com.abernard.properties.data.Plane;
/**
* Une vue basique qui affiche simplement nos donnees.
*
@author
A. BERNARD
*
*/
public
class
ModelView extends
ViewPart {
private
List<
Airline>
data;
private
TreeViewer viewer;
public
ModelView
(
) {
this
.data =
DataGenerator.generateData
(
);
}
@Override
public
void
createPartControl
(
Composite parent) {
parent.setLayout
(
new
FillLayout
(
SWT.HORIZONTAL));
viewer =
new
TreeViewer
(
parent, SWT.BORDER |
SWT.FULL_SELECTION);
viewer.setContentProvider
(
new
AirlineTreeContentProvider
(
));
viewer.setLabelProvider
(
new
LabelProvider
(
) {
@Override
public
String getText
(
Object element) {
if
(
element instanceof
Airline) {
return
((
Airline)element).getName
(
) +
" ("
+
((
Airline)element).getOaci
(
) +
")"
;
}
else
if
(
element instanceof
Plane) {
return
((
Plane)element).getManufacturer
(
).toString
(
) +
" "
+
((
Plane)element).getName
(
);
}
return
super
.getText
(
element);
}
}
);
viewer.setInput
(
data);
// Informer le workbench a chaque selection
getSite
(
).setSelectionProvider
(
viewer);
}
@Override
public
void
setFocus
(
) {
viewer.getTree
(
).setFocus
(
);
}
}
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.
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
package
com.abernard.properties.tabbed;
import
org.eclipse.jface.viewers.ISelection;
import
org.eclipse.jface.viewers.IStructuredSelection;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.custom.CLabel;
import
org.eclipse.swt.events.ModifyEvent;
import
org.eclipse.swt.events.ModifyListener;
import
org.eclipse.swt.layout.FormAttachment;
import
org.eclipse.swt.layout.FormData;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.swt.widgets.Text;
import
org.eclipse.ui.IWorkbenchPart;
import
org.eclipse.ui.views.properties.tabbed.AbstractPropertySection;
import
org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;
import
com.abernard.properties.data.Airline;
/**
* La section qui affichera les proprietes d'un objet
{@link
Airline
}
.
*
@author
alain
*
*/
public
class
AirlinePropertySection extends
AbstractPropertySection {
private
Text airlineName;
private
Text oaciCode;
private
Airline input;
public
AirlinePropertySection
(
) {
}
@Override
public
void
createControls
(
Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) {
super
.createControls
(
parent, aTabbedPropertySheetPage);
Composite composite =
getWidgetFactory
(
)
.createFlatFormComposite
(
parent);
CLabel labelText =
getWidgetFactory
(
)
.createCLabel
(
composite, "Airline:"
); //$NON-NLS-1$
airlineName =
getWidgetFactory
(
).createText
(
composite, ""
); //$NON-NLS-1$
airlineName.addModifyListener
(
new
ModifyListener
(
) {
@Override
public
void
modifyText
(
ModifyEvent e) {
input.setName
(
airlineName.getText
(
));
}
}
);
CLabel labelText2 =
getWidgetFactory
(
)
.createCLabel
(
composite, "OACI:"
); //$NON-NLS-1$
oaciCode =
getWidgetFactory
(
).createText
(
composite, ""
); //$NON-NLS-1$
oaciCode.addModifyListener
(
new
ModifyListener
(
) {
@Override
public
void
modifyText
(
ModifyEvent e) {
input.setOaci
(
oaciCode.getText
(
));
}
}
);
FormData data =
new
FormData
(
);
data.top =
new
FormAttachment
(
airlineName, 0
, SWT.CENTER);
labelText.setLayoutData
(
data);
data =
new
FormData
(
);
data.left =
new
FormAttachment
(
labelText, 5
);
data.right =
new
FormAttachment
(
100
, 0
);
airlineName.setLayoutData
(
data);
data =
new
FormData
(
);
data.top =
new
FormAttachment
(
oaciCode, 0
, SWT.CENTER);
labelText2.setLayoutData
(
data);
data =
new
FormData
(
);
data.top =
new
FormAttachment
(
airlineName, 5
);
data.left =
new
FormAttachment
(
airlineName, 0
, SWT.LEFT);
data.right =
new
FormAttachment
(
100
, 0
);
oaciCode.setLayoutData
(
data);
}
@Override
public
void
setInput
(
IWorkbenchPart part, ISelection selection) {
super
.setInput
(
part, selection);
IStructuredSelection ssel =
(
IStructuredSelection)selection;
if
(!
ssel.isEmpty
(
) &&
ssel.getFirstElement
(
) instanceof
Airline) {
input =
(
Airline)ssel.getFirstElement
(
);
airlineName.setText
(
input.getName
(
));
oaciCode.setText
(
input.getOaci
(
));
}
}
}
Puis la classe PlanePropertySection, dans laquelle nous avons simplifié au maximum. Libre à vous d'aller plus loin !
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
package
com.abernard.properties.tabbed;
import
org.eclipse.jface.viewers.ISelection;
import
org.eclipse.jface.viewers.IStructuredSelection;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.layout.GridData;
import
org.eclipse.swt.layout.GridLayout;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.swt.widgets.Text;
import
org.eclipse.ui.IWorkbenchPart;
import
org.eclipse.ui.views.properties.tabbed.AbstractPropertySection;
import
org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;
import
com.abernard.properties.data.Plane;
/**
* La section qui affichera les proprietes d'un objet
{@link
Plane
}
.
*
@author
alain
*
*/
public
class
PlanePropertySection extends
AbstractPropertySection {
private
Text planeName;
private
Text planeManuf;
private
Plane input;
public
PlanePropertySection
(
) {
//
}
@Override
public
void
createControls
(
Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) {
super
.createControls
(
parent, aTabbedPropertySheetPage);
Composite composite =
getWidgetFactory
(
)
.createFlatFormComposite
(
parent);
composite.setLayout
(
new
GridLayout
(
2
, false
));
getWidgetFactory
(
).createCLabel
(
composite, "Name: "
);
planeName =
getWidgetFactory
(
).createText
(
composite, ""
);
planeName.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
));
getWidgetFactory
(
).createCLabel
(
composite, "Manufacturer: "
);
planeManuf =
getWidgetFactory
(
).createText
(
composite, ""
);
planeManuf.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
));
}
@Override
public
void
setInput
(
IWorkbenchPart part, ISelection selection) {
super
.setInput
(
part, selection);
IStructuredSelection ssel =
(
IStructuredSelection)selection;
if
(!
ssel.isEmpty
(
) &&
ssel.getFirstElement
(
) instanceof
Plane) {
input =
(
Plane)ssel.getFirstElement
(
);
planeName.setText
(
input.getName
(
));
planeManuf.setText
(
input.getManufacturer
(
).toString
(
));
}
}
}
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 :
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.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
package
com.abernard.properties.tabbed;
import
java.util.List;
import
org.eclipse.jface.viewers.LabelProvider;
import
org.eclipse.jface.viewers.TreeViewer;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.layout.FillLayout;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.ui.part.ViewPart;
import
org.eclipse.ui.views.properties.IPropertySheetPage;
import
org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor;
import
org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;
import
com.abernard.properties.data.Airline;
import
com.abernard.properties.data.DataGenerator;
import
com.abernard.properties.data.Plane;
import
com.abernard.properties.simple.AirlineTreeContentProvider;
/**
* Une vue basique qui affiche simplement nos donnees.
*
@author
A. BERNARD
*
*/
public
class
ModelViewTabbed extends
ViewPart implements
ITabbedPropertySheetPageContributor {
private
List<
Airline>
data;
private
TreeViewer viewer;
public
ModelViewTabbed
(
) {
this
.data =
DataGenerator.generateData
(
);
}
@Override
public
void
createPartControl
(
Composite parent) {
parent.setLayout
(
new
FillLayout
(
SWT.HORIZONTAL));
viewer =
new
TreeViewer
(
parent, SWT.BORDER |
SWT.FULL_SELECTION);
viewer.setContentProvider
(
new
AirlineTreeContentProvider
(
));
viewer.setLabelProvider
(
new
LabelProvider
(
) {
@Override
public
String getText
(
Object element) {
if
(
element instanceof
Airline) {
return
((
Airline)element).getName
(
) +
" ("
+
((
Airline)element).getOaci
(
) +
")"
;
}
else
if
(
element instanceof
Plane) {
return
((
Plane)element).getManufacturer
(
).toString
(
) +
" "
+
((
Plane)element).getName
(
);
}
return
super
.getText
(
element);
}
}
);
viewer.setInput
(
data);
// Informer le workbench a chaque selection
getSite
(
).setSelectionProvider
(
viewer);
}
@Override
public
void
setFocus
(
) {
viewer.getTree
(
).setFocus
(
);
}
@SuppressWarnings
(
"unchecked"
)
@Override
public
<
T>
T getAdapter
(
Class<
T>
adapter) {
if
(
adapter ==
IPropertySheetPage.class
) {
return
(
T) new
TabbedPropertySheetPage
(
this
);
}
return
super
.getAdapter
(
adapter);
}
@Override
public
String getContributorId
(
) {
// Attention a bien indiquer l'ID du propertyContributor defini dans le plugin.xml !
return
"com.abernard.properties.airlinesContributor"
;
}
}
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 SiriusA la découverte d'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 :
IX. Remerciements▲
Je tiens à remercier Mickaël BARON et f-leb pour leur relecture technique et orthographique attentive.