Flame Maker – Etape 6 - Gestion de la liste des transformations

Introduction

Le but de cette étape est d'améliorer l'interface graphique en ajoutant la gestion de la liste des transformations Flame composant la fractale. Au terme de cette étape, la fenêtre principale de votre programme devrait ressembler à la copie d'écran ci-dessous.

images/stage6.png

L'interface graphique de cette étape

La principale difficulté de cette étape est de bien comprendre les liens entre les nombreux observateurs qui y sont définis et qui garantissent la cohérence des différentes parties de l'interface graphique.

Vue d'ensemble

Cette étape ajoute les éléments suivants à l'interface utilisateur :

  • une liste des transformations Flame composant la fractale, dont une est toujours sélectionnée,
  • deux boutons, le premier permettant de supprimer la transformation sélectionnée de la fractale, le second permettant d'y ajouter une nouvelle transformation.

Pour être correctement mis en page, ces éléments doivent être placés dans des panneaux (c-à-d des instances de JPanel) placés eux-même dans la hiérarchie des conteneurs.

L'image ci-dessous montre la hiérarchie des conteneurs de cette étape. Le contenu du panneau supérieur, réalisé lors de l'étape précédente, n'est pas montré afin d'alléger la présentation.

Comme précédemment, le gestionnaire de mise en page associé à chaque panneau est indiqué entre parenthèses. De plus, les éventuels arguments de ce gestionnaire sont placés entre crochets après celui-ci. P.ex. GridLayout [1,2] signifie que le gestionnaire est de type GridLayout avec une ligne et deux colonnes.

Finalement, lorsqu'un panneau est placé dans un autre panneau qui utilise le gestionnaire BorderLayout, la zone occupée par le panneau fils est indiquée entre parenthèses après son nom. P.ex. « Panneau inférieur (PAGE_END) » signifie que le panneau inférieur est placé dans la zone PAGE_END de son panneau parent (en l'occurrence le panneau de contenu de la fenêtre), qui utilise un gestionnaire de type BorderLayout.

images/containment_hierarchy.png

Hiérarchie des conteneurs de cette étape avec leur gestionnaire de mise en page

Comme dans l'étape précédente, certains panneaux semblent inutiles à ce stade. En particulier, il y a redondance entre le panneau inférieur et le panneau d'édition des transformations. Encore une fois, cette situation est temporaire et les deux panneaux prendront tout leur sens dès l'interface complétée.

Tous les gestionnaires de mise en page utilisés pour cette étape ont déjà été utilisés pour l'étape précédente, à l'exception de BoxLayout. Ce gestionnaire place les composants du conteneur auquel il est attaché côte-à-côte, soit selon un axe horizontal, soit selon un axe vertical. Le choix de l'axe se fait en passant soit la constante LINE_AXIS (horizontal), soit la constante PAGE_AXIS (vertical) au constructeur du gestionnaire. Les composants ajoutés sont mis en page dans l'ordre de lecture, à savoir de gauche à droite pour LINE_AXIS ou de haut en bas pour PAGE_AXIS.

Référence : How to Use BoxLayout

Liste des transformations

Le premier composant à ajouter pour cette étape est celui gérant la liste des transformations.

Swing fournit le composant JList pour présenter les listes d'éléments. Ce composant offre également la possibilité de gérer la sélection d'un ou plusieurs éléments dans la liste. La configuration et l'utilisation de ce composant n'est pas triviale et est donc détaillée ci-dessous.

Le composant JList est générique et prend en paramètre le type des éléments contenus dans la liste. Etant donné que la liste est celle des bâtisseurs des transformations Flame, il semblerait logique d'instancier le composant avec le type FlameTransformation.Builder. Toutefois, pour des raisons qui deviendront claires plus loin, vous utiliserez String.

Modèle de liste

Le composant JList ne gère pas directement la liste qu'il affiche. Au lieu de cela, il utilise—conformément au patron MVC—un modèle de liste qui lui est passé au moment de la construction. Ce modèle doit implémenter l'interface ListModel.

La première chose à faire pour pouvoir utiliser un composant JList est donc de définir une classe implémentant l'interface ListModel et d'en passer une instance au composant JList lors de sa construction.

