CiviKey évolue encore un peu

Comme je vous le disais il y a pile un mois (ou un peu plus), une version 2.6.0 de CiviKey est dans les cartons pour la nouvelle année.

Et bien une nouvelle release de la distribution Alpha est disponible (la 2.6.1). Cette release apporte des corrections de bugs dans différents plugins, et surtout de nouvelles fonctionnalités !

Une aide intégrée, facile à utiliser, et bientôt facile à éditer.

 

Un éditeur de clavier beaucoup plus puissant.

Il permet maintenant de créer, modifier, supprimer des touches, des zones, ainsi que leurs layouts etc etc etc. Bref vous pouvez (presque) tout faire avec !

 

 

Niveau stabilité, il reste un peu de chemin à faire afin de corriger les derniers bugs qui peuvent survenir lors de l'utilisation des nouveaux plugins. Cependant le plus dur est fait, il reste maintenant à passer les derniers coups de polish et nous serons près d'ici 2013 pour vous livrer une nouvelle version !

On vous tient au courant :)

Comme la dernière fois, n'hésitez pas à lire le code sur GitHub ou à nous suivre sur Twitter ou Facebook

Une version 2.6.0 de CiviKey pour 2013

Et oui, outre la sortie du nouveau site officiel en aout dernier et un léger problème de mise à jour automatique, CiviKey continu d'évoluer !

Grâce au soutien de la Fondation Garches et d'Alcatel Lucent, CiviKey passera pour la nouvelle de la version 2.5.2 à la version 2.6 !

Une 2.6, pas une 3.0 ?

Et non, pas une 3.0 car aucun changement majeur n'a été fait au sein du noyau depuis la grande bataille de la 2.5. L'objectif de la 2.6 est d'ajouter à CiviKey différents plugins indispensables aux ergothérapeutes et personnes en situations de handicap qui l'utilisent au jour le jour.

Au menu de cette 2.6 les fonctionnalités que tout bon clavier visuel se doit d'avoir : une prédiction de mot, et une solution de défilement des touches du clavier. S'ajoute à cela quatre nouveautés :

  • un clavier permettant grâce au défilement de déplacer et d'effectuer opérations de base de la souris
  • un assistant au premier démarrage
  • un assistant création 
  • et édition de clavier, et enfin une aide contextuelle intégrée

D'ici 2013 ... et avant ?

La sortie finale de la version 2.6 est prévu pour 2013. Vous souhaitez d’ores et déjà essayer nos builds fraichement sortis du four ?

Contactez-nous et nous vous fournirons un installeur spécifique pour la pre-release. Cette alpha version:

  • est déployée parallèlement à tout autre installation existante;
  • se mettra à jour sur notre "canal alpha", un canal de mise à jour différent de celui de la version stable actuelle.

En attendant voici quelques captures des derniers plugins !

La prédiction et défilement fonctionnant sur la skin WPF

Le clavier souris avec le défilement

La première étape de l'assistant création et édition de clavier

 

Evidemment pour ne pas perdre la tradition des posts technique sur l'architecture et l'implémentation de CiviKey de ce blog, deux posts suivront dans les prochaines semaines.

Le premier fera le point sur l'implémentation actuelle du défilement, les concepts sous-jacents ainsi que les évolutions prévues. Et enfin le second plongera dans les tréfonds de la prédiction de mot et explicitera comment cette fonctionnalité majeure est intégrée au sein de notre architecture de plugins. Vaste programme !

En attendant, n'hésitez pas à lire le code sur GitHub ou à nous suivre sur Twitter ou Facebook

Les opérations sur les modes de clavier

Les Keyboard Modes ont chacun (cf. le post précédent), une liste des modes atomiques qui les composent : c’est un ensemble au sens mathématique du terme et les caractéristiques et opérations traditionnelles sur les ensembles sont utiles dans notre cas :

  • L’ensemble vide : Θ est notre mode vide (EmptyMode).
  • L’union : « Ctrl + Shift » U « Ctrl + Frigo » = « Ctrl + Shift + Frigo »
  • L’intersection : « Ctrl + Shift » ∩ « Ctrl + Frigo » = « Ctrl »
  • La soustraction : « Ctrl + Shift » - « Ctrl + Frigo » = « Shift »

