Keyboard Modes version 2.5

Ce billet est le premier d’une série consacré aux Modes d’un clavier. Il explique la modélisation et l’implémentation des Modes dans CiviKey. D’autres billets traiteront notamment des points suivants :

  • Les Modes se combinent, s’intersectent, se contiennent : quelles sont les opérations de base que doivent supporter les modes et surtout comment les implémenter efficacement ?
  • Le « Fallback d’un Mode », où comment calculer de la meilleure à la pire les combinaisons de modes qui découlent d’un mode combiné. Ci-dessous un exemple :
    1. « Ctrl » + « Frigo » + « Shift »
    2. « Ctrl » + « Frigo »
    3. « Ctrl » + « Shift »
    4. « Frigo » + « Shift »
    5. « Ctrl »
    6. « Frigo »
    7. « Shift »
    8. «»
    Combien y-a-t-il de fallbacks en fonction de du nombre de modes atomiques ? Comment les générer ?
  • La gestion de « AvailableMode » et « CurrentMode »
    Le fait est que sur un clavier, il y a deux collections de modes très différents : les modes possibles et les modes courants. Exemple de modes possibles « Alt+Ctrl+ Frigo+Word » et de modes courants « » (aucun) ou « Ctrl+ Frigo ». Le fait de supprimer un mode possible entraîne nécessairement la suppression de celui-ci des modes courants, ce qui entraîne la suppression des KeyMode et LayoutKeyMode qui étaient définis par ce mode…
  • Le « Fallback » des KeyMode et des LayoutKeyMode : le Mode courant d’un clavier pilote le comportement (KeyMode) et l’affichage (LayoutKey) d’une touche (Key). Quelle implémentation efficace pour la gestion des touches peut-on envisager ?

 

Un cache centrale : un objet, un mode.

Les Modes sont totalement « objectivés ». Les objets IKeyBoardMode ne peuvent être obtenus que via l’objet Context : sa méthode ObtainMode( string m ) se charge de la normalisation et de trouver l’objet-mode correspondant dans un cache interne (de le créer et de l’y ajouter si besoin). Grâce à ce cache, les « objets-modes » sont uniques (dans le cadre d’un Contexte) : il n’y a qu’un et un seul objet « Ctrl + Shift » (il y a également un unique EmptyMode – le mode vide qui correspond à la chaîne vide).

L’empreinte mémoire est diminuée et on peut se servir de ces « objets-modes » pour porter de la donnée potentiellement plus lourde (ce que l’on va exploiter pour stocker la liste des fall backs de chaque mode) et comme clef efficace de hash (l’identité de l’objet garantit celle du mode).

IKeyboardMode m = Context.ObtainMode( "Alpha+Beta" );
Assert.That( Context.ObtainMode( "\r++\t Beta ++ Alpha ++" ) == m, 
                                        "Canonical ordering and trimming." );

Combined & Atomic Modes : les débuts de l’implémentation

Ces deux types de modes doivent rentrer dans un moule commun : le design pattern du Composite s’impose. Tout mode expose donc les modes atomiques qui le composent.

public interface IKeyboardMode : IComparable<IKeyboardMode>
{
    /// <summary>
    /// Gets the <see cref="IContext"/>. 
    /// </summary>
    IContext Context { get; }

    /// <summary>
    /// Gets the atomic modes that this mode contains.
    /// </summary>
    IReadOnlyList<IKeyboardMode> AtomicModes { get; }

    /// <summary>
    /// Gets a value indicating whether this mode contains zero 
    /// (the empty mode is considered as an atomic mode) or only one atomic mode.
    /// </summary>
    bool IsAtomic { get; }

}

Comme le montre le support de l’interface correspondante, les modes sont comparables (en mathématique, on dit que l’on peut définir une relation d’ordre – strict ou total, au choix – sur l’ensemble des modes) ce qui permet de les ordonner (donc de les trier) en s’appuyant sur la chaîne de caractères normalisée, mais avec une astuce :

 public int CompareTo( IKeyboardMode other )
 {
    if( _modes == other ) return 0;
    int cmp = _modes.Count - other.AtomicModes.Count;
    if( cmp == 0 ) cmp = StringComparer.Ordinal.Compare( other.ToString(), _mode );
    return cmp;
}

