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 + " : \nEquipage : " + crew
+ "\nNombre 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.