Le modèle de liste doit être observable—au sens du patron Observer—afin que le composant puisse être redessiné lorsque le contenu de la liste change. Pour ce faire, l'interface ListModel inclut les méthodes addListDataListener et removeListDataListener, qui permettent respectivement d'ajouter et de supprimer des observateurs. A noter que ces observateurs sont nommés ici des auditeurs (listeners), comme c'est souvent le cas dans Swing. Généralement, le seul observateur attaché à un modèle de liste est le composant JList qui l'affiche.

Pour faciliter l'écriture d'un modèle de liste, Swing met à disposition la classe AbstractListModel, qui implémente l'interface ListModel. Cette classe se charge de gérer la liste des observateurs—en particulier, elle fournit des mises en œuvre des méthodes addListDataListener et removeListDataListener—et fournit des méthodes protégées permettant de les informer d'un changement dans la liste observée. Les sous-classes concrètes de AbstractListModel doivent appeler ces méthodes dès que le contenu de la liste change.

Le moyen le plus simple de créer le modèle de liste nécessaire au composant de ce projet est donc de définir une sous-classe concrète de AbstractListModel, que vous pourrez nommer p.ex. TransformationsListModel. Le mieux est d'imbriquer cette classe dans la classe FlameMakerGUI afin qu'elle ait directement accès au bâtisseur de fractale.

Cette classe doit bien entendu définir les méthodes laissées abstraites dans AbstractListModel, à savoir getSize et getElementAt. La première retourne le nombre d'éléments dans la liste, la seconde retourne l'élément à l'index donné. Le nombre d'éléments est le nombre de transformations Flame que contient le bâtisseur de fractale. Pour l'élément à l'indice donné, vous pouvez retourner p.ex. « Transformation n° i » où i est l'indice en question. Ce texte n'est pas très informatif, mais il est difficile de donner une description textuelle plus utile d'une transformation Flame. (Ceci est la raison pour laquelle le composant JList est instancié avec le type String et pas le type FlameTransformation.Builder)

En plus de ces deux méthodes, votre classe modèle doit offrir des méthodes permettant de modifier la liste des transformations et d'en informer les observateurs (auditeurs). Ces méthodes seront appelées lorsque l'utilisateur cliquera sur l'un des deux boutons prévus à cet effet, comme expliqué plus bas.

La première de ces méthodes, que vous pouvez appeler p.ex. addTransformation, ajoute une nouvelle transformation Flame au bâtisseur. Il est conseillé d'ajouter la transformation identité, à savoir celle dont la composante affine est l'identité, et tous les poids sont à 0 sauf celui de la variation Linear, qui est à 1. Mais attention, cette méthode ne peut pas se contenter d'ajouter cette transformation au bâtisseur, elle doit aussi informer les observateurs du changement, ce qui peut se faire au moyen de la méthode fireIntervalAdded héritée de AbstractListModel.

La seconde de ces méthodes, que vous pouvez appeler p.ex. removeTransformation, prend en argument un index de transformation et la supprime du bâtisseur. Là aussi, les observateurs doivent être informés, ce qui peut se faire au moyen de la méthode fireIntervalRemoved héritée de AbstractListModel.

Configuration du composant

Une fois la classe du modèle de liste écrite, le gros du travail de configuration du composant JList est fait. Vous êtes maintenant en mesure de créer ce composant et de lui passer un modèle de liste en argument. Cela fait, il faut encore le configurer un peu en :

  • demandant à ce que seule la sélection d'un élément unique soit possible, grâce à la méthode setSelectionMode à laquelle vous passerez la constante SINGLE_SELECTION provenant de l'interface ListSelectionModel,
  • demandant à ce que 3 lignes de la liste soient toujours visibles, grâce à la méthode setVisibleRowCount (si vous omettez de le faire, la liste n'apparaîtra pas à l'écran),
  • demandant à ce que l'élément d'indice 0 soit initialement sélectionné, grâce à la méthode setSelectedIndex.

Référence : How to Use Lists

Défilement

