I. Introduction▲
Dans cet article, nous étudierons comment mettre en place un système d'autocomplétion similaire à ce qui existe de base dans la plateforme Eclipse pour correspondre aux besoins spécifiques d'une application. Le principe d'autocomplétion dans Eclipse peut se découper en deux principales catégories : l'autocomplétion dans les éditeurs ou l'autocomplétion dans les composants SWT/JFace tels que les champs texte, comme le montrent les captures d'écran ci-dessous :
Cet article nécessite de posséder des connaissances sur :
II. L'autocomplétion sur des composants SWT▲
Dans cette partie nous détaillerons la mise en place de l'autocomplétion sur des champs texte SWT. Ce mécanisme est le plus simple à mettre en place tout en permettant de fournir rapidement aux utilisateurs de l'application une liste de propositions. Ce type d'assistant peut se mettre en place soit sur des champs texte soit sur des listes déroulantes. Cet assistant est déclenché soit par une combinaison de touches (par exemple le célèbre Ctrl+Espace), soit par une liste de caractères prédéfinis. Tout d'abord, nous allons mettre en œuvre ce mécanisme sur un exemple très simple.
II-A. Un premier exemple simple▲
Créez un nouveau plugin, avec une unique vue contenant un champ texte. Le contenu de la vue est donné ci-dessous. L'intégralité du comportement de l'assistant de contenu est défini dans la méthode « constructFieldAssist » :
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.
package
com.abernard.contentassist.swt;
import
org.eclipse.core.runtime.Status;
import
org.eclipse.jface.bindings.keys.KeyStroke;
import
org.eclipse.jface.bindings.keys.ParseException;
import
org.eclipse.jface.fieldassist.ContentProposalAdapter;
import
org.eclipse.jface.fieldassist.ControlDecoration;
import
org.eclipse.jface.fieldassist.FieldDecorationRegistry;
import
org.eclipse.jface.fieldassist.SimpleContentProposalProvider;
import
org.eclipse.jface.fieldassist.TextContentAdapter;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.ui.part.ViewPart;
import
org.eclipse.ui.statushandlers.StatusManager;
import
org.eclipse.swt.graphics.Image;
import
org.eclipse.swt.layout.GridLayout;
import
org.eclipse.swt.widgets.Label;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.widgets.Text;
import
org.eclipse.swt.layout.GridData;
import
com.abernard.contentassist.Activator;
/**
* Cette vue met en oeuvre un assistant de contenu basique sur un composant SWT.
*
@author
A. BERNARD
*
*/
public
class
ContentAssistView extends
ViewPart {
private
Text textAssist;
@Override
public
void
createPartControl
(
Composite parent) {
parent.setLayout
(
new
GridLayout
(
2
, false
));
Label label =
new
Label
(
parent, SWT.NONE);
label.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
, false
, 1
, 1
));
label.setText
(
"Enter your text here:"
);
// Definition du champ texte ou l'assistant de contenu sera propose
textAssist =
new
Text
(
parent, SWT.BORDER);
textAssist.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
// Creation d'un indicateur sur le champ texte
ControlDecoration deco =
new
ControlDecoration
(
textAssist, SWT.TOP
|
SWT.LEFT);
// L'image utilisee est l'image standard pour les decorateurs informatifs
Image image =
FieldDecorationRegistry.getDefault
(
)
.getFieldDecoration
(
FieldDecorationRegistry.DEC_INFORMATION)
.getImage
(
);
deco.setDescriptionText
(
"Utilisez CTRL + ESPACE pour voir la liste des mots-cles."
);
deco.setImage
(
image);
deco.setShowOnlyOnFocus
(
true
); // Ne montrer l'indicateur que lorsque le champ texte a le
focus
// Construction de l'assistant de contenu
constructFieldAssist
(
);
}
/**
* Construit l'assistant de contenu sur le champ texte
*/
private
void
constructFieldAssist
(
) {
// Definition des caracteres d'activation automatique
char
[] autoActivationCharacters =
new
char
[] {
'_'
}
;
try
{
// Definition du raccourci clavier d'activation
KeyStroke keyStroke =
KeyStroke.getInstance
(
"Ctrl+Space"
);
// Creation de l'assistant de contenu
SimpleContentProposalProvider provider =
new
SimpleContentProposalProvider
(
new
String[]
{
"Item_1"
, "Deuxieme_Item"
, "Item_n3"
}
);
// Filtre les propositions suivant le contenu du champ texte
provider.setFiltering
(
true
);
// Construit l'assistant de contenu
ContentProposalAdapter adapter =
new
ContentProposalAdapter
(
textAssist,
new
TextContentAdapter
(
), provider, keyStroke, autoActivationCharacters);
// Avec cette commande, le champ texte sera integralement remplace par le contenu
choisi
adapter.setProposalAcceptanceStyle
(
ContentProposalAdapter.PROPOSAL_REPLACE);
}
catch
(
ParseException e1) {
StatusManager.getManager
(
).handle
(
new
Status
(
Status.ERROR, Activator.PLUGIN_ID,
"Unable to create key stroke for content assist"
, e1), StatusManager.SHOW);
}
}
@Override
public
void
setFocus
(
) {
textAssist.setFocus
(
);
}
}
Nous pouvons visualiser le résultat en lançant notre plugin dans une application Eclipse :
Nous pouvons aussi constater que lorsqu'on entre le caractère '_' les éléments apparaissent, filtrés :
Détaillons ensemble le contenu de la méthode
« constructFieldAssist ».
En premier lieu, le tableau autoActivationChars nous permet de définir les
caractères d'activation automatique. Nous définissons aussi un
objet de type « KeyStroke » qui permet de définir
le raccourci de déclenchement de l'assistant de contenu.
Nous instancions ensuite un « SimpleContentProposalProvider »
qui permet de définir très simplement une liste de contenus
à partir d'une liste de mots. Nous verrons par la suite comment
définir un provider plus complexe.
Après cela nous créons le « ContentProposalAdapter »
qui va mettre en place l'assistant de contenu proprement dit.
Enfin, nous devons indiquer par quel moyen le contenu validé sera
inséré dans le champ texte : soit par le remplacement du
contenu existant (ContentProposalAdapter.PROPOSAL_REPLACE), soit par
insertion (ContentProposalAdapter.PROPOSAL_INSERT), soit pas de
modification du champ texte (ContentProposalAdapter.PROPOSAL_IGNORE). Ce
dernier cas est réservé aux champs dont le traitement de
l'insertion est plus complexe et nécessite des actions
spécifiques. On pourra être notifié de l'acceptation de
l'assistant un ajoutant un
« IContentProposalListener ». Ceci sera
détaillé dans le prochain exemple.
En effet, le comportement basique que nous venons de décrire manque de
raffinement pour des interfaces plus complexes, où l'information
proposée nécessite de donner plus d'informations.
II-B. Un exemple plus complexe▲
Dans ce deuxième exemple, nous allons insérer
des éléments de manière plus complexe.
Mettons par exemple que nous voulons proposer une liste
d'avions qui possèdent tous un certain nombre de
propriétés : le nom de l'avion, l'avionneur,
le nombre de membres d'équipage navigant, ainsi que le
nombre de moteurs. À la manière d'Eclipse, nous
souhaitons qu'un cadre affiche les informations
détaillées sur les différents
éléments disponibles à côté de la
liste affichée.
La première étape consiste à définir des objets qui
implémentent l'interface « IContentProposal ».
C'est cette interface qui est utilisée par le mécanisme
d'autocomplétion pour afficher les éléments dans la liste.
Nous définissons par exemple notre propre implémentation,
donnée ci-dessous, de cette interface pour nos avions :
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.
package
com.abernard.contentassist;
import
org.eclipse.jface.fieldassist.IContentProposal;
/**
* Cette classe donne une description des elements a afficher dans
* l'assistant de contenu.
*
@author
A. BERNARD
*
*/
public
class
PlaneProposal implements
IContentProposal {
private
String name;
private
int
crew;
private
int
engines;
private
String manufacturer;
/** Constructeur
*
@param
name
le nom de l'avion
*
@param
manufacturer
l'avionneur
*
@param
crew
le nombre de membres d'équipage
*
@param
engines
le nombre de moteurs
*/
public
PlaneProposal
(
String name, String manufacturer, int
crew, int
engines) {
this
.name =
name;
this
.manufacturer =
manufacturer;
this
.crew =
crew;
this
.engines =
engines;
}
@Override
public
String getContent
(
) {
// Cette methode doit retourner le contenu a placer dans le champ texte
return
name;
}
@Override
public
int
getCursorPosition
(
) {
// Cette methode retourne la position que doit avoir le curseur apres insertion
return
name.length
(
);
}
@Override
public
String getLabel
(
) {
// Le texte a afficher dans la liste des elements disponibles
return
name +
" ("
+
manufacturer +
")"
;
}
@Override
public
String getDescription
(
) {
// Cette description est affichee dans l'encart a cote de l'assistant
return
manufacturer +
" "
+
name +
" :
\n
Equipage : "
+
crew
+
"
\n
Nombre de moteurs: "
+
engines;
}
/**
* Donne l'avionneur
*
@return
*/
public
String getManufacturer
(
) {
return
manufacturer;
}
/**
* Donne le nombre de membres d'equipage
*
@return
*/
public
int
getCrew
(
) {
return
crew;
}
/**
* Donne le nombre de moteurs
*
@return
*/
public
int
getEngines
(
) {
return
engines;
}
}
Nous pouvons ensuite définir l'interface graphique où nous mettrons notre assistant de contenu en place. Cette interface comporte simplement quatre champs texte destinés à afficher les différentes informations sur l'avion qui aura été sélectionné. Seul le premier champ pour le nom de l'avion proposera l'autocomplétion. En tout premier lieu, nous créons la liste des avions disponibles en instanciant pour chaque avion l'objet de type « PlaneProposal » idoine.
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.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
package
com.abernard.contentassist.swt;
import
java.util.ArrayList;
import
java.util.List;
import
org.eclipse.core.runtime.Status;
import
org.eclipse.jface.bindings.keys.KeyStroke;
import
org.eclipse.jface.bindings.keys.ParseException;
import
org.eclipse.jface.fieldassist.ContentProposalAdapter;
import
org.eclipse.jface.fieldassist.ControlDecoration;
import
org.eclipse.jface.fieldassist.FieldDecorationRegistry;
import
org.eclipse.jface.fieldassist.IContentProposal;
import
org.eclipse.jface.fieldassist.IContentProposalListener;
import
org.eclipse.jface.fieldassist.IContentProposalProvider;
import
org.eclipse.jface.fieldassist.TextContentAdapter;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.graphics.Image;
import
org.eclipse.swt.layout.GridData;
import
org.eclipse.swt.layout.GridLayout;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.swt.widgets.Label;
import
org.eclipse.swt.widgets.Text;
import
org.eclipse.ui.part.ViewPart;
import
org.eclipse.ui.statushandlers.StatusManager;
import
com.abernard.contentassist.Activator;
import
com.abernard.contentassist.PlaneProposal;
/**
* Cette vue met en oeuvre un assistant de contenu plus avance sur un composant
* SWT.
*
@author
A. BERNARD
*
*/
public
class
ContentAssistAdvanced extends
ViewPart {
private
Text textAssist;
private
List<
PlaneProposal>
elements;
private
Text textManufacturer;
private
Text textCrew;
private
Text textEngines;
/**
* Constructeur. Initialise la liste des elements disponibles
*/
public
ContentAssistAdvanced
(
) {
elements =
new
ArrayList<
PlaneProposal>(
);
elements.add
(
new
PlaneProposal
(
"A320"
, "Airbus"
, 2
, 2
));
elements.add
(
new
PlaneProposal
(
"Rafale"
, "Dassault"
, 1
, 1
));
elements.add
(
new
PlaneProposal
(
"Falcon 7x"
, "Dassault"
, 3
, 3
));
elements.add
(
new
PlaneProposal
(
"B777"
, "Boeing"
, 4
, 2
));
elements.add
(
new
PlaneProposal
(
"A380"
, "Airbus"
, 2
, 4
));
}
@Override
public
void
createPartControl
(
Composite parent) {
parent.setLayout
(
new
GridLayout
(
4
, false
));
Label labelPlane =
new
Label
(
parent, SWT.NONE);
labelPlane.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
, false
, 1
, 1
));
labelPlane.setText
(
"Plane name:"
);
// Definition du champ texte ou l'assistant de contenu sera propose
textAssist =
new
Text
(
parent, SWT.BORDER);
textAssist.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
// Contruction de l'assistant de contenu
constructFieldAssist
(
);
// L'image utilisee est l'image standard pour les decorateurs informatifs
Image image =
FieldDecorationRegistry.getDefault
(
)
.getFieldDecoration
(
FieldDecorationRegistry.DEC_INFORMATION)
.getImage
(
);
// Creation d'un indicateur sur le champ texte
ControlDecoration deco =
new
ControlDecoration
(
textAssist, SWT.TOP
|
SWT.LEFT);
deco.setDescriptionText
(
"Utilisez CTRL + ESPACE pour voir la liste des mots-cles."
);
deco.setImage
(
image);
deco.setShowOnlyOnFocus
(
true
);
// Creation des autres champs d'affichage
Label labelManufacturer =
new
Label
(
parent, SWT.NONE);
labelManufacturer.setText
(
"Manufacturer:"
);
textManufacturer =
new
Text
(
parent, SWT.READ_ONLY);
textManufacturer.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
Label labelCrew =
new
Label
(
parent, SWT.NONE);
labelCrew.setText
(
"Crew:"
);
textCrew =
new
Text
(
parent, SWT.READ_ONLY);
textCrew.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
Label labelEngines =
new
Label
(
parent, SWT.NONE);
labelEngines.setText
(
"Engines:"
);
textEngines =
new
Text
(
parent, SWT.READ_ONLY);
textEngines.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
}
/**
* Construit l'assistant de contenu sur le champ texte
*/
private
void
constructFieldAssist
(
) {
// Definition des caracteres d'activation automatique
char
[] autoActivationCharacters =
new
char
[] {
'A'
, 'B'
}
;
try
{
// Definition du raccourci clavier d'activation
KeyStroke keyStroke =
KeyStroke.getInstance
(
"Ctrl+Space"
);
// Creation de l'assistant de contenu
ContentProposalAdapter adapter =
new
ContentProposalAdapter
(
textAssist,
new
TextContentAdapter
(
), new
PlaneContentProposalProvider
(
true
),
keyStroke, autoActivationCharacters);
// Avec cette commande, le champ texte sera integralement remplace par le contenu
choisi
adapter.setProposalAcceptanceStyle
(
ContentProposalAdapter.PROPOSAL_REPLACE);
adapter.addContentProposalListener
(
new
IContentProposalListener
(
) {
@Override
public
void
proposalAccepted
(
IContentProposal proposal) {
// On peut dans cette methode effectuer des traitements supplementaires
// apres que la validation du contenu.
if
(
proposal instanceof
PlaneProposal) {
PlaneProposal planeProposal =
(
PlaneProposal) proposal;
textManufacturer.setText
(
planeProposal.getManufacturer
(
));
textCrew.setText
(
Integer.toString
(
planeProposal.getCrew
(
)));
textEngines.setText
(
Integer.toString
(
planeProposal.getEngines
(
)));
}
}
}
);
}
catch
(
ParseException e1) {
StatusManager.getManager
(
).handle
(
new
Status
(
Status.ERROR, Activator.PLUGIN_ID,
"Unable to create key stroke for content assist"
, e1), StatusManager.SHOW);
}
}
@Override
public
void
setFocus
(
) {
textAssist.setFocus
(
);
}
/**
* Cette classe fournit les elements a afficher a l'assistant de contenu
*
@author
A. BERNARD
*
*/
public
class
PlaneContentProposalProvider implements
IContentProposalProvider {
private
boolean
filterProposals =
false
;
/**
* Constructeur
*
@param
filter
<
code
>
true
<
/code
>
si les elements doivent etre filtres
*
*/
public
PlaneContentProposalProvider
(
boolean
filter) {
this
.filterProposals =
filter;
}
@Override
public
IContentProposal[] getProposals
(
String contents, int
position) {
if
(
filterProposals) {
// Effectuer un filtrage s'il est active: les elements a afficher
// sont ceux qui correspondent au texte deja entre
ArrayList<
IContentProposal>
list =
new
ArrayList<
IContentProposal>(
);
for
(
IContentProposal proposal : elements) {
if
(
proposal.getContent
(
).startsWith
(
contents)) {
list.add
(
proposal);
}
}
return
list.toArray
(
new
IContentProposal[list.size
(
)]);
}
return
elements.toArray
(
new
IContentProposal[elements.size
(
)]);
}
}
}
Afin de construire correctement la liste des avions à proposer à l'utilisateur, nous effectuons un filtrage sur les éléments à afficher. Ce filtrage est fait au travers de la classe « PlaneContentProposalProvider ». Grâce à la définition de la méthode « getDescription » de la classe « PlaneProposal », le cadre jaune affiché à côté de la liste affiche bien la description de nos éléments :
De plus, afin de pouvoir remplir les champs texte lorsque l'utilisateur valide une proposition, nous ajoutons un écouteur grâce à la méthode « addContentProposalListener(IContentProposalListener) » :
adapter.addContentProposalListener
(
new
IContentProposalListener
(
) {
@Override
public
void
proposalAccepted
(
IContentProposal proposal) {
// Ici le traitement particulier.
}
}
);
Ces deux exemples nous ont permis de voir rapidement
comment mettre en place un assistant de contenu sur des
champs texte ou des listes déroulantes. Notons qu'il
est possible de construire ses assistants sur n'importe
quel type de composant SWT, en redéfinissant
soi-même une implémentation de l'interface
« IControlContentAdapter » et en
l'utilisant en lieu et place du
« TextContentAdapter ».
La mise en place de l'assistant de contenu sur des champs texte reste très
simple. Celle pour des éditeurs est plus complexe, mais offre bien
davantage de possibilités.
III. Autocomplétion dans les éditeurs▲
III-A. Éditeurs de texte classiques▲
De la même manière que l'autocomplétion pour les fichiers de code Java, il est possible de mettre en place un assistant de contenu dans un éditeur personnalisé. Nous expliquons ici comment mettre en place un tel assistant, toujours en se basant sur le même exemple que dans le paragraphe précédent : une liste d'avions. Dans cet exemple, nous nous baserons sur un éditeur de texte très classique qui hérite directement de la classe « AbstractDecoratedTextEditor ».
III-A-1. Mise en place de l'éditeur▲
Pour commencer à mettre en place l'autocomplétion, nous devons créer notre éditeur « squelette ». Pour cela, vérifier que vous avez dans les dépendances de votre plugin, les plugins suivants :
- org.eclipse.jface.text ;
- org.eclipse.ui.editors ;
- org.eclipse.core.resources ;
- org.eclipse.ui.ide.
Il faut ensuite créer l'éditeur. Il dérive directement de la classe « AbstractDecoratedTextEditor ». Dans cet éditeur, nous devons créer un objet de type « TextSourceViewerConfiguration » :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
package
com.abernard.contentassist.editor;
import
org.eclipse.ui.texteditor.AbstractDecoratedTextEditor;
/**
* Cet editeur affiche du texte brut. Il propose une autocompletion sur une
* liste de mots-cles.
*
@author
A. BERNARD
*
*/
public
class
AircraftsEditor extends
AbstractDecoratedTextEditor {
/**
* Constructeur. Definit la configuration de l'editeur.
*/
public
AircraftsEditor
(
) {
super
(
);
this
.setSourceViewerConfiguration
(
new
AircraftSourceViewerConfiguration
(
getSharedColors
(
),
getPreferenceStore
(
)));
}
}
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.
package
com.abernard.contentassist.editor;
import
org.eclipse.jface.preference.IPreferenceStore;
import
org.eclipse.jface.text.DefaultInformationControl.IInformationPresenter;
import
org.eclipse.jface.text.source.ISharedTextColors;
import
org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
/**
* Cette configuration de l'editeur permet d'activer l'autocompletion sur le
* texte en cours de saisie.
*
@author
A. BERNARD
*
*/
public
class
AircraftSourceViewerConfiguration extends
TextSourceViewerConfiguration {
/**
* Constructeur.
*
@param
sharedColors
*
@param
preferenceStore
*/
public
AircraftSourceViewerConfiguration
(
final
ISharedTextColors sharedColors,
IPreferenceStore preferenceStore) {
super
(
preferenceStore);
}
}
À ce stade, si vous lancez votre application Eclipse, vous devriez pouvoir ouvrir des fichiers dans l'éditeur défini.
Dans le fichier « plugin.xml », vous devez déclarer à quelle extension de fichier est associé votre éditeur. Nous avons choisi dans cet exemple l'extension totalement arbitraire « *.ac ».
III-A-2. Mise en place de l'autocomplétion▲
Nous pouvons maintenant mettre en place l'autocomplétion dans notre éditeur. Cela passe par deux étapes : en premier nous devons définir une classe qui implémente l'interface « IContentAssistProcessor », puis indiquer à notre classe « AircraftSourceViewerConfiguration » d'indiquer cette classe pour la gestion de l'autocomplétion. Créons tout d'abord la classe « AircraftCompletionProcessor » :
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.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
package
com.abernard.contentassist.editor;
import
java.util.ArrayList;
import
java.util.List;
import
org.eclipse.core.runtime.Status;
import
org.eclipse.jface.text.BadLocationException;
import
org.eclipse.jface.text.IDocument;
import
org.eclipse.jface.text.ITextViewer;
import
org.eclipse.jface.text.contentassist.CompletionProposal;
import
org.eclipse.jface.text.contentassist.ContextInformation;
import
org.eclipse.jface.text.contentassist.ContextInformationValidator;
import
org.eclipse.jface.text.contentassist.ICompletionProposal;
import
org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import
org.eclipse.jface.text.contentassist.IContextInformation;
import
org.eclipse.jface.text.contentassist.IContextInformationValidator;
import
org.eclipse.ui.statushandlers.StatusManager;
import
com.abernard.contentassist.Activator;
import
com.abernard.contentassist.PlaneProposal;
/**
* Cree la liste des elements a afficher dans l'assistant de contenu de l'editeur.
*
@author
A. BERNARD
*
*/
public
class
AircraftCompletionProcessor implements
IContentAssistProcessor {
private
List<
PlaneProposal>
elements;
/**
* Constructeur. Initialise la liste des elements disponibles
*/
public
AircraftCompletionProcessor
(
) {
elements =
new
ArrayList<
PlaneProposal>(
);
elements.add
(
new
PlaneProposal
(
"A320"
, "Airbus"
, 2
, 2
));
elements.add
(
new
PlaneProposal
(
"Rafale"
, "Dassault"
, 1
, 1
));
elements.add
(
new
PlaneProposal
(
"Falcon 7x"
, "Dassault"
, 3
, 3
));
elements.add
(
new
PlaneProposal
(
"B777"
, "Boeing"
, 4
, 2
));
elements.add
(
new
PlaneProposal
(
"A380"
, "Airbus"
, 2
, 4
));
}
@Override
public
char
[] getCompletionProposalAutoActivationCharacters
(
) {
return
new
char
[] {
'A'
, 'B'
}
;
}
@Override
public
ICompletionProposal[] computeCompletionProposals
(
ITextViewer viewer, int
offset) {
IDocument document =
viewer.getDocument
(
);
int
currOffset =
offset-
1
;
String currWord =
""
;
if
(
currOffset >=
0
) {
try
{
char
currChar;
/*
* Retrouver le debut du mot sur lequel on a declenche la completion
* On se deplace d'offset en offset jusqu'a rencontrer un caractere
* de type 'whitespace' (espace, ou retour a la ligne, ...).
* Chaque caractere est ajoute au debut du mot en cours de lecture
*/
while
(
currOffset >=
0
&&
!
Character.isWhitespace
(
currChar =
document
.getChar
(
currOffset))) {
currWord =
currChar +
currWord;
currOffset--
;
}
// Une fois le mot reconstruit, retrouver les elements a proposer
List<
PlaneProposal>
availableSuggests =
getAvailableElements
(
currWord);
// A partir des elements disponibles construire la liste des propositions
ICompletionProposal[] proposals =
null
;
if
(
availableSuggests.size
(
) >
0
) {
proposals =
buildProposals
(
availableSuggests, currWord, offset -
currWord.length
(
));
}
return
proposals;
}
catch
(
BadLocationException e) {
StatusManager.getManager
(
).handle
(
new
Status
(
Status.ERROR, Activator.PLUGIN_ID,
e.getMessage
(
), e), StatusManager.LOG);
return
null
;
}
}
else
{
// Pas de texte dans le document, donc pas de completion disponible !
return
null
;
}
}
@Override
public
IContextInformation[] computeContextInformation
(
ITextViewer viewer, int
offset) {
ContextInformation[] contextInfos =
new
ContextInformation[elements.size
(
)];
for
(
int
i =
0
; i <
contextInfos.length; i++
) {
contextInfos[i] =
new
ContextInformation
(
"Avion : "
+
elements.get
(
i).getContent
(
),
elements.get
(
i).getContent
(
));
}
return
contextInfos;
}
@Override
public
char
[] getContextInformationAutoActivationCharacters
(
) {
return
null
;
}
@Override
public
String getErrorMessage
(
) {
return
"No completions found"
;
}
@Override
public
IContextInformationValidator getContextInformationValidator
(
) {
return
new
ContextInformationValidator
(
this
);
}
/**
* Construit la liste des elements qui, peuvent etre inseres a cet endroit
* dans l'editeur.
*
@param
word
le mot en cours d'ecriture dans l'editeur
*
@return
la liste des elements disponibles
*/
private
List<
PlaneProposal>
getAvailableElements
(
String word) {
List<
PlaneProposal>
availableItems =
new
ArrayList<
PlaneProposal>(
);
for
(
PlaneProposal proposal : elements) {
if
(
proposal.getContent
(
).startsWith
(
word)) {
availableItems.add
(
proposal);
}
}
return
availableItems;
}
/**
* Construit la liste des elements d'autocompletion a partir des elements
* disponibles.
*
@param
availableElements
la liste des elements disponibles
*
@param
replacedWord
le mot a remplacer dans l'editeur
*
@param
offset
la position du curseur dans le document
*
@return
la liste des suggestions d'autocompletion
*/
private
ICompletionProposal[] buildProposals
(
List<
PlaneProposal>
availableElements, String
replacedWord, int
offset) {
ICompletionProposal[] proposals =
new
ICompletionProposal[availableElements.size
(
)];
int
index =
0
;
// Create proposals from model elements.
for
(
PlaneProposal proposal : availableElements) {
IContextInformation contextInfo =
new
ContextInformation
(
null
, proposal.getContent
(
));
proposals[index] =
new
CompletionProposal
(
proposal.getContent
(
), offset,
replacedWord.length
(
), proposal.getContent
(
).length
(
),
null
, proposal.getContent
(
),
contextInfo, proposal.getDescription
(
));
index++
;
}
return
proposals;
}
}
Notez que, par facilité, nous utilisons la
classe « PlaneProposal » que
nous avons créée précédemment
mais que cela n'est pas nécessaire. N'importe
quelle classe pourra convenir. Le constructeur ne
sert ici qu'à créer la liste des
éléments disponibles. Dans un cas
réel d'utilisation, les éléments
disponibles seront probablement chargés
dynamiquement à l'appel de
l'autocomplétion. C'est en effet dans la
méthode « public
ICompletionProposal[]
computeCompletionProposals(ITextViewer viewer, int
offset) » que les éléments
à afficher dans la popup d'autocomplétion
doivent être créés. Cette
méthode est appelée à chaque fois
que l'utilisateur appuie sur la combinaison de
touche « Ctrl+Espace » pour
afficher les éléments disponibles.
En premier lieu, notre implémentation tente de retrouver le début de mot
que nous sommes en train d'écrire. Pour cela, on lit les
caractères un à un jusqu'à tomber sur un caractère
d'espacement (espace, tabulation, retour à la ligne, etc.). Si on est
au début du fichier, il n'y pas d'éléments disponibles (cas
où offset = 0). Une fois reconstitué le mot en cours de saisie
(« currWord »), on filtre la liste des
éléments disponibles afin de trouver ceux compatibles avec le
mot, au sein de la méthode « private List<PlaneProposal>
getAvailableElements(String word) ». Les éléments
disponibles sont simplement ceux qui commencent par
« currWord ».
De là, nous pouvons construire les éléments à afficher dans le
popup d'autocomplétion, au sein de la méthode « private
ICompletionProposal[] buildProposals(List<PlaneProposal>
availableElements, String replacedWord, int offset) ». Pour
chaque élément disponible, nous devons instancier un
élément de type « CompletionProposal ».
Remarquez que nous créons en plus un élément de type
« ContextInformation ». Cet élément sera
affiché dans un tooltip lors de la validation de la proposition
choisie : . Notre méthode
principale retourne la liste de propositions ainsi construite, permettant
à la plateforme d'afficher le popup d'autocomplétion.
Ceci fait, nous devons activer le mécanisme d'autocomplétion dans notre
classe « AircraftSourceViewerConfiguration » :
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.
package
com.abernard.contentassist.editor;
import
org.eclipse.jface.preference.IPreferenceStore;
import
org.eclipse.jface.text.DefaultInformationControl;
import
org.eclipse.jface.text.DefaultInformationControl.IInformationPresenter;
import
org.eclipse.jface.text.IDocument;
import
org.eclipse.jface.text.IInformationControl;
import
org.eclipse.jface.text.IInformationControlCreator;
import
org.eclipse.jface.text.TextPresentation;
import
org.eclipse.jface.text.contentassist.ContentAssistant;
import
org.eclipse.jface.text.contentassist.IContentAssistant;
import
org.eclipse.jface.text.source.ISharedTextColors;
import
org.eclipse.jface.text.source.ISourceViewer;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.custom.StyleRange;
import
org.eclipse.swt.widgets.Display;
import
org.eclipse.swt.widgets.Shell;
import
org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
/**
* Cette configuration de l'editeur permet d'activer l'autocompletion sur le
* texte en cours de saisie.
*
@author
A. BERNARD
*
*/
public
class
AircraftSourceViewerConfiguration extends
TextSourceViewerConfiguration {
private
IInformationPresenter presenter;
/**
* Constructeur.
*
@param
sharedColors
*
@param
preferenceStore
*/
public
AircraftSourceViewerConfiguration
(
final
ISharedTextColors sharedColors,
IPreferenceStore preferenceStore) {
super
(
preferenceStore);
presenter =
new
DefaultInformationControl.IInformationPresenter
(
) {
@Override
public
String updatePresentation
(
Display display, String hoverInfo,
TextPresentation presentation, int
maxWidth, int
maxHeight) {
// Mise en gras du "titre" (reference et avionneur)
int
firstColon =
(
hoverInfo.indexOf
(
':'
) !=
-
1
? hoverInfo.indexOf
(
':'
) : 0
);
StyleRange title =
new
StyleRange
(
0
, firstColon, null
, null
, SWT.BOLD |
SWT.ITALIC);
presentation.addStyleRange
(
title);
return
hoverInfo;
}
}
;
}
@Override
public
IContentAssistant getContentAssistant
(
final
ISourceViewer sourceViewer) {
ContentAssistant assistant =
new
ContentAssistant
(
);
assistant.setDocumentPartitioning
(
getConfiguredDocumentPartitioning
(
sourceViewer));
AircraftCompletionProcessor processor =
new
AircraftCompletionProcessor
(
);
assistant.setContentAssistProcessor
(
processor, IDocument.DEFAULT_CONTENT_TYPE);
assistant.setInformationControlCreator
(
getInformationControlCreator
(
sourceViewer));
// Insere automatiquement la seule possibilite si elle est unique
assistant.enableAutoInsert
(
true
);
// Autorise l'"autoactivation", i.e. le declenchement sur des caracteres particuliers
assistant.enableAutoActivation
(
true
);
// Affiche la ligne de statut en bas de la popup de l'assistant
assistant.setStatusLineVisible
(
true
);
assistant.setStatusMessage
(
"Available planes to insert"
);
return
assistant;
}
@Override
public
IInformationControlCreator getInformationControlCreator
(
ISourceViewer sourceViewer) {
return
new
IInformationControlCreator
(
) {
@Override
public
IInformationControl createInformationControl
(
Shell parent) {
return
new
DefaultInformationControl
(
parent, presenter);
}
}
;
}
}
La déclaration de l'assistant de contenu
est faite dans la méthode « public
IContentAssistant getContentAssistant(final
ISourceViewer sourceViewer) ». Il faut
créer un élément de type
« ContentAssistant », qui va
déclencher les différents assistants
disponibles suivant le type de contenu où l'on
se trouve dans le document. Ainsi, il est
nécessaire de configurer le partitionnement du
document grâce à l'instruction
« assistant.setDocumentPartitioning(Partioning) »
et d'affecter ensuite nos différents
éléments
« IContentAssistProcessor »
suivant le type de contenu grâce à
l'instruction
« assistant.setContentAssistProcessor(ContentAssistProcessor,
ContentType) ». Dans notre cas, nous
n'avons pas défini de partitionnement, notre
assistant de contenu est donc configuré pour
le type de contenu par défaut.
On peut ensuite configurer l'assistant grâce aux quelques méthodes
utilisées dans notre exemple ici :
- autoriser l'activation automatique sur certains caractères : « assistant.enableAutoActivation(true) » ;
- insérer automatiquement la seule option disponible le cas échéant : « assistant.enableAutoInsert(true) » ;
- afficher la barre de statut avec le texte idoine : « assistant.setStatusLineVisible(true) » accompagné de la méthode « assistant.setStatusMessage("texte") »;
- permettre un assistant de contenu cyclique : « assistant.setRepeatedInvocationMode(true) ».
Enfin, nous devons aussi configurer la manière dont Eclipse va afficher les détails concernant les différentes propositions affichées (le cadre jaune affiché à côté de la liste). Pour cela, nous définissons notre variable « presenter » de type « IInformationPresenter ». Puis nous indiquons à la plateforme d'utiliser ce mode de présentation dans la méthode « getInformationControlCreator ».
Ce mode de rendu sera utilisé dans tous les tooltips affichés par votre éditeur, notamment par exemple pour les descriptions des erreurs de syntaxe dans les markers.
Nous pouvons lancer notre application et constater le résultat.
III-A-3. Mis en place des templates▲
Nous pouvons compléter notre assistant de contenu avec des templates. Les templates sont des éléments qui, activés par l'utilisateur, insèrent automatiquement dans l'éditeur un ensemble de contenu. La meilleure manière de définir les templates est de le faire via le fichier « plugin.xml ». Nous allons dans cet exemple créer un template qui insère tous les avions disponibles. La première étape est de définir une nouvelle extension pour le point d'extension « org.eclipse.ui.editors.templates ». Au sein de ce point d'extension, il faut définir un nouvel élément de type « contextType ». Dans notre cas, nous utilisons pour ce contexte celui par défaut de la plateforme.
Puis nous définissons le template proprement dit. Le template est associé au context type défini précédemment afin que la plateforme puisse déterminer dans quel cas proposer ce template. Dans ce template, le sous-élément « pattern » définit le contenu du template à proprement parler.
Pour afficher les templates, notre classe « AircraftCompletionProcessor » doit être une sous-classe de « TemplateCompletionProcessor ». Notre classe se trouve donc modifiée comme suit :
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.
package
com.abernard.contentassist.editor;
import
java.io.IOException;
import
org.eclipse.jface.text.templates.Template;
import
org.eclipse.jface.text.templates.TemplateCompletionProcessor;
import
org.eclipse.jface.text.templates.TemplateContextType;
import
org.eclipse.swt.graphics.Image;
import
org.eclipse.ui.editors.text.templates.ContributionContextTypeRegistry;
import
org.eclipse.ui.editors.text.templates.ContributionTemplateStore;
import
com.abernard.contentassist.Activator;
import
com.abernard.contentassist.Constants;
// ... autres imports
/**
* Cree la liste des elements a afficher dans l'assistant de contenu de l'editeur.
*
@author
A. BERNARD
*
*/
public
class
AircraftCompletionProcessor extends
TemplateCompletionProcessor {
private
List<
PlaneProposal>
elements;
private
ContributionTemplateStore fTemplateStore;
private
ContributionContextTypeRegistry fRegistry;
/**
* Constructeur. Initialise la liste des elements disponibles
*/
public
AircraftCompletionProcessor
(
) {
// ... construction de la liste des elements comme precedemment
fRegistry=
new
ContributionContextTypeRegistry
(
);
fRegistry.addContextType
(
Constants.TEMPLATE_CONTEXT_TYPE);
fTemplateStore=
new
ContributionTemplateStore
(
fRegistry,
Activator.getDefault
(
).getPreferenceStore
(
),
Constants.PREF_TEMPLATE);
try
{
fTemplateStore.load
(
);
}
catch
(
IOException x) {
Activator.getDefault
(
).getLog
(
).log
(
new
Status
(
Status.ERROR,
Activator.PLUGIN_ID, x.getMessage
(
), x));
}
}
@Override
public
ICompletionProposal[] computeCompletionProposals
(
ITextViewer viewer, int
offset) {
ICompletionProposal[] proposals =
super
.computeCompletionProposals
(
viewer, offset);
IDocument document =
viewer.getDocument
(
);
int
currOffset =
offset-
1
;
String currWord =
""
;
if
(
currOffset >=
0
) {
try
{
char
currChar;
/*
* Retrouver le debut du mot sur lequel on a declenche la completion
* On se deplace d'offset en offset jusqu'a rencontrer un caractere
* de type 'whitespace' (espace, ou retour a la ligne, ...).
* Chaque caractere est ajoute au debut du mot en cours de lecture
*/
while
(
currOffset >=
0
&&
!
Character.isWhitespace
(
currChar =
document
.getChar
(
currOffset))) {
currWord =
currChar +
currWord;
currOffset--
;
}
// Une fois le mot reconstruit, retrouver les elements a proposer
List<
PlaneProposal>
availableSuggests =
getAvailableElements
(
currWord);
// A partir des elements disponibles construire la liste des propositions
List<
ICompletionProposal>
proposalsList =
new
ArrayList<
ICompletionProposal>(
);
proposalsList.addAll
(
Arrays.asList
(
proposals));
if
(
availableSuggests.size
(
) >
0
) {
proposalsList.addAll
(
buildProposals
(
availableSuggests,
currWord, offset -
currWord.length
(
)));
}
proposals =
proposalsList.toArray
(
new
ICompletionProposal[proposalsList.size
(
)]);
return
proposals;
}
catch
(
BadLocationException e) {
StatusManager.getManager
(
).handle
(
new
Status
(
Status.ERROR, Activator.PLUGIN_ID,
e.getMessage
(
), e), StatusManager.LOG);
return
null
;
}
}
else
{
// Pas de texte dans le document, donc pas de completion disponible !
return
proposals;
}
}
@Override
protected
TemplateContextType getContextType
(
ITextViewer viewer,
IRegion region) {
return
fRegistry.getContextType
(
Constants.TEMPLATE_CONTEXT_TYPE);
}
@Override
protected
Image getImage
(
Template template) {
return
Activator.getDefault
(
).getImageRegistry
(
).get
(
Constants.IMG_TEMPLATE);
}
@Override
protected
Template[] getTemplates
(
String contextTypeId) {
return
fTemplateStore.getTemplates
(
contextTypeId);
}
// ... autres methodes implementees precedemment
}
La première étape consiste à
indiquer quels templates Eclipse charger dans le
contexte de notre éditeur. C'est pour cela que
nous initialisons les attributs fTemplatesStore et
fRegistry dans le constructeur de la classe. Le
lien est fait entre le type de contexte que nous
avons défini pour notre template dans le
fichier « plugin.xml » et les
préférences d'Eclipse. Il ne reste
ensuite qu'à implémenter les trois
méthodes
« getContextType »,
« getImage » (optionnelle)
et »getTemplates ».
À chaque appel de l'assistant de contenu, les templates idoines sont
recherchés au sein des templates existant, en faisant tout simplement
un appel à « super.computeCompletionProposals(viewer,
offset) ». Nous les ajoutons ensuite à la liste des
éléments affichés à l'utilisateur. En lançant
notre application, nous pouvons observer la présence de notre template
au sein de la liste de propositions, ainsi que l'insertion de ce template
au sein de notre document.
III-B. Utilisation de Xtext▲
Le système que nous avons décrit dans ce paragraphe utilise les API bas niveau d'Eclipse. Si l'on ajoute à la gestion de l'assistant de contenu la gestion de la coloration syntaxique, de la vérification de syntaxe et de toutes les préférences associées, la mise en place de tous ces éléments peut devenir lourde sur des éditeurs complexes. Certains frameworks, tel Xtext permettent de mettre en place ces éléments de manière plus simple. Vous trouverez dans cet articlePersonnalisation de l'IDE généré par Xtext comment mettre en place l'autocomplétion dans Xtext.
IV. Conclusion▲
Nous avons vu dans cet article deux méthodes pour mettre en place un assistant de contenu dans une application Eclipse RCP : sur un composant SWT ou au sein d'un éditeur de texte. Bien que différentes par leur complexité et les possibilités qu'elles offrent, ces deux méthodes ont le même but : proposer à l'utilisateur de votre application un moyen simple et intuitif de connaître les différentes possibilités qui s'offrent à lui. Ces systèmes sont très appréciés et apporteront rapidement une réelle plus-value à vos interfaces.
V. Liens utiles▲
À propos de l'autocomplétion :
Sur les éditeurs en général :
VI. Remerciements▲
Je tiens à remercier l'équipe de Developpez.com, particulièrement Marc et Mickaël, ainsi que Claude LELOUP pour sa relecture orthographique attentive.