Programmation d'IHM - Zone de texte

From OFSET Wiki

Jump to: navigation, search

Une zone de texte permet de visualiser et éventuellement de modifier du texte. Dans Squeak, on dispose des classes :

  • PluggableTextMorph dont l'interface comprend un panneau central pour l'édition d'un texte sur plusieurs lignes et éventuellement, suivant les préférences, deux barres de défilement, une horizontale et l'autre verticale.
  • StringMorph et UpdatingStringMorph permettant d'afficher un label ou de saisir un champs simple sur une seule ligne (i.e. nom, prénom ...)


Contents

[edit] Edition avec un PluggableTextMorph

Dans la classe MUIDSimpleEditor1, la méthode d'instance suivante retourne le PluggableTextMorph :

MUIDSimpleEditor1>>contentsMorph
  ^ PluggableTextMorph
      on: self                  "modèle du morph"
      text: #fileContents       "message qui est envoye au modele pour récupérer le texte"
      accept: #setFileContents: "message qui est envoyé au modèle pour modifier le texte (dans le modèle)"

Une instance de PluggableTextMorph est créée en envoyant le message on:text:accept: à la classe. On remarque que le PluggableTextMorph est créé avec les deux sélecteurs fileContents et setFileContents: pour, respectivement, récupérer le texte depuis le modèle ou demander au modèle de modifier le texte.

Pour contenir le texte lu ou modifié par le morph, notre éditeur comprend une variable d'instance suplémentaire, fileContents :

Object subclass: #MUIDSimpleEditor1
	instanceVariableNames: 'filename fileContents'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'MUIDoc'

Dans notre application exemple, une instance de PluggableTextMorph est donc créée et placée dans la fenêtre principale. C'est, dans cette première version, le seul morph contenu, ses bords sont donc collés aux bords intérieurs de la fenêtre et demeurent collés en cas de redimensionnement.

Pour intégrer un morph dans un autre en indiquant explicitement les dimensions relatives et le comportement des bords du morph contenu (la zone de texte) par rapport au morph contenant (la fenêtre) on envoie le message addMorph:fullFrame: au morph contenant. Le premier argument est le morph à ajouter et le second indique un cadre. Le cadre doit être une instance de LayoutFrame, ses coordonnées sont indiquées en utilisant le morph contenant (la fenêtre) comme repère.

Voici la méthode buildMainWindow revue pour implanter la zone de texte :

MUIDSimpleEditor1>>buildMainWindow
	"construction de la fenetre"
	| wdw |
	(wdw := SystemWindow new) model: self.
	wdw
		addMorph: self contentsMorph
		fullFrame: (LayoutFrame
				fractions: (0 @ 0 corner: 1 @ 1)).
	self changedFilename.
	^ wdw

Les histoires de cadre, de contenant, de contenu et de fraction ne sont pas compliquées à manipuler. Le chapitre sur les panneaux décrit de façon détaillée l'utilisation des LayoutFrame pour la construction d'IHM plus complexes.

Avant de pouvoir tester, il reste à implanter les méthodes fileContents et setFileContents: qui assurent la mise en oeuvre des interactions entre la vue et le modèle.

MUIDSimpleEditor1>>fileContents
	^ fileContents
		ifNil: [fileContents := '']

MUIDSimpleEditor1>>setFileContents: aStringOrText 
	fileContents := aStringOrText.
	^ true

Pour tester :

MUIDSimpleEditor1 new open

[edit] Interaction de la vue vers le modèle

Lorsque vous saisissez du texte dans la zone de texte, un cadre rouge entoure le morph. Ce cadre indique une édition en cours. Pour valider votre saisie, c'est comme partout dans Squeak, avec alt-s; la conséquence est l'envoi du message setFileContents: au modèle par la vue. Ainsi, la variable d'instance fileContents est affectée avec le texte contenu dans la vue.

setFileContents: affecte le texte passé en argument à la variable d'instance et indique à la vue l'acceptation ou le rejet du texte par le modèle en retournant un booléen; si la méthode retourne true, alors le texte est indiqué comme accepté par le modèle et le cadre rouge disparaît; si le méthode retourne false, alors le texte est indiqué comme rejeté par le modèle et le cadre rouge demeure.