Les prédicats suivants sont pratiques :

  • ContainsOne (teste que l’intersection de deux modes est non vide) : X.ContainsOne( Y ) <=> X ∩ Y != Θ
  • ContainsAll (teste qu’un mode est totalement contenu dans un autre) : X.ContainsAll( Y ) <=> X ∩ Y = Y

La liste des modes atomiques est ordonnée (selon la comparaison de chaîne ordinale évoquée précédemment) et cette propriété permet d’implémenter les différentes opérations efficacement. L’idée est d’avancer deux index dans les 2 listes (Gauche contient m éléments et Droite en contient n) à traiter en comparant les éléments : à chaque comparaison, on détecte un élément à gauche uniquement, à droite uniquement ou dans les deux.

Lorsque les deux éléments sont égaux, on peut avancer les deux index simultanément, sinon seul l’un des index avance : au pire des cas il y a donc m+n comparaisons et dans le meilleur des cas min(m,n).

L’implémentation repose sur une unique fonction qui applique l’algorithme ci-dessus sur les deux listes et qui est paramétrée par 3 prédicats (des fonctions booléenes prenant un élément en paramètre) :

static void Process( KeyboardMode left, 
                     IKeyboardMode right, 
                     Predicate<IKeyboardMode> onLeft, 
                     Predicate<IKeyboardMode> onRight,  
                     Predicate<IKeyboardMode> onBoth );

Ces prédicats sont appelés dans l’ordre d’apparition des modes atomiques dans les deux modes left et right selon que l’élément apparait à gauche uniquement, à droite uniquement ou dans les deux. Ces fonctions peuvent retourner false pour arrêter le processus. Process est utilisé pour toutes les opérations sur les modes.

Une seule fonction : pour combien d'opérations différentes ?

Voyons d'abord l'implémentation des deux prédicats.

Les prédicats ContainsOne/ContainsAll

ContainsOne est peut-être la plus simple : dès qu’un élément commun est trouvé, on peut conclure positivement et arrêter le processus. Il suffit donc de fournir une fonction qui arrête le processus et mémorise le succès dans le cas où un élément a été trouvé dans les deux listes.

bool IKeyboardMode.ContainsOne( IKeyboardMode mode )
{
  bool found = false;
  Process( this, mode, 
           null, 
           null,
           delegate( IKeyboardMode m ) { found = true; return false; } );
  return found;
}

ContainsAll est presque identique : c’est le cas « à droite seulement » qui est pisté et l’opposé qui est retourné car si on trouve un mode à droite seulement, c'est que c'est un "alien" qui ne devrait pas être là. (C'est une façon de parler, hein! Les étrangers ont bien le droit d'être là, sinon où serions nous ?)

bool IKeyboardMode.ContainsAll( IKeyboardMode mode )
{
  bool foundAlien = false;
  Process( this, mode,
           null,  // Use lambda syntax below instead of delegate keyword.
           m => { foundAlien = true; return false; }, 
           null );
  return !foundAlien;
}

Un Process, deux prédicats. Il est temps d'aborder les opérations plus complexes.

L’intersection

L’intersection est la plus simple des combinaisons : seuls les éléments qui apparaissent dans les deux listes doivent apparaitre dans le résultat final. On utilise donc une fonction (paramètre onBoth) qui ajoute l’élément à une liste (un collecteur) et continue le processus.

Problème : il nous faut donc appeler la méthode List.Add (qui ne retourne rien) alors que la méthode Process attend un prédicat (une fonction qui retourne un booléen).

Le code est le suivant :

IKeyboardMode IKeyboardMode.Intersect( IKeyboardMode mode )
{
  List m = new List<IKeyboardMode>();
  Process( this, mode, null, null, Adapter.AlwaysTrue<IKeyboardMode>( m.Add ) );
  return Context.ObtainMode( m );
}

L’adaptateur qui suit permet d’envelopper (wrapper) une action dans un prédicat toujours vrai :

public class Adapter 
{
  static public Predicate<T> AlwaysTrue<T>( Action<T> a )
  {
    return delegate( T o ) { a( o ); return true; };
  }
}

