A.10 exercices de la section 6.5, page 544

  1. Construction d’un compteur. Le modèle d’un compteur est simple: toute variable pouvant contenir un entier fait l’affaire. Notre classe de compteur, Compteur a donc besoin d’au moins une variable d’instance. Ce qui nous donne la définition de la classe que voici:
    Object subclass: #Compteur  
       instanceVariableNames: ’compteur’  
       classVariableNames: ’’  
       poolDictionaries: ’’  
       category: ’Cours-Smalltalk’
    Avant toute utilisation le compteur doit être initalisé:
    Compteur>>initalize  
         compteur := 0
    Et, pour ne pas charger l’utilisateur de cette initialisation, définissons la méthode de classe new qui effectue l’initialisation de la variable d’instance:
    Compteur class>>new  
        ^ super new  initialize
    Comme pour la majorité des autres classes que nous avons rencontrées, définissons également la méthode spécifiant comment une instance de la classe Compteur doit être imprimée. Une bonne solution semble d’imprimer juste la valeur du compteur. Ce qui nous donne la méthode printOn: que voici:
    Compteur>>printOn: aStream  
       aStream nextPutAll: compteur printString
    Après ces quelques méthodes préparatoires, définissons les deux méthodes agissant sur notre compteur, la méthode pour le décrémenter:
    Compteur>>decremente  
        self ajoute: -1
    et celle pour l’incrémenter:
    Compteur>>incremente  
        self ajoute: 1
    Les deux méthodes précédentes utilisent la méthode auxiliaire ajoute: que voici:
    Calculatrice>>ajoute: unNombre  
       compteur := compteur + unNombre.
    Vérifions que ces quelques méthodes sont suffisantes pour définir complètement le modèle d’un compteur. Pour cela, ouvrons un inspecteur sur une nouvelle instance de la classe Compteur43 Comme nous pouvons le voire dans la figure A.49, où nous avons exécuté deux doIt sur l’expression self incremente, notre compteur fonctionne parfaitement. Il ne nous reste qu’à définir l’interface utilisateur graphique.
    FIG. A.49: Un inspecteur ouvert sur le modèle du compteur

    Comme dans nos calculatrices, l’écran d’affichage peut être défini par un PluggableTextMorph et les deux touches d’incrémentation et de décrémentation peuvent être définis par un PluggableButtonMorph sur le modèle Compteur. Le morph contenant ces trois objets peut être un SystemWindow.

    Écrivons alors une méthode openInWorld qui met tous ces morphs ensemble:

    Compteur>>openInWorld  
       | window boutonIncrement boutonDecrement ecran |  
       window := SystemWindow labelled: ’Compteur’.  
       window model: self.  
       boutonIncrement :=  
           PluggableButtonMorph new model: self;  
                 action: #incremente;  
                 label: ’+’;  
                 borderWidth: 2.  
       boutonDecrement :=  
           PluggableButtonMorph new model: self;  
                 action: #decremente;  
                 label: ’-’;  
                 borderWidth: 2.
       ecran := PluggableTextMorph  
                on: self  
                text: #printString  
                accept: nil.  
       window  
          addMorph: ecran  
          frame: (0 @ 0 extent: 1 @ 0.5).  
       window  
          addMorph: boutonIncrement  
          frame: (0 @ 0.5 extent: 0.5 @ 0.5).  
       window  
          addMorph: boutonDecrement  
          frame: (0.5 @ 0.5 extent: 0.5 @ 0.5).  
       window openInWorld
    Avec cette définition de notre interface, la transmission Compteur new openInWorld affiche bien un compteur comme nous le désirons, mais l’affichage du compteur ne change pas – comme montré dans la figure A.50, où l’inspecteur dit que le compteur est à la valeur 9 (nous avons neuf fois cliqué sur la touche +), mais l’écran du compteur affiche toujours 0. Pourquoi?

    FIG. A.50: Une contradiction entre modèle et vue

    Ici nous avons le problème que nous avons une représentation graphique de l’écran sans lien dynamique avec son modèle. Nous avons biens les liens statiques: le compteur a été déclaré comme modèle du PluggableTextMorph représentant l’écran. Comme le modèle des touches est un compteur aussi. Si ce n’était pas le cas, les cliques sur la touche + n’auraient pas fait avancer le compteur! Mais pour les touches, des PluggableButtonMorph, le lien dynamique part des morphs: à chaque clique le morph envoie un message (incremente ou decremente) à son modèle. L’écran n’est pas un objet sur lequel nous, utilisateurs, agissons. Ce ne peut être que le modèle qui agit sur l’écran. Et c’est le modèle qui doit envoyer un message à l’écran chaque fois que l’écran doit être mis à jour. Les liens dynamique ne sont pas bi-directionnels – c’était la raisons pour laquelle l’écran historique de nos premiers calculatrices avait la calculatrice comme objet dépendant et la calculatrice avait l’écran comme dépendant, seulement ainsi nous avons obtenu des liens de dépendance bi-directionnels.

    Modifions alors notre méthode ajoute: pour qu’elle informe les dépendants du fait qu’elle vient de changer la valeur du compteur:

    Compteur>>ajoute: unNombre  
        compteur := compteur + unNombre.  
        self changed: #printString
    Maintenant, à chaque modification du compteur l’écran en est informé et peut se mettre à jour avec cette nouvelle valeur.
  2. Cet exercice est beaucoup moins anodin qu’il a l’air. Avant de commencer à programmer nous devons répondre aux questions suivantes:
    1. Quelle structure utiliser pour ce tableau à deux dimensions?
    2. Où mettre les textes de chacune des cases?
    3. Qui est modèle de quoi?

    Arbitrairement, décidons que les textes affichés dans chacun des rectangles sont des instances d’un classe modèle donnant accès à leurs contenus textuels. Appelons cette classe ValueModel. Chaque instance de cette classe se distinguera des autres par la valeur de sa variable d’instance contents. Ce début de réflection nous permets de définir cette classe ainsi que les méthodes d’accès à sa variable d’instance:

    Model subclass: #ValueModel  
       instanceVariableNames: ’contents’  
       classVariableNames: ’’  
       poolDictionaries: ’’  
       category: ’Cours-Smalltalk’
    ValueModel>>contents  
       ^contents
    contents: aValue  
       contents:= aValue.
    Pour implémenter le tableau à deux dimension nous pouvons utiliser la classe Matrix du système SQUEAK.44

    Une matrice45 peut être créée avec le message rows:columns:, où l’argument rows: donne le nombre de lignes et l’argument columns: donne le nombre des colonnes.

    Pour l’accès aux éléments d’une matrice SQUEAK offre la méthode at:at: où le premier argument est l’indice de la ligne et le deuxième l’indice de la colonne d’un élément particulier. La modification d’un élément se fait avec la méthode at:at:put:. Bien entendu, comme toute collection – Matrix est sous-classe de la classe Collection – les matrices offrent les méthode do: et indicesDo: pour parcourir l’ensemble de ses éléments, la méthodes collect: pour construire une nouvelle matrice avec chacun des éléments obtenu par l’application du bloc argument aux éléments de la matrice receveur, toute une variété de méthodes testant la présence d’un objet particulier dans la matrice: includes:, includes:AnyOf: et includes:AllOf:. Les méthodes rowCount et columnCount livrent respectivement le nombre de lignes et le nombre de colonnes de la matrice.



    FIG. A.51: La chaîne des valeurs des instances de TableauDeTexte

    Nous allons donc construire la classe TableauDeTexte qui doit pouvoir contenir une instance de la classe Matrix dont chaque élément doit être une instance de la classe ValueModel qui contient une chaîne de caractères qui doit être affiché dans la case correspondant à cet élément. Cet enchaînement des valeurs est esquissé dans la figure A.51.

    Continuons alors la programmation de notre tableau de textes par la définition de la classe TableauDeTexte:

    Object subclass: #TableauDeTexte  
       instanceVariableNames: ’textMatrix’  
       classVariableNames: ’’  
       poolDictionaries: ’’  
       category: ’Cours-Smalltalk’
    et sa méthode d’initialisation:
    TableauDeTexte class>>width: x height: y  
       ^ super new setMatrixWidth: y height: x
    Cette méthode transmet le message setMatrixWidth:height: à l’instance nouvellement créée de la classe TableauDeText. Cette méthode doit initialiser la variable d’instance textMatrix à une matrice de x lignes et y colonnes.

    Elle doit également initialiser chaque élément de la matrice à une instance de la classe ValueModel, dont la variable d’instance contents prendra comme valeur la chaîne de caractères correspondant à un Point avec les coordonnées les indices de l’élément de la matrice dont il sera la valeur. Voici alors la méthode réalisant ces initialisations:

    TableauDeTexte>>setMatrixWidth: x height: y  
       | value |  
       textMatrix := Matrix rows: x columns: y.  
       1  
          to: x  
          do: [:ro | 1  
                to: y  
                do: [:co |  
                   value :=  
                     ValueModel new  
                        contents:  
                          ro asString , ’@’ , co asString;  
                        yourself.  
                   textMatrix  
                      at: ro  
                      at: co  
                      put: value]]
    La transmission du message yourself à la nouvelle instance de ValueModel (dans la quatorzième ligne) est crucial: les éléments de la matrice ne doivent pas contenir le texte, mais le modèle du texte.
    PIC PIC PIC PIC
    FIG. A.52: La chaîne des valeurs vu à travers des inspecteurs

    Comme toujours, vérifions le bon fonctionnement du programme en inspectant une instance de notre classe. La figure A.52 montre une suite d’inspecteurs qui retrace la même chaîne de valeurs que celle montrée dans la figure A.51. En haut à gauche de la figure se trouve l’inspecteur sur l’instance de la classe TableauDeText créée par la transmission TableauDeTexte width: 5 height: 4. Sa variable d’instance textMatrix contient bien une instance de la classe Matrix. L’inspecteur sur cette instance se trouve en haut à droite de la figure. La variable d’instance contents de la classe Matrix, la variable contenant l’ensemble de ses éléments, contient bien une collection d’instances de la classe ValueModel. L’inspecteur situé en bas à gauche de la figure, un inspecteur sur cette variable contents, montre que la matrice est internement implémenter comme un tableau, comme une instance de la classe Array. Nous y avaons lançé un inspecteur sur le premier élément, qui est, comme montré dans l’inspecteur en bas à droite dans la figure, une instance de la classe ValueModel dont la variable d’instance contents contient la chaîne de carcatère ’1@1’. C’est bien un Point dont la coordonnée x est le numéro de la ligne et la coordonnée y le numéro de la colonne de cet élément dans la matrice.

    Tournons-nous alors vers la programmation de l’interface utilisateur graphique. Pour cela, principalement, nous devons créer une vue pour chacun des éléments de la matrice textMatrix. Chaque vue sera un PluggableTextMorph. Sa taille sera déterminée par un simple calcul sur le nombre de lignes et de colonnes de la matrice: par rapport à la hauteur de la fenêtre principale, sa hauteur sera 1 / nombre de lignes et sa largeur sera 1 / nombre do colonnes.

    Voici alors notre méthode openInWorld pour les instances de la classe TableauDeTexte:

    TableauDeTexte>>openInWorld  
       | window textView testViewRectangle |  
       window := SystemWindow labelled: ’Tableau de Textes’.  
       window model: self.  
       1  
          to: textMatrix rowCount  
          do: [:ro | 1  
                to: textMatrix columnCount  
                do: [:co |  
                   textView :=  
                       self textViewOnRow: ro  
                                   column: co.  
                   testViewRectangle :=  
                       self rectangleForRow: ro  
                                     column: co.  
                   window addMorph: textView  
                             frame: testViewRectangle]].  
       window openInWorld
    Elle crée, avec la méthode textViewOnRow:column: que nous verrons par la suite, une PluggableTextMorph pour chaque élément de la matrice, calcule, avec la méthode rectangleForRow:column:, le rectangle dans lequel il faut insérer ce morph dans le morph principal, une instance de SystemWindow. Voici la définition de ces deux méthodes auxiliaires:
    TableauDeTexte>>textViewOnRow: row column: column  
       | model textView |  
       model := textMatrix at: row at: column.  
       textView := PluggableTextMorph  
                on: model  
                text: #contents  
                accept: #contents:.  
       ^ textView
    TableauDeTexte>>rectangleForRow: row column: column  
       | width height extent left top |  
       height := 1 / textMatrix rowCount.  
       width := 1 / textMatrix columnCount.  
       extent := width @ height.  
       left := width * (column - 1).  
       top := height * (row - 1).  
       ^ left @ top extent: extent
    Si nous activons mainteant la transmission:
    (TableauDeTexte width: 5 height: 4) openInWorld
    nous obtenons la vue de la figure A.53 où chaque sous-fenêtre de texte possède sa propre barre de défilement.
    PIC
    FIG. A.53: Une instance de TableauDeTexte avec des barres de défilement

    Si nous ne voulons pas de ces barres de défilement, comme dans la figure 6.29 (page 545), il suffit d’envoyer aux PluggableTextMorph représentant ces sous-fenêtres le message hideScrollBarIndefinitely. Par exemple, nous pouvons insérer la ligne
    textView hideScrollBarIndefinitely.
    comme avant dernière ligne dans méthode textViewOnRow:column:.

    Quand nous positionnons la souris dans un de ces morphs et nous la cliquons, un curseur de texte (représenté comme une barre verticale de couleur verte) apparaît au début du texte. Nous pouvons alors insérer du texte supplémentaire, modifier le texte qui s’y trouve, voire l’éliminer complètement.

    Dès que vous modifier le texte d’une manière quelconque apparît un cadre rouge autour de la sous-fenêtre de laquelle le texte a été modifié. Vous avez sûrement observé l’apparition d’un tel cadre lors de l’édition de méthodes. Tout PluggableTextMorph indique de cette manière que son texte a changé et que ce changement n’a pas encore été sauvegardé. Ainsi le cadre rouge entourant la sous-fenêtre contenant le texte d’une méthode que vous avez modifiée, disparaît dès que vous sauvegardez (avec un «Alt+s») les modifications.

    En clair: tout PluggableTextMorph vous livre – sans même que vous le demandez – accès à l’editeur de texte de Squaek. C’est fort utile. Mais, si nous l’essayons avec nos textes, comme montré dans la figure A.54,


    PIC
    FIG. A.54: Une instance de TableauDeTexte avec des textes édités

    nous pouvons bien modifier les textes, mais après un «Alt+s» le cadre rouge ne diparait pas. Ainsi (c’est difficile à voir dans la figure) toutes les sous-fenêtres éditées dans la figure A.54 gardent leur cadre rouge. Cela veut dire que les textes ne sont pas modifiable. Seulement la vue du texte peut être modifié. Pour que le texte lui même soit modifiable, il faut que l’instance de la classe ValueModel qui contient ce texte permette sa modification. Sinon le texte ne sera accéssible que pour la lecture et non pas aussi pour l’écriture.

    Pour qu’un PluggableTextMorph ait droit à modifier le texte qu’il affiche il faut que lors de sa création le sélecteur d’une méthode du modèle ait été donné comme argument accept:. Reprenons notre méthode de creation des morphs:

    TableauDeText>>textViewOnRow: row column: column  
       | model textView |  
       model := textMatrix at: row at: column.  
       textView := PluggableTextMorph  
                      on: model  
                      text: #contents  
                      accept: #contents:.  
       textView hideScrollBarIndefinitely.  
       ^ textView
    Nous avons donné le sélecteur contents:. Ceci permet au morph de connaître la méthode du modèle qui peut modifier le text avec la méthode contents:. Le modèle indique au morph qu’il veut bien modifier le texte en retournant true quand on lui transmet ce message.

    Ainsi la méthode de modification de texte, dans notre cas c’est la méthode content:, a un double rôle: celui de modifier son texte et celui de signaler aux morphs ayant des vues sur le texte que l’instance accept ou pas des modification.

    Nous devons donc modifier la définition de la méthode contents: de:

    ValueModel>>contents: aValue  
       contents := aValue.
    vers:
    ValueModel>>contents: aValue  
       contents := aValue.  
       ^ true
    pour que les modifications de l’éditeur soient prises en compte par le modèle.

    Notons qu’un PluggableTextMorph garde les textes qu’il affiche sous forme de texte, donc comme une instance de la classe Text. Ces instances contiennent en plus des chaînes de caractères formant le texte propremnt dit, des informations sur son style, les mises en formes etc. Si le modèle désire récupérer juste la chaîne de caractères correspondant au texte, il doit faire une conversion, comme ci-dessous:46

    ValueModel>>contents: aValue  
       contents := aValue asString.  
       ^ true