Prenons l'exemple d'une zone de texte dans laquelle on saisi un programme. Le modèle serait un objet pouvant lancer une analyse syntaxique du texte reçu. En cas d'erreur de syntaxe, le compilateur indique une erreur. Dans ce cas, la méthode setFileContents: retourne false et n'affecte pas la variable d'instance fileContents.

Pour sauvegarder le fichier en cours d'édition, on peut implanter la méthode suivante dans le modèle :

MUIDSimpleEditor1>>saveFile
	| filePath fileStream |
	filePath := StandardFileStream fullName: filename.
	fileStream := FileStream forceNewFileNamed: filePath.
	fileStream nextPutAll: self fileContents.
	fileStream close

Pour permettre l'envoi du message, il suffit d'ajouter un item au menu de la fenêtre (voir La fenêtre).

On remarque donc que c'est la dernière version acceptée par le modèle qui peut ainsi être sauvegardée et que pour l'instant, il est encore impossible de sauver le texte en cours d'édition.

[edit] Interaction du modèle vers la vue

La méthode fileContents est simplement un accesseur en lecture sur la variable d'instance correspondante. Elle est exécutée par la vue quand elle doit se mettre à jour en cohérence avec son modèle. Dans notre application, imaginons que la variable d'instance fileContents soit mise à jour par un autre biais que par la saisie, il faut alors que la vue soit mise à jour avec ce nouveau texte.

Comme le montre cette nouvelle version de la méthode setNewFilename:, le message setFileContents: est ici appelée explicitement (et donc pas seulement par la vue). Pour comprendre, le modèle envoie le message setNewFilename: à lui même dans la méthode askForFilename qui est exécutée par menu de la fenêtre ("lire un fichier").

MUIDSimpleEditor1>>setNewFilename: aFilename 
	"change le nom du fichier"
	| filePath fileStream |
	(StandardFileStream isAFileNamed: (filePath := StandardFileStream fullName: aFilename))
		ifTrue: [(fileStream := FileStream oldFileNamed: filePath)
				ifNil: [^ self inform: 'Erreur d''ouverture en ecriture du fichier "' , aFilename , '"']
				ifNotNil: [self setFileContents: fileStream contentsOfEntireFile.
					fileStream close]]
		ifFalse: [self setFileContents: ''].
	filename := aFilename.
	self changedFilename

Si vous testez, vous constatez ... que ca ne marche pas !!!

En fait ca marche presque : le modèle est correctement mis à jour (la variable fileContents) mais pas la vue. En effet, il faut indiquer à la vue de se mettre à jour, c'est à dire, de récupérer le contenu de la variable fileContents pour l'insérer dans la zone de texte. Pour cela, on utilise encore le mécanisme cablé par changed:/update:.


En rappel, update: est envoyé à tous les objets dépendants d'un objet si changed: est envoyé à cet objet. Voici le code de la méthode d'instance changed: de la classe Object :

MUIDSimpleEditor1>>changed: aParameter 
	"Receiver changed. The change is denoted by the argument
	aParameter. Usually the argument is a Symbol that is part of the dependent's
	change protocol. Inform all of the dependents."
	self dependents
		do: [:aDependent | aDependent update: aParameter]