Lorsque la liste contient plus d'éléments qu'il n'est possible d'en afficher dans la zone réservée au composant JList, les éléments excédentaires ne sont ni visibles ni atteignables par l'utilisateur. Pour résoudre ce problème, il suffit de placer le composant JList dans un panneau de défilement, c-à-d une instance de JScrollPane. Ce panneau se charge de tout, p.ex. de créer les barres de défilement (scroll bars) lorsque c'est nécessaire, de gérer le défilement lui-même (aussi bien avec les barres qu'avec la molette de la souris), etc.

Comme le montre la figure illustrant la hiérarchie des conteneurs, c'est ce panneau de défilement qui est placé dans la zone CENTER du panneau d'édition des transformations.

Référence : How to Use Scroll Panes

Transformation sélectionnée

Une fois la liste affichée à l'écran, il reste à gérer la sélection de la transformation courante. Même si le composant JList fait le gros du travail, en interceptant les clics de l'utilisateur sur les éléments de la liste et en surlignant le dernier élément cliqué, il reste à diffuser l'information au sein du programme.

En effet, la transformation sélectionnée a un impact sur plusieurs éléments de l'interface. Par exemple, la composante affine de la transformation sélectionnée est affichée en rouge dans le composant des transformations affines. De plus, les poids de ses variations seront affichés par la suite dans les zones prévues à cet effet.

La manière la plus propre de faire en sorte que les éléments qui doivent savoir quand la transformation sélectionnée change est d'utiliser le patron Observer. La question est de savoir ce qui doit être observé exactement.

Une première idée serait d'observer le composant JList directement, puisqu'il offre la possibilité d'observer sa sélection. Toutefois, cela impliquerait que le composant JList soit connu, entre autres, du composant affichant les transformations affines. Cela n'est pas très propre du point de vue de l'encapsulation. En effet, le fait que la sélection de la transformation courante se fasse par l'intermédiaire d'un composant JList est un détail d'implémentation de la version actuelle de l'interface graphique, qui pourrait très bien changer dans le futur. Par exemple, on pourrait imaginer laisser l'utilisateur sélectionner une transformation en cliquant directement sur la paire de flèches représentant la partie affine de cette transformation dans le composant les visualisant.

Dès lors, une meilleure solution consiste à faire de l'index de la transformation actuellement sélectionnée un attribut observable de la classe de l'interface graphique, c-à-d FlameMakerGUI. Cet attribut pourra p.ex. être nommé selectedTransformationIndex. Tous les éléments devant connaître la transformation sélectionnée peuvent ensuite observer cet attribut et réagir à ses changements de valeur.

Pour ajouter un attribut observable à FlameMakerGUI, il suffit de mettre en œuvre le patron Observer, ce qui peut se faire ainsi :

  1. Ajouter à la classe FlameMakerGUI un champ privé contenant l'index de la transformation actuellement sélectionnée, ainsi qu'une paire de méthodes permettant de connaître et de modifier la valeur de ce champ (paire getter et setter).
  2. Définir une interface pour les observateurs. Cette interface est très simple et contient uniquement une méthode sans arguments qui est appelée à chaque changement de sélection. Cette interface peut être imbriquée dans la classe FlameMakerGUI pour éviter de polluer l'espace de noms global.
  3. Ajouter à la classe FlameMakerGUI des méthodes pour ajouter et supprimer de tels observateurs.
  4. Faire en sorte que les observateurs soient prévenus lorsque l'index de la transformation actuellement sélectionnée change.

Une fois cet attribut observable créé, il est possible d'ajouter un observateur qui se charge de changer l'index de la transformation sélectionnée du composant affichant les parties affines des transformations.

Finalement, il faut s'assurer que lorsque l'utilisateur clique sur une transformation dans la liste, celle-ci devient la nouvelle transformation sélectionnée. Cela se fait en ajoutant un observateur sur la sélection du composant JList, au moyen de sa méthode addListSelectionListener. Cet observateur ne doit rien faire d'autre que de changer l'attribut observable défini plus haut pour que sa valeur reflète la sélection faite par l'utilisateur.

Boutons d'ajout et de suppression

Pour terminer cette étape, il reste à ajouter les deux boutons gérant l'ajout et la suppression de transformations Flame à la fractale.

Dans Swing, les boutons sont des instances de JButton. Ces composants sont très simple à utiliser, puisqu'il suffit de leur donner un nom (passé au constructeur) et de les ajouter à un conteneur pour les voir apparaître à l'écran.

Pour être informé lorsque l'utilisateur clique sur un bouton, il suffit de s'enregistrer comme observateur auprès du bouton, ce qui se fait au moyen de la méthode addActionListener. Le paramètre à cette méthode doit implémenter l'interface ActionListener qui ne contient qu'une seule méthode, actionPerformed. Cette méthode est appelée chaque fois que l'utilisateur clique sur le bouton.

Pour créer les observateurs, on utilise généralement une classe imbriquée anonyme. La configuration d'un bouton ressemble donc à ceci :

JButton button = new JButton("nom");
button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        // ...
    }
});