Digression:

Tout le long de ce livre nous utilisons des morphs pluggable. Cet adjective est dérivé du mot anglais plug qui désigne en électricité une cheville, une pièce amovible, généralement tronconique, non reliée à un conducteur et destinée à établir un contact par son insertion entre deux plots. Les pluggable morphs jouent un peu ce rôle en SQUEAK: ils existent et ne prennent de sens que quand ils sont mis en contact avec un modèle. Nous pourrions traduire le mot anglais pluggable en enfichable.

Bien entendu, il y a aussi des morphs non enfichable, c’est-à-dire: des morphs qui ne contiennent pas, dès leur création, les parties permettant l’interface entre eux et un modèle. Au PluggableButtonMorph correspond alors le SimpleButtonMorph et le StringButtonMorph. Le PluggabeTextMorph a comme pendant non enfichable le TextMorph. Ce dernier possède quelques capacités assez remarquables, telles qu’il peut communiquer avec d’autres TextMorph, des morphs prédecesseur ou succésseurs, qu’il peut s’adapter à toute forme de son morph propriétaire, qu’il peut générer des représentations de soi-même en format postscript47 et qu’il donne accès à l’éditeur de texte de SQUEAK.

La figure A.55 montre un TextMorph mis dans un EllipseMorph. Son succésseur est le TextMorph à sa gauche. Ce qui veut dire que le texte entré ne tenant pas dans le morph se situant dans l’ellipse est automatiquement transmis vers son succésseur. Les deux textes que nous voyons dans l’image sont un seul et unique texte: le début se trouve dans l’ellipse et les parties du textes qui n’y tiennent pas sont transmises vers le deuxième morph, placé ici à sa gauche. Cet ensemble de morphs48 a été obtenu par la suite de transmissions (dans un Workspace) que nous développons ci-dessous:



