Le modes d’un clavier

Une des idées que je préfère dans le CVK, ce sont les « Modes » du clavier qui généralisent les touches « modifiantes » telles que Ctrl, Alt, Shift et autres Fn, et Alt Gr. Dans la première version du CVK (celle écrite en C++), les modes étaient gérés par un entier dans lequel chaque bit était assigné à un mode : on ne pouvait pas en définir plus de 32 – ce qui ne représentait pas un inconvénient majeur – et leur sémantique était fixée, figée, gravée dans ces bit flags – ce qui était beaucoup plus gênant.

L’idée

Le principe retenu, depuis la version 2, s’affranchit totalement de ces deux limitations et permet d’introduire quelques notions, design patterns et algorithmes intéressants.

L’idée est simple :

  • Les modes sont identifiés par des chaînes de caractères quelconques : « Ctrl », « Frigo », « Majuscule », etc. On les appelle modes atomiques (Atomic Mode).
  • Ils sont combinés simplement en les appariant séparés avec des ‘+’ : « Ctrl+Majuscule », « Frigo+Ctrl+Majuscule ». Ce sont les modes combinés (Combined Mode).
  • A chaque « touche » (Key) est associée un ensemble de « touches réelles » (Actual Keys) qui, chacune, est dédiée à un mode (combiné ou atomique) précis.

L’exemple

Un exemple très simplifié de contexte est donné ci-dessous, qui respecte la structure des objets d’un Contexte : Context / Keyboard / Zone / Key / ActualKey. Ce contexte contient un clavier avec deux touches :

  • Clavier « Démo »
    • Zone « Alphabet »
      • Touche n°1
        • Par défaut (pas de mode actif) : Label = ‘a’, Command = ‘SenKey « a »’
        • En mode « Maj » : Label = ‘A’, Command = ‘SenKey « A »’
        • En mode « Frigo » : Label = ‘Ouvrir Porte’, Command = ‘Domo.OpenFridgeDoor()’
      • Touche n°2
        • Par défaut (pas de mode actif) : Label = ‘b’, Command = ‘SenKey « b »’
        • En mode « Frigo » : Enabled = false

En résumé ce clavier va pouvoir envoyer les caractères ‘a’, ‘A’ et ‘b’ ainsi qu’ouvrir la porte du frigo. Il n’y a plus qu’à se poser quelques questions :

  • En mode « Maj + Frigo », qui gagne ? ‘A’ ou ‘Ouvrir Porte’ ?
  • Si on décide de supprimer le mode « Maj » des modes possibles, que devient l’ActualKey correspondante ? (Bon, là, on se doute qu’il faut la supprimer.)
  • « Shift + Ctrl » est bien le même mode que « Ctrl + Shift », non ?

Il existe bien d’autres questions à se poser ET à résoudre. Un gros paquet. Vraiment.

Normalisation : « Shift + Ctrl » est bien le même mode que « Ctrl + Shift » !

Il est donc nécessaire d’introduire une forme canonique (une représentation pivot, normée) qui permette de s’affranchir de cette dépendance à l’ordre des modes.

Ce n’est pas compliqué : « Shift + Ctrl » est transformé en « Ctrl + Shift » car… « Ctrl » précède « Shift » dans l’ordre lexicographique.

Grâce à cet ordonnancement, les modes combinés sont aussi facilement manipulables que les modes atomiques. D’autre part, l’ordre lexicographique nous apporte aussi une possibilité de désambiguïsation : si « Ctrl » et plus fort que « Shift » (il est devant), alors « Frigo » est plus fort que « Maj » !

Et je peux alors répondre à la première des questions précédentes : en mode « Frigo+Maj » (remarquer que Frigo est devant), c’est ‘Ouvrir Porte’ que voit l’utilisateur.

Cette normalisation ayant été comprise, on peut aborder l’implémentation. La version 2.5 n’a plus grand-chose à voir avec la version 2.0 en termes de gestion des modes. Avant de détailler la nouvelle, je voudrai revenir rapidement sur la précédente afin de montrer ses limites.

La première version

La première version implémentait les Modes comme une classe qui encapsulait la mise en forme canonique dans son constructeur. Un Mode n’était qu’un objet (immuable) autour d’une chaîne de caractère : utiliser ce type de classe est aisé, notamment en définissant des opérateurs de casting implicite de et vers les chaînes de caractères.

Le constructeur de l’objet KeyboardMode acceptait une chaîne de caractère et la normalisait avant de la conserver et de l’exposer en lecture seule : toute chaîne de caractère, à tout moment, pouvait donc être « transformé » en mode. L’aspect « lecture seule » de la chaîne une fois le mode construit garantissait ensuite l’immuabilité d’un tel mode.

L’inconvénient majeur de cette approche est qu’il n’y a pas de centralisation, de réutilisation des différents modes en mémoire : chaque objet mode est, en fait, une « valeur » (au sens des Types Valeurs) qui existe là où elle est nécessaire (dans chaque définition d’ActualKey par exemple).

Ce principe de programmation (une classe avec une sémantique de valeur) a eu plusieurs impacts négatifs :

  1. 1 - Il n’a pas été facile à mettre en œuvre par les étudiants : on ne sait jamais, finalement, s’il vaut mieux utiliser un objet Mode ou une chaîne de caractère. Cette ambigüité a polluée l’API à plusieurs endroits.
  2. 2 - Il n’était pas spécialement performant ni particulièrement extensible (pas d’opportunités pour cacher des informations lourdes ou coûteuse à produire notamment).

Ajoutons à cela que la manipulation de chaînes de caractères systématiquement requise pour travailler sérieusement avec les Modes (intersection, combinaison, soustraction, etc.) a freiné leur mise en œuvre et les développements des fonctionnalités s’y attachant.

Il était temps (et amusant :-)) de faire quelque chose. L’implémentation nouvelle est disponible dans la 2.5 et fera l’objet de très prochains billets.