L’air de rien, ces quelques lignes mettent en œuvre simultanément les génériques, les fonctions anonymes et la clôture.

Personnellement je trouve ça superbe et c’est - à mon humble avis - un exemple de code qui démontre bien la puissance de ce drôle de langage que devient C#.

J’ai cependant essayé de trouvé plus élégant, l’appel devenant par exemple :

// Perfection (at least for me)… Sadly, it does not work.
var actionAlwaysTrue = m.Add.ToPredicate( true ); 

Malheureusement, cette solution (une méthode d’extension sur le delegate Action) ne permet que d’écrire :

var actionAlwaysTrue = ((Action<IKeyboardMode>)m.Add).ToPredicate( true );

Car la méthode Add doit être explicitement considérée comme un delegate afin que les méthodes d’extension puissent s’appliquer. De même, la forme suivante n’est pas syntaxiquement correcte :

// Not perfect… And does not work.
var actionAlwaysTrue = Adapter.ToPredicate( m.Add, true ); 

Le compilateur exige la définition explicite du type, celui-ci ne pouvant être inférée par le type de l’action, ce qui nous ramène à :

var actionAlwaysTrue = Adapter.ToPredicate<IKeyboardMode>( m.Add, true ); 

Alors, en désespoir de cause, je préfère conserver mes deux petits helpers AlwaysTrue (et AlwaysFalse) :

var actionAlwaysTrue = Adapter.AlwaysTrue<IKeyboardMode>( m.Add );

L’union, la soustraction et le toggle

L’union se définit ainsi : que l’élément apparaisse à droite, à gauche ou dans les deux, il doit apparaitre dans le résultat final. C’est donc la même fonction qui est fournie aux 3 cas et qui ajoute l’élément à la liste (le collecteur).

IKeyboardMode IKeyboardMode.Add( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  var add = Adapter.AlwaysTrue<IKeyboardMode>( m.Add );
  Process( this, mode, add, add, add );
  return Context.ObtainMode( m );
}

Ici, j’ai utilisé var pour définir la variable car le contexte est suffisamment explicite et l’algorithme suffisamment simple. En règle générale, je type explicitement mes variables : d’expérience le code est nettement plus lisible et maintenable ainsi.

La soustraction consiste à ne conserver que les modes qui apparaissent à gauche :

IKeyboardMode IKeyboardMode.Remove( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  Process( this, mode, Adapter.AlwaysTrue<IKeyboardMode>( m.Add ), null, null );
  return Context.ObtainMode( m );
}

Le petit dernier est le « Toggle ». Intuitivement, il s’agit de l’opération qui « inverse » l’état d’un mode particulier, par exemple :

  • Toggle( « Ctrl + Shift », « Ctrl » ) => « Shift »
  • Toggle( « Shift », « Ctrl » ) => « Ctrl + Shift »

En quelques mots: si le mode est présent je le retire, sinon je l’ajoute. Evidemment, cela doit fonctionner avec plusieurs modes :

  • Toggle( « Ctrl + Shift », « Ctrl + Frigo » ) => « Shift + Frigo »
  • Toggle( « Shift + Frigo », « Ctrl + Frigo » ) => « Ctrl + Shift »

Ce toggle est trivial à implémenter grâce à la fonction Process car il suffit simplement de collecter ceux qui sont à gauche, ceux qui sont à droite, mais pas les éléments communs.

IKeyboardMode IKeyboardMode.Toggle( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  var add = Adapter.AlwaysTrue( m.Add );
  Process( this, mode, add, add, null );
  return Context.ObtainMode( m );
}

This is the end. Mais j’ai pourtant eu envie de continuer…

Toutes les opérations possibles ?

Intrigante cette fonction Process qui permet de tout implémenter. A-t-on oublié des opérations intéressantes ?

Mon idée est de me servir de Process pour vérifier que je n’ai rien oublié : le tableau suivant recense toutes les utilisations possibles de la méthode d’ajout à un collecteur (il y a 3 slots, chacun pouvant être null ou add, il y a donc 23 = 8 possibilités).