Tout mode contenant plus de mode atomique qu’un autre est plus grand. Si les deux modes ont autant de modes atomiques l’un que l’autre, alors la chaîne est utilisée. Cela est rendu possible grâce à la normalisation des chaînes de modes (ordonnancement des modes atomiques du plus fort au plus petit séparés par des +), et il faut inverser la comparaison : « Ctrl » est meilleur que « Shift » (alors qu’il est « plus petit » dans l’ordre lexicographique).

Toutes les opérations liées aux modes utilisent la comparaison ordinale : elle est plus rapide et tout aussi solide que celle indépendante de la culture (sous réserve de normaliser les chaînes, au niveau de la représentation interne de l’Unicode, ce qui est fait dans ObtainMode).

Dans l’implémentation, on retrouve les 3 types de modes (vide, atomiques, combinés) via les 3 constructeurs :

class KeyboardMode : IKeyboardMode
{
  Context _context;
  string _mode;
  IReadOnlyList<IKeyboardMode> _modes;

  /// <summary>
  /// Initializes the new empty mode of a Context.
  /// </summary>
  internal KeyboardMode( Context ctx )
  {
    Debug.Assert( ctx.EmptyMode == null, 
                                "There is only one empty mode per context." );
    _context = ctx;
    _mode = String.Empty;
    _modes = ReadOnlyListEmpty<IKeyboardMode>.Empty;
    _fallbacks = new ReadOnlyListMono<IKeyboardMode>( this );
  }

  /// <summary>
  /// Initializes a new atomic mode.
  /// </summary>
  internal KeyboardMode( Context ctx, string atomicMode )
  {
    _context = ctx;
    _mode = atomicMode;
    _modes = new ReadOnlyListMono<IKeyboardMode>( this );
    _fallbacks = ctx.EmptyMode.Fallbacks;
  }

  /// <summary>
  /// Initializes a new combined mode.
  /// </summary>
  internal KeyboardMode( Context ctx, string combinedMode, IReadOnlyList<IKeyboardMode> modes )
  {
     Debug.Assert( combinedMode.IndexOf( '+' ) > 0 && modes.Count > 1, 
                            "There is more than one mode in a Combined Mode." );
     Debug.Assert( modes.All( m => m.IsAtomic ), "Provided modes are all atomic." );
     Debug.Assert( modes.GroupBy( m => m ).Where( g => g.Count() != 1 ).Count() == 0, 
                            "No duplicate in atomic in modes." );
     _context = ctx;
     _mode = combinedMode;
     _modes = modes;
  }
}

Notez l’utilisation des helpers ReadOnlyListEmpty et ReadOnlyListMono. Le premier implémente la liste des modes du mode vide : le mode vide ne contient aucun mode. Le deuxième implémente les modes d’un mode atomique : un mode atomique contient un seul mode atomique : lui-même.

Attention ! Note importante !

Un mode est immuable (immutable in English), exactement comme une chaîne de caractère en C# ou en Java. Ce qui veut dire qu’ajouter un mode à un mode existant en faisant (en utilisant une des opérations qui feront l'ob jet d'un prochain billet) :

m.Add( m2 );

aboutit à… rien de rien de nada. Comme pour les chaînes de caractères, il faut faire

m = m.Add( m2 )

car toutes les opérations sont des fonctions (qui seraient décorées par des const - http://www.parashift.com/c++-faq-lite/const-correctness.html - en C++ mais ce modificateur n’existe malheureusement pas en C#).

ObtainMode : le cœur

La méthode ObtainMode est centrale à la gestion de ces objets. Elle gère le cache qui est un dictionnaire dont la clef est la version normalisée de la chaîne de caractère d’un mode. Sans être complexe, son implémentation effectue le moins possible d’opérations et elle est un peu longue à commenter de façon détaillée ici. Le code source est disponible ici : https://svn.invenietis.com/svn/CK/Core/trunk/CK.Context/Context/KeyboardMode.cs (anonymous/anonymous)

Le billet suivant traite des opérations possibles sur les modes (et de la façon dont elles sont implémentés).

 

Ajouter un commentaire

  Country flag

biuquote
  • Commentaire
  • Prévisualiser
Loading