Tutoriel sur l'autocomplétion dans une application Eclipse RCP

Le mécanisme d'autocomplétion occupe une place centrale dans l'IDE Eclipse : il procure une aide de tous les instants au développeur en plus de permettre un gain de temps considérable lors du développement. Il est possible de mettre en place un tel système personnalisé et adapté aux besoins spécifiques dans une application Eclipse RCP. Cela peut se faire de deux manières différentes que nous allons étudier dans cet article : soit dans un éditeur de texte, soit sur des composants SWT tels que des champs texte par exemple. Les sources de cet exemple sont disponibles à l'adresse suivante : FTPftp-sources ou HTTPhttp-sources.

Pour toute remarque ou question sur ce tutoriel, profitez de cette discussion : 1 commentaire Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

Autocomplétion dans un éditeur Java
Autocomplétion dans un éditeur Java
Autocomplétion sur un champ texte
Autocomplétion sur un champ texte

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 » :

ContentAssistView.java
CacherSélectionnez
1.
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 :

Assistant basique
Assistant basique

Nous pouvons aussi constater que lorsqu'on entre le caractère '_' les éléments apparaissent, filtrés :

Assistant basique avec filtre
Assistant basique avec filtre

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 :

PlaneProposal.java
CacherSélectionnez
1.
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.

ContentAssistAdvanced.java
CacherSélectionnez
1.
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 :

Description du contenu
Description du contenu

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) » :

 
Sélectionnez
adapter.addContentProposalListener(new IContentProposalListener() {
		@Override
		public void proposalAccepted(IContentProposal proposal) {
		    // Ici le traitement particulier.
		}
	    });
Remplissage des champs texte
Remplissage des champs texte

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 » :

AircraftsEditor.java
CacherSélectionnez
1.
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()));
    }

}
AircraftSourceViewerConfiguration.java
CacherSélectionnez
1.
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 » :

AircraftCompletionProcessor.java
CacherSélectionnez
1.
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 : Image non disponible. 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 » :

AircraftSourceViewerConfiguration.java
CacherSélectionnez
1.
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.

Notre assistant de contenu
Notre assistant de contenu

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.

Définition du contextType
Définition du contextType

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.

Définition du template
Définition du template

Pour afficher les templates, notre classe « AircraftCompletionProcessor » doit être une sous-classe de « TemplateCompletionProcessor ». Notre classe se trouve donc modifiée comme suit :

AircraftCompletionProcessor.java
CacherSélectionnez
1.
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.

Notre template dans l'assistant de contenu
Notre template dans l'assistant de contenu

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

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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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