FIG. A.55: Un TextMorph dans un EllipseMorph interconnecté avec un autre TextMorph: le texte ne tenant pas dans l’ellipse est automatiquement poussé vers le deuxième morph


  1. Commonçons à créer un TextMorph et un EllipseMorph avec les deux transmissions:
    textA := TextMorph new.  
    ellipse := EllipseMorph new.
  2. Insérons textA dans l’ellipse:
    ellipse addMorph: textA.
  3. Disons au morph textA, avec le message fillingOnOff, qu’il doit épouser les contours de son morph propriétaire – le propriétaire est ici l’ellipse.
    textA fillingOnOff.
  4. Entrons un peu de texte dans le morph textA:49
    textA contentsWrapped: ’Eine Urteilskraft die diale...’
  5. Jouons un peu avec l’editeur pour changer sélectivement le jeu de caractères:50
    (textA editor)  
            selectFrom: (textA text string  
                           findString: ’aesthetischen’)  
                    to: 1071;  
            setEmphasis: #bold;  
            selectFrom: 62 to: 74; setEmphasis: #underlined.
    Dans la figure A.55 nous pouvons bien voir l’effet de ces quatre transmissions: deux mots apparaissent en caractères gras et un autre en sousligné.
  6. Créons un deuxième TextMorph qui sera le succésseur de textA:
    textB := TextMorph new setPredecessor: textA.
  7. Il faut le dire aussi au morph textA:
    textA setSuccessor: textB;  recomposeChain.
  8. Finalement, ouvrons nos morphs, pour qu’ils deviennent visible:
    ellipse openInWorld. textB openInWorld.

Après ces transmissions si vous agrandissez la taille de l’ellipse, vous verrez le deuxième morphe se diminuer pendant que le texte revient, au fur et à mesure de l’agrandissement, vers l’ellipse, et si vous diminuez l’ellipse, le deuxième morph s’agrandit du texte qui ne tient plus dans l’ellipse diminuée. C’est charmant, n’est-ce pas?

Vous pouvez aussi éditer les deux textes – inserer la suite des réflexions de Kant ou enlever quelques-uns – avec les mêmes effets sur le flux entre les deux TextMorph.