Les observateurs à attacher aux deux boutons ne sont pas particulièrement difficiles à écrire, puisque leur rôle principal est d'appeler les méthodes addTransformation et removeTransformation du modèle de liste, définies plus haut. Toutefois, leur travail est rendu plus compliqué par la gestion de la sélection. En effet, pour simplifier d'autres aspects du programme, on demande à ce qu'une transformation soit toujours sélectionnée dans la liste. Cela a plusieurs conséquences :

  • Lors de la suppression d'une transformation, il faut changer la sélection, puisque la transformation qui était sélectionnée a justement été supprimée. Il est suggéré de sélectionner si possible la prochaine transformation dans la liste, sauf si la transformation supprimée est la dernière, auquel cas il faut sélectionner celle qui la précédait.
  • Lors de l'ajout d'une transformation, il est raisonnable de supposer que l'utilisateur voudra modifier justement cette transformation. Il faut donc la sélectionner dès qu'elle a été ajoutée.

Prenez bien garde à utiliser la méthode setSelectedIndex du composant JList pour changer la sélection, et pas la méthode setSelectedTransformationIndex de FlameMakerGUI. En effet, avec la structure d'observation mise en place plus haut, les changements de sélection du composant JList sont transmis au FlameMakerGUI, mais le contraire n'est pas vrai1 !

Finalement, pour qu'il soit possible de sélectionner une transformation en permanence, il faut bien entendu interdire la suppression de la dernière transformation de la liste. Le moyen le plus simple de faire cela est de désactiver le bouton de suppression lorsque la fractale ne contient plus qu'une transformation, en n'oubliant pas de le réactiver lorsqu'une transformation y est ajoutée. Un bouton peut être (des)activé au moyen de la méthode setEnabled.

A noter que, normalement, l'affichage de la fractale devrait être mis à jour lors de l'ajout ou de la suppression d'une transformation. Toutefois, il faudrait pour cela que le bâtisseur de fractale soit observable, ce qui n'est pas le cas actuellement. Ce problème sera résolu lors de la prochaine étape.

Dans l'intervalle, si vous désirez vérifier que l'ajout et la suppression de transformations fonctionnent bien, vous pouvez redimensionner légèrement la fenêtre de l'interface juste après avoir effectué les modifications désirées. Cela provoquera une mise à jour du composant affichant la fractale, qui permet de constater l'effet des modifications.

Résumé

Pour cette étape, vous devez :

  1. Créer la hiérarchie de conteneurs décrite plus haut en pensant à configurer correctement les gestionnaires de mise en page.
  2. Créer la classe du modèle de liste à fournir au composant JList, en fonction des indications données ci-dessus.
  3. Créer le composant JList en lui passant le modèle défini précédemment, le configurer et le placer dans le panneau de défilement JScrollPane.
  4. Ajouter un attribut observable contenant l'indice de la sélection courante à la classe de l'interface graphique (FlameMakerGUI) et ajouter les observateurs nécessaires pour que la sélection d'une transformation dans la liste provoque bien (indirectement) la mise en évidence de la partie affine de cette transformation dans le composant qui les affiche.
  5. Ajouter les boutons d'ajout et de suppression de transformation à la fractale, en prenant bien garde à ce qu'il y ait toujours une transformation sélectionnée, et à ce que la suppression de la dernière transformation de la fractale soit impossible.

Notes

1 La raison pour laquelle les changements de la sélection ne sont propagés que depuis le composant JList vers FlameMakerGUI et pas également dans l'autre sens est que cela introduirait un cycle dans le graphe d'observation, qui poserait un problème de boucle infinie lors de la propagation des changements. D'autre part, il faut que FlameMakerGUI observe les changements de sélection du composant JList, car c'est le seul moyen pour que le premier soit informé des changments dûs aux clics de l'utilisateur dans le second.

Michel Schinz – 2013-04-29 17:47