En rappel encore, une vue (ici le PluggableTextMorph) est déclarée comme dépendante du modèle (en général par l'envoi du message model: à la vue lors de la construction de la vue).


Maintenant, regardez les premières lignes de la méthode d'instance update: dans la classe PluggableTextMorph :

MUIDSimpleEditor1>>update: aSymbol 
	aSymbol
		ifNil: [^ self].
	aSymbol == #flash
		ifTrue: [^ self flash].
	aSymbol == getTextSelector
		ifTrue: [self setText: self getText.
			^ self setSelection: self getSelection].
        ...

Si le symbol reçu en argument est identique au symbol correspondant au nom du message qui permet de lire le texte dans le modèle (ici #fileContents) alors, le texte du modèle est affecté à la vue (c'est ce qui est fait par self setText: self getText). Pour assurer cette mise à jour, il suffit d'ajouter la méthode changedFileContents et de modifier la méthode setFileContents: de la façon suivante :

MUIDSimpleEditor1>>setFileContents: aStringOrText 
	fileContents := aStringOrText.
	self changedFileContents.
	^ true

MUIDSimpleEditor1>>changedFileContents
	self changed: #fileContents

Voila, testez et constatez que votre vue est bien mise à jour lorsque vous chargez un fichier.

[edit] Un modèle et plusieurs vues

Si le code que vous écrivez s'intègre correctement à l'environnement Squeak (au sens framework Squeak) vous vous appercevrez avec contentement que vos applications font plus que ce que vous pensiez faire au départ. Ce qui étonnera (j'espère) le débutant c'est que, sans code supplémentaire, il est possible, avec cette version d'éditeur, d'ouvrir plusieurs vues sur le même modèle est que ca fonctionne correctement.

Testez :

| wdw |
 wdw := MUIDSimpleEditor1 new.
 wdw open.
 wdw open

On a bien un seul modèle et deux vues sur le même modèle. Si vous modifiez une vue (par saisie ou par lecture d'un fichier) vous observez que l'autre vue est modifiée automatiquement. Ainsi, les deux vues sont automatiquement maintenues en cohérence.

[edit] Un label et un champ

Pour affiner un peu notre première version d'editeur, on ajoute un label et un champ pour le nom du fichier courant. Le label est un texte fixe et inerte alors que le champ se comporte comme une zone de texte mais sur une seule ligne. Ici, on va aussi pouvoir se servir du champ pour saisir le nom du fichier à lire.

[edit] Le label

Pour créer un label inerte, on peut utiliser le morph StringMorph: pour récupérer un label, on envoie le message #contents:font:emphasis: à la classe StringMorph. Voici la méthode qui crée notre label :

MUIDSimpleEditor1>>filenameLabel
	| sm |
	sm := StringMorph
				contents: 'Filename :'
				font: Preferences standardMenuFont
				emphasis: 1.
	sm lock.
	^ sm

emphasis: est pour récupérer l'emphase du label (gras, italique, souligné ...). Bizarrement, c'est juste un entier (1 c'est pour bold par exemple). Pour un programmeur C, l'utilisation directe d'un entier paraît complètement normal mais en Squeak...on préfère des objets. On dispose de la classe TextEmphasis pour les emphases et on peut demander à une instance de TextEmphasis son code entier en lui envoyant le message #emphasisCode. Pour récupérer le code entier correspondant à gras vous pouvez inspecter le code suivant:

TextEmphasis bold emphasisCode

Voici donc une version plus intelligible de notre méthode :

MUIDSimpleEditor1>>filenameLabel
	| sm |
	sm := StringMorph
				contents: 'Filename :'
				font: Preferences standardMenuFont
				emphasis: TextEmphasis bold emphasisCode.
	sm lock.
	^ sm

On note enfin l'envoi du message #lock qui inactive le StringMorph créé.

[edit] Le champ

Pour la saisie d'une chaîne sur une ligne, on peut utiliser un StringMorph. Pour saisir, on clique dessus avec le bouton gauche et en appuyant simultanément sur la touche shift. Cette ergonomie peut convenir pour des saisies occasionnelles. Ici, on préfère utiliser la classe UpdatingStringMorph qui spécialise StringMorph :

  • La saisie est directement possible en cliquant sur la zone avec le bouton gauche de la souris (pas de shift). Pour accepter la saisie, on valide avec la touche entrée. A noter le comportement follow mouse focus qui fait que c'est toujours le morph qui se trouve sous le pointeur de la souris qui reçoit les évènements. Il faut donc faire attention à bien conserver le pointeur au dessus du champ pendant la saisie (c'est très inconfortable pour des petites zones et pour des dialogues avec beaucoup de champs).
  • UpdatingStringMorph prend en compte un modèle pour la lecture et l'écriture de la chaîne.

La construction s'effectue en utilisant le message UpdatingStringMorph class>>#on:selector:. Le premier argument est le modèle, le second est le symbole qui correspond au message envoyé au modèle pour récupérer le contenu du champ. On indique l'action à effectuer pour mettre à jour le modèle avec le message UpdatingStringMorph>>#putSelector: qui prend en argument un symbole correspondant au message à envoyer au modèle lors de la validation du champ.

On doit aussi indiquer au morph de considérer son contenu comme une chaine de caractères à ne pas interpréter avec le message UpdatingStringMorph>>#useStringFormat. Enfin, par défaut, dès que le focus est perdu, le message associé au morph pour modifier le modèle est envoyé à ce dernier. On peut modifier ce comportement avec le message UpdatingStringMorph>>#autoAcceptOnFocusLoss: avec false comme argument. Le modèle n'est alors modifié que si l'utilisateur valide explicitement la saisie avec la touche entrée.

Le message #hResizing: indique au morph d'occuper le maximum de place dans son conteneur (voir Morph>>#hResizing:). Bon, on ne va pas s'étendre sur ce point ici, reportez vous au chapitre sur les panneaux pour plus d'informations.

Voici le code qui construit notre champ.

MUIDSimpleEditor1>>filenameStringMorph
	| sm |
	sm := UpdatingStringMorph on: self selector: #filename.
	sm putSelector: #setNewFilename:.
        sm useStringFormat.
	sm autoAcceptOnFocusLoss: false.
        sm hResizing: #spaceFill.
	^ sm

[edit] Construction de l'IHM (premier jet)

Voici la construction de l'IHM avec le label et le champ de saisie. Le principe est d'utiliser un panneau horizontal pour contenir le label et le champ bien alignés et solidaires. Un panneau est un AlignmentMorph qui sert de contenant et permet d'organiser son contenu (d'autres morphs) horizontalement et/ou verticalement (voir le chapitre sur les panneaux). On crée donc un panneau contenant le label et le champ de saisie puis on insère le panneau et la zone de texte dans la fenêtre :

MUIDSimpleEditor1>>buildMainWindow
	| wdw searchRow |
	(wdw := SystemWindow new) model: self.
	searchRow := AlignmentMorph newRow.
	searchRow layoutInset: 4.
	searchRow addMorphBack: self filenameLabel.
	searchRow addTransparentSpacerOfSize: 6.
	searchRow addMorphBack: self filenameStringMorph.
	searchRow vResizing: #rigid.
	wdw
		addMorph: searchRow
		fullFrame: (LayoutFrame
				fractions: (0 @ 0 corner: 1 @ 0)
				offsets: (0 @ 0 corner: 0 @ 30)).
	wdw
		addMorph: self contentsMorph
		fullFrame: (LayoutFrame
				fractions: (0 @ 0 corner: 1 @ 1)
				offsets: (0 @ 30 corner: 0 @ 0)).
	self changedFilename.
	^ wdw

On remarque l'utilisation d'un offset vertical (0 @ 30) pour le point bas-droit du panneau (offsets: (0 @ 0 corner: 0 @ 30))) et aussi pour le point haut-gauche la zone de texte (offsets: (0 @ 30 corner: 0 @ 0))). En effet, on veut que le panneau contenant le label et le champ de saisie occupe 30 points de hauteur et que cette hauteur soit fixe quelque-soit la taille globale de la fenêtre (voir le chapitre sur les panneaux).

[edit] Construction de l'IHM (en tenant compte de la fonte)

Dans le premier jet, on utilise un offset vertical de taille fixe arbitraire (0 @ 30). Le problème est que, pour le label et le champ, on utilise une fonte standard (Preferences standardMenuFont) qui peut être modifiée via les préférences globales. Il faut donc tenir compte de la hauteur de la fonte choisie pour le label et pour le champ pour calculer cet offset. Ainsi, l'apparence demeure acceptable en cas de modification des fontes. Pour cela, il suffit d'utiliser directement la hauteur de la fonte pour le calcul de l'offset : on prend sa hauteur et on lui ajoute 8 points pour ne pas coller le texte aux bords supérieur et inférieur du panneau contenant. Maintenant, on peut modifier la fonte utilisée pour le label et pour le champ sans que cela massacre notre IHM :

MUIDSimpleEditor1>>buildMainWindow
	| wdw searchRow offset fl fsm |
	(wdw := SystemWindow new) model: self.
	searchRow := AlignmentMorph newRow.
	searchRow addMorphBack: (fl := self filenameLabel).
	searchRow addTransparentSpacerOfSize: 6.
	searchRow addMorphBack: (fsm := self filenameStringMorph).
	searchRow vResizing: #rigid.
	offset := (fl font height max: fsm font height)
				+ 8.
	wdw
		addMorph: searchRow
		fullFrame: (LayoutFrame
				fractions: (0 @ 0 corner: 1 @ 0)
				offsets: (0 @ 0 corner: 0 @ offset)).
	wdw
		addMorph: self contentsMorph
		fullFrame: (LayoutFrame
				fractions: (0 @ 0 corner: 1 @ 1)
				offsets: (0 @ offset corner: 0 @ 0)).
	self changedFilename.
	^ wdw


[edit] Un menu contextuel pour la zone de texte

L'IHM de l'éditeur montrant un menu contextuel qui surgit lorsqu'on clique sur la zone de texte avec le bouton du milieu (sous unix)
L'IHM de l'éditeur montrant un menu contextuel qui surgit lorsqu'on clique sur la zone de texte avec le bouton du milieu (sous unix)

On veut maintenant ajouter un menu contextuel pour amméliorer un peu l'ergonomie. En effet, utiliser le menu de la fenêtre pour activer des actions propres aux fonctionnalités de l'application n'est pas très intuitif. Par contre, les menu contextuels, activés par le bouton du milieu (sous Unix, bouton droit sous windows il me semble) sont très pratiques. Ce chapitre présente une utilisation simple des menu, reportez vous au chapitre sur les menus pour plus d'informations.

[edit] Menu spécifique

Classiquement, un menu contextuel est construit par une methode qui prend deux arguments, un MenuMorph et un booléen. Le MenuMorph passé est complété avec l'ajout d'items mis en oeuvre par des objets de la classe MenuItemMorph. Le booléen sert à indiquer si la touche shift est pressée lors de l'invocation du menu (accès au menu étendu).

Avec le PluggableTextMorph utilisé pour la zone de texte, il est possible d'utiliser un menu contextuel. Pour cela, on utilise le message PluggableTextMorph>>#on:text:accept:readSelection:menu:. Comme le montre le code suivant, on peut utiliser ce message de construction pour indiquer le symbole correspondant à la methode mise en oeuvre par le modèle pour construire le menu (ici fileContentsMenu:shifted:) :

MUIDSimpleEditor1>>contentsMorph
	^ PluggableTextMorph
		on: self
		text: #fileContents
		accept: #setFileContents:
		readSelection: nil
		menu: #fileContentsMenu:shifted:

Le premier argument de #fileContentsMenu:shifted: est un MenuMorph déjà créé par l'appelant (le controleur).

[edit] Menu simple

Dans un menu simple, on peut avoir soit un séparateur, soit un item actif :

  • On ajoute un séparateur (un trait) en envoyant le message #addLine au MenuMorph.
  • On ajoute un item actif en envoyant le message #add:target:action: au MenuMorph. Le premier argument est le label présenté dans le menu, le second est le receveur, et le troisième argument est le symbole sélecteur. Ce symbole correspond au message qui est envoyé au receveur lorsque l'item est activé.

Lors de la réception du message #addLine ou du message #add:target:action:, le menu morph crée, initialise et ajoute un MenuItemMorph. En fait, normalement, la gestion des MenuItemMorph est complètement masquée (il s'agit d'une classe privée gérée de façon transparente pour le développeur).

MUIDSimpleEditor1>>fileContentsMenu: aMenu shifted: shifted 
	aMenu addLine.
	aMenu
		add: 'accepter (s)'
		target: self
		action: #accept.
	aMenu addLine.
	aMenu
		add: 'lire un fichier'
		target: self
		action: #askForFilename.
	aMenu
		add: 'sauver'
		target: self
		action: #saveFile.
	^ aMenu

Important : la methode qui construit le menu doit retourner le menu construit. Ici, MUIDSimpleEditor1>>#fileContentsMenu:shifted: retourne bien le menu construit avec ^ aMenu

[edit] Sous-menu

Pour structurer ou mieux organiser un menu, on peut mettre en place des sous-menus. Un sous-menu est encore un objet de la classe MenuMorph. Ainsi, de façon régulière, on peut avoir un sous-menu, un sous-sous-menu...

Un sous-menu est ajouté à un menu par l'envoi du message #add:subMenu: au menu. Le premier argument est le label et le second argument est le MenuMorph utilisé comme sous-menu.

Ici, on peut regrouper les opérations d'entrée sortie depuis ou vers le fichier physique dans un sous-menu fichier par exemple. La méthode MUIDSimpleEditor1>>#fileContentsMenu:shifted: est modifiée de la façon suivante :

MUIDSimpleEditor1>>fileContentsMenu: aMenu shifted: shifted 
	| fileIO |
        " construction du sous-menu"
	fileIO := MenuMorph new.
	fileIO
		add: 'lire'
		target: self
		action: #askForFilename.
	fileIO
		add: 'sauver'
		target: self
		action: #saveFile.
	aMenu addLine.
        "ajout d'un item actif simple"
	aMenu
		add: 'accepter (s)'
		target: self
		action: #accept.
	aMenu addLine.
        "ajout du sous-menu"
	aMenu add: 'fichier ' subMenu: fileIO.
	^ aMenu

[edit] Menu étendu

Le second argument de la méthode MUIDSimpleEditor1>>#fileContentsMenu:shifted: est un booléen qui est true si l'utilisateur presse la touche shift en même temps qu'il invoque le menu. On peut se servir de cette information pour construire un menu étendu. Par exemple, ici, on peut considérer que les opérations d'entrée sortie sont accessibles directement dans le menu étendu. Dans la version suivante, si shifted est true, alors, la méthode retourne directement le menu fileIO :

MUIDSimpleEditor1>>fileContentsMenu: aMenu shifted: shifted 
	| fileIO |
	fileIO := MenuMorph new.
	fileIO
		add: 'lire'
		target: self
		action: #askForFilename.
	fileIO
		add: 'sauver'
		target: self
		action: #saveFile.
	shifted
		ifTrue: [^ fileIO]
		ifFalse: [aMenu addLine.
			aMenu
				add: 'accepter (s)'
				target: self
				action: #accept.
			aMenu addLine.
			aMenu add: 'fichier ' subMenu: fileIO.
			^ aMenu]

[edit] Menu standard

Un menu standard pour les zones de texte
Un menu standard pour les zones de texte

Le contrôleur de la classe PluggableTextMorph met en oeuvre un menu standard pour accepter le texte, évaluer, inspecter ou déboguer la sélection... Les méthodes sont mise en oeuvre dans la catégorie PluggableTextMorph>>menu commands. Par exemple, la méthode permettant d'évaluer une selection :

doIt
	self
		handleEdit: [textMorph editor evaluateSelection]

Parallèlement, on dispose aussi directement des raccourcis clavier standards. Pour l'évaluation d'une sélection, c'est alt-d. Les menus et les raccourcis standards sont donc pré-cablés.

[edit] Réutilisation des menus standards

Ces menus standards peuvent être réutilisés pour la construction du menu contextuel de notre éditeur. Ils sont construit par la classe ParagraphEditor. Regardez ses méthodes de classe, dans la catégorie class initialization. On a les méthodes ParagraphEditor class>> #shiftedYellowButtonMenu et ParagraphEditor class>>#yellowButtonMenu. Ces méthodes retournent des instances de la classe SelectionMenu.

SelectionMenu est une sous-classe de PopUpMenu. Cette classe est aussi utilisée pour construire des menus contextuels (sic! deux classes pour la même chose!!). Pour réutiliser ces menus, il faut récupérer leurs constituants (les découper en morceaux) et les réinjecter dans le MenuMorph. Voici un code type qui récupère un menu standard et réinjecte ses éléments dans le menu en cours de construction en utilisant la méthode MenuMorph>>#labels:lines:selections: :

buildMenu: aMenu shifted: shifted 
	stdMenu := shifted
		ifTrue: [ParagraphEditor shiftedYellowButtonMenu]
		ifFalse: [ParagraphEditor yellowButtonMenu].
        aMenu
		labels: stdMenu labelString
		lines: stdMenu lineArray
		selections: stdMenu selections.
        ^ aMenu

On peut bien sur n'intégrer qu'une partie des éléments d'un menu standard. Mais attention, il n'y a pas de notion d'élément de menu standard, on a pas d'objet mais des libellés ('do it (d)' par exemple). La sélection porterait donc sur un ensemble de libellés ce qui est embêtant en cas de changement des libellés.

[edit] Redéfinition des actions

Jusqu'à présent, dans notre éditeur, on a pas développé de méthode pour l'action 'accepter (s)' : on s'est contenté de construire cet élément de menu en indiquant le sélecteur #accept sans mettre en oeuvre de méthode correspondante dans le modèle. Pourtant, ca marche !?.

En effet, si le modèle met en oeuvre une méthode correspondant à un élément de menu, alors, cette méthode est exécutée, sinon, c'est la méthode précablée dans PluggableTextMorph qui est exécutée. Pour comprendre ce qui se passe, il faut savoir que, une fois le sélecteur choisi, le contrôleur du menu envoit le message #perform:orSendTo: au modèle. Le premier argument est le symbole sélecteur de l'élément de menu choisit, le second argument est le morph sur lequel est branché le menu. Regardez le code de cette méthode :

Object>>perform: selector orSendTo: otherTarget 
	"Selector was just chosen from a menu by a user. If can respond,
	then perform it on myself. If not, send it to otherTarget, presumably
	the editPane from which the menu was invoked."
	(self respondsTo: selector)
		ifTrue: [^ self perform: selector]
		ifFalse: [^ otherTarget perform: selector]

En français : si le receveur comprend le message alors, envoyer le message au receveur, sinon, l'envoyer à l'autre cible (qui est ici, le PluggableTextMorph).

Pour vérifier cela, implantez la méthode MUIDSimpleEditor1>>#accept de la façon suivante :

MUIDSimpleEditor1>>accept
   self halt

Pour tester, il vous suffit de saisir un texte dans l'éditeur puis d'accepter ce texte avec le menu et voilà. De cette manière, vous pouvez redéfinir les actions standards quand elles sont activées depuis le menu.


[edit] Barre de titre de menu

[edit] Titre simple

Le menu avec un titre simple
Le menu avec un titre simple

Pour avoir un menu avec un titre, on peut, au niveau de la contruction du PluggableTextMorph l'indiquer en envoyant au morph le message #menuTitleSelector:. Ce message prend en argument un symbole correspondant à un message mis en oeuvre par le modèle et qui retourne le titre du menu associé. Voici la construction du PluggableTextMorph adapté :

MUIDSimpleEditor1>>contentsMorph
	| tm |
	tm := PluggableTextMorph
				on: self
				text: #fileContents
				accept: #setFileContents:
				readSelection: nil
				menu: #fileContentsMenu:shifted:.
	tm menuTitleSelector: #fileContentsMenuTitle.
	^ tm

Le modèle met en oeuvre la méthode #fileContentsMenuTitle :

MUIDSimpleEditor1>>fileContentsMenuTitle
	^ 'MUIDoc: edition'

[edit] Bouton d'accrochage

Le menu avec une punaise
Le menu avec une punaise
On peut ajouter un bouton d'accrochage ou punaise au menu. Ce bouton, à droite de la barre de titre, permet de rendre le menu persistant, accroché dans l'environnement. Pour ce faire, on envoi le message #addStayUpItem au menu, par exemple, dans la méthode de construction du menu, MIDSimpleEditor1>>#fileContentsMenu:shifted:.

[edit] Les raccourcis clavier

[edit] Organisation des classes en jeu

Pour comprendre et éventuellement adapter la gestion des évènements clavier, on n'intervient pas directement sur la classe PluggableTextMorph. Il faut tout d'abord bien visualiser l'organisation des classes en jeu, c'est ce qui est montré par la figure ci-dessus :

  • on utilise la classe PluggableTextMorph, qualifiée donc de publique;
  • un PluggableTextMorph classe délègue la gestion de l'édition à une instance de la classe TextMorphForEditView; quand on saisie du texte, c'est en fait une instance de cette classe qui agit;
  • le contrôle bas niveau de l'édition (gestion de la boucle de lecture du clavier) est effectué par un TextMorphEditor avec notamment le traitement des évènements particuliers comme les raccourcis clavier ou l'accès aux menus (TextMorphEditor est une sous-classe de ParagraphEditor dont on a discuté précédemment pour l'accès aux menus standards).

La méthode PluggableTextMorph>>#textMorph retourne l'instance de TextMorphForEditView associée au PluggableTextMorph.. La méthode PluggableTextMorph>>#textMorphClass retourne la classe à utiliser pour créer le TextMorph associé :

PluggableTextMorph>>textMorphClass
	"Answer the class used to create the receiver's textMorph"
	^ TextMorphForEditView

La méthode TextMorph>>#editor retourne le TextMorphEditor associé au TextMorph. La méthode TextMorph>>#editorClass retourne la classe à utiliser pour créer l'éditeur associé :

TextMorph>>editor
	"Return my current editor, or install a new one."
	editor
		ifNotNil: [^ editor].
	^ self installEditorToReplace: nil

TextMorph>>installEditorToReplace: priorEditor 
	"Install an editor for my paragraph. This constitutes 'hasFocus'.
	If priorEditor is not nil, then initialize the new editor from its state.
	We may want to rework this so it actually uses the prior editor."
	| stateArray |
	priorEditor
		ifNotNil: [stateArray := priorEditor stateArray].
	editor := self editorClass new morph: self.
	editor changeParagraph: self paragraph.
	priorEditor
		ifNotNil: [editor stateArrayPut: stateArray].
	self selectionChanged.
	^ editor

TextMorph>>editorClass
	"Answer the class used to create the receiver's editor"
	^ TextMorphEditor

[edit] Gestion des évènements clavier

Les évènements clavier sont traités par la méthode TextMorphForEditView>>keyStroke: :

TextMorphForEditView>>keyStroke: evt 
	| view |
	(editView scrollByKeyboard: evt)
		ifTrue: [^ self].
	self editor model: editView model.
	"For evaluateSelection"
	view := editView.
	"Copy into temp for case of a self-mutating doit"
	(acceptOnCR
			and: [evt keyCharacter = Character cr])
		ifTrue: [^ self editor accept].
	super keyStroke: evt.
	view scrollSelectionIntoView

Lors d'une réception d'un évènement clavier, le contrôleur global envoit le message keyStroke: au morph qui possède le focus avec en argument, une instance de KeyboardEvent contruite en amont. Mais le traitement proprement dit est effectué par l'éditeur (le TextMorphEditor) qui va vider le buffer clavier et exécuter les actions adéquoites : insérer le caractère ou traiter un raccourci par exemple.

Les raccourcis clavier sont mis en oeuvre au niveau du TextMorphEditor. Ils ont stockés dans les variables de classe ParagraphEditor>>CmdActions et ParagraphEditor>>ShiftCmdActions. Ces variables sont des tableaux comprenant des symboles sélecteurs indexés par des entiers calculés à partir des valeurs ASCII des touches des raccourcis.

Dynamiquement, lorsqu'un raccourcis clavier est saisi, le message correspondant au sélecteur stocké dans le tableau est envoyé au ParagraphEditor.

Voici par exemple, un extrait de la méthode de classe qui initialise la variable CmdActions :

ParagraphEditor>>initializeCmdKeyShortcuts
	"Initialize the (unshifted) command-key (or alt-key) shortcut table."
	"NOTE: if you don't know what your keyboard generates, use Sensor kbdTest"
	| cmdMap cmds |
	cmdMap := Array new: 256 withAll: #noop:.
	"use temp in case of a crash"
	cmdMap at: 1 + 1 put: #cursorHome:.
	"home key"
	cmdMap at: 4 + 1 put: #cursorEnd:.
	"end key"
         ...
	cmdMap at: 127 + 1 put: #forwardDelete:.
	"del key"
	'0123456789-=' do: [:char | cmdMap at: char asciiValue + 1 put: #changeEmphasis:].
	'([{''"<' do: [:char | cmdMap at: char asciiValue + 1 put: #enclose:].
	cmdMap at: $, asciiValue + 1 put: #shiftEnclose:.
	cmds := #($a #selectAll: $b #browseIt: $c #copySelection: ... $z #undo: ).
	1 to: cmds size	by: 2
		do: [:i | cmdMap at: (cmds at: i) asciiValue + 1 put: (cmds at: i + 1)].
	CmdActions := cmdMap

Si vous regardez la classe TextMorphEditor, vous pouvez voir le code de #cursorHome:, #cursorEnd:, #forwardDelete:, #changeEmphasis: etc...

[edit] Redéfinition/spécialisation

Pour modifier ou adapter le comportement d'un PluggableTextMorph il faut spécialiser les classes en jeu. La figure ci-dessus schématise les classes à créer pour une spécialisation :

  • une sous-classe de TextMorphEditor (i.e. MonTextMorphEditor) pour les raccourcis clavier, les menus et pour le comportement lors de la frappe d'un caractère;
  • une sous-classe de TextMorphForEditView (i.e. MonTextMorphForEditView) contenant au moins la méthode #editorClass qui retourne la classe contrôleur à utiliser (i.e. MonTextMorphEditor);
  • enfin, une sous-classe de PluggableTextMorph (MonTextMorph) contenant au moins la méthode #textMorphClass qui retourne la classe du morph à utiliser pour afficher le texte (i.e. MonTextMorphForEditView).

Vous trouverez un exemple dans l'image avec le package Shout. Notez enfin que vous pouvez aussi redéfinir le comportement lors de la frappe d'un caractère en redéfinissant la méthode TextMorphForEditView>>keyStroke: dans la nouvelle sous-classe de TextMorphForEditView. Ainsi, vous pouvez éventuellement éviter d'implanter une nouvelle sous-classe de TextMorphEditor.

Personal tools