onLeft    onRight    onBoth    Name & description    Comments
null null null “Empty” Useless.
null null add Intersect (keep commons) Implemented. Kind of symmetric of ‘Toggle’.
null add null “Cleanup” (keep theirs only) Useless since it is the symmetric of ‘Remove’
null add add “Other” (keep theirs and commons, reject mine) Useless. Opposite of ‘This’.
add null null Remove (keep mine only) Implemented. Opposite of ‘Cleanup’
add null add “This” (keep mine and commons and reject theirs) Useless. Opposite of ‘Other’.
add add null Toggle (keep mine, theirs, but reject commons) Implemented.
add add add Add Implemented.

Ces 4 fonctions couvrent bien l’ensemble des opérations sensées. Vous certainement pas, mais moi, ce constat me rassure Smile.

 

PS: Pourquoi le tableau ci-dessus est-il en anglais ? Parceque qand je code, je "suis" en anglais. Et que ce tableau, je l'ai fais dans le code.

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).

 

NextResult() and so what ?

J’ai parfois du mal à me souvenir du fonctionnement précis de certaines API. Le comportement du IDataReader avec plusieurs résultats en fait partie. Un petit test unitaire s’impose :

[Test]
public void TestDataReaderBehavior()
{
       ExecuteReader( "select 1;",
              delegate( IDataReader r )
       {
             Assert.IsTrue( r.Read(), "We are directly BEFORE the first record." );
             Assert.AreEqual( 1, r.GetInt32( 0 ) );
             Assert.IsFalse( r.Read() );
             Assert.IsFalse( r.Read(), "One can always call Read." );
             Assert.IsFalse( r.NextResult() );
             Assert.IsFalse( r.NextResult(), "One can always call NextResult." );
             Assert.IsFalse( r.IsClosed, "A data reader must explicitely be closed (except if obtained with ExecuteReader( CommandBehavior.CloseConnection )." );
       } );
 
       ExecuteReader( "select 1; select 2;",
              delegate( IDataReader r )
       {
             Assert.IsTrue( r.Read() );
             Assert.AreEqual( 1, r.GetInt32( 0 ) );
             Assert.IsTrue( r.NextResult() );
             Assert.IsTrue( r.Read(), "Here also we are BEFORE the first record." );
             Assert.AreEqual( 2, r.GetInt32( 0 ) );
             Assert.IsFalse( r.Read() );
             Assert.IsFalse( r.NextResult() );
       } );
 
       ExecuteReader( "select 1 from sys.tables where 0=1; select 2;", 
              delegate( IDataReader r )
       {
             Assert.IsFalse( r.Read(), "First select has no results." );
             Assert.IsTrue( r.NextResult() );
             Assert.IsTrue( r.Read() );
             Assert.AreEqual( 2, r.GetInt32( 0 ) );
             Assert.IsFalse( r.Read() );
             Assert.IsFalse( r.NextResult() );
       } );
 
       ExecuteReader( "select 1;select 'Impossible' from sys.tables where 0=1;select 3",
              delegate( IDataReader r )
       {
             Assert.IsTrue( r.Read() );
             Assert.AreEqual( 1, r.GetInt32( 0 ) );
             Assert.IsTrue( r.NextResult(), "There is a 2nd result..." );
             Assert.IsFalse( r.Read(), "...but it is empty." );
             Assert.IsTrue( r.NextResult(), "There is a 3rd result..." );
             Assert.IsTrue( r.Read() );
             Assert.AreEqual( 3, r.GetInt32( 0 ) );
             Assert.IsFalse( r.Read() );
             Assert.IsFalse( r.NextResult() );
             Assert.IsFalse( r.IsClosed );
       } );
}

Cela vaut bien un long discours, non ?
En tous cas, cela vaut mieux que la documentation MSDN ci-dessous qui annonce faussement qu’après l’appel à NextResult, le lecteur est positionné sur le premier enregistrement. J’ai signalé l’erreur (après m’être créé un login .Net passport suite à des années de résistance).

Je vais pouvoir compter les jours avant la correction Smile.
 
Update jeudi 8 octobre 2009 : Il n’y a toujours pas eu de corrections mais une contribution supplémentaire de David Matson qui considère que l’interprétation de la remarque doit se comprendre au niveau de chaque result set et non au niveau des lignes.

Il a complètement raison ce monsieur !