Archives pour la catégorie Tips & Tricks

Some tips and tricks about Nspire Lua scripting and/or Lua in general.

Crée tes propres menus sans limite !

Aujourd’hui nous allons apprendre quelque chose qui vous sera extrêmement utile dans vos programmes Lua.
Vous connaissez surement ces fameux menus sur la plupart des applications de votre TI-nspire, que l’on ouvre avec la touche menu, et ou l’on peut soit choisir l’option avec le touchpad, soit sélectionner une lettre ou un chiffre pour exécuter la commande.
Vous devriez penser que reproduire cela sur vos applications Lua est très peu envisageable, vue qu’il faudrait beaucoup de code pour reproduire ces menus de manière exacte.
Pourtant, c’est bien plus simple que vous ne le pensez.

En effet, depuis l’os 3.0, le Lua intègre une fonction très intéressante, la fonction toolpalette.register.
Mais comment elle fonctionne ?
Ce que nous allons faire maintenant, c’est créer une table (si vous ne connaissez pas ce genre de variable en Lua, allez vous renseigner).
Une table qui va contenir toutes les informations nécessaires à notre menu. Voici donc un exemple qui vous fera comprendre plus vite que des mots :

votreTable = {
    {"Couleur",  --Menu parent
        {"Rouge"},  -- Sous options
        {"Vert"},
        {"Bleue"}
    }
}

Notez que vous pouvez très bien construire votre table de la manière suivante :

 votreTable = {{"Couleur",{"Rouge"},{"Vert"},{"Bleue"}}}

Mais la manière montrée au dessus est bien plus claire et lisible, grâce à l’indentation.

Si vous l’avez bien compris, ce menu va nous servir d’abord à choisir l’option Couleur, puis choisir notre couleur entre Rouge, Vert et Bleue.
Super, vous avez crée votre table, qu’est ce qui vous reste à faire ?
Nous allons simplement utiliser le toolpalette.register cité plus haut :

toolpalette.register(votreTable)

Et voila, vous pouvez essayer et voire votre magnifique menu ! …Mais que ce passe-t-il, Erreur ?
Oui, comme l’indique votre erreur, vous devez associer une fonction à votre table. Sinon votre menu ne servirait qu’à décorer.
Notre superbe menu sert à changer la couleur du fond, nous allons donc devoir faire quelques réglages avant de pouvoir admirer le travail.

D’abord, nous allons créer la fonction qui changera notre couleur. Nous allons l’appeler changerCouleur(menuparent,option).
Les deux arguments de cet fonction sont prédéfinis par toolpalette, le premier renvoie le menu parent (ici, l’option « Couleur ») tandis que l’autre renvoie l’option qu’on a sélectionné (Rouge, Vert ou Bleue)
Nous allons également créer une table couleurdufond qui contiendra l’information de quelle est la couleur du fond (par défaut, le blanc : {255,255,255}).
Il nous suffit juste de rajouter un misérable test « If » pour changer la variable couleurdufond en fonction de notre choix d’option.
Voila ce que ca donne :

couleurdufond = {255,255,255}
 
function changerCouleur(menuparent,option)
    if option=="Rouge" then
        couleurdufond = {255,0,0}
    elseif option=="Vert" then
        couleurdufond = {0,255,0}
    elseif option=="Bleue" then
        couleurdufond = {0,0,255}
    end
    platform.window:invalidate()
end

Notez le platform.window:invalidate() à la fin de la fonction changerCouleur, qui sert à appeler on.paint(gc) pour repeindre l’écran avec la nouvelle couleur.
Voila, c’est presque terminé. Plus qu’à coder notre fonction on.paint(gc) :

w = platform.window:width()
h = platform.window:height()
 
function on.paint(gc)
    gc:setColorRGB(unpack(couleurdufond))
    gc:fillRect(0,0,w,h)
end

Le unpack comme son nom l’indique, sert simplement à « casser » la table pour revoyer les valeures qu’elle contient une par une.

Et voila le tant attenu résultat :

Et le beau code source qui va avec :

w = platform.window:width()
h = platform.window:height()
 
couleurdufond = {255,255,255}
 
function changerCouleur(menuparent,option)
    if option=="Rouge" then
        couleurdufond = {255,0,0}
    elseif option=="Vert" then
        couleurdufond = {0,255,0}
    elseif option=="Bleue" then
        couleurdufond = {0,0,255}
    end
    platform.window:invalidate()
end
 
votreTable = {
    {"Couleur",
        {"Rouge",changerCouleur},
        {"Vert",changerCouleur},
        {"Bleue",changerCouleur}
    }
}
 
toolpalette.register(votreTable)
 
function on.paint(gc)
    gc:setColorRGB(unpack(couleurdufond))
    gc:fillRect(0,0,w,h)
end

Nous allons maintenant apprendre à activer/désactiver une option, grâce à la fonction toolpalette.enable(menuparent,option,booléen)
Comme pour notre fonction changerCouleur, l’argument menuparent renvoie « Couleur » alors que l’option renvoie Rouge, Vert ou Bleue.
Si on veut activer l’option, on indique true en dernier argument, sinon, on indique false.
Pour l’exemple, nous allons désactiver l’option Vert :

toolpalette.enable("Couleur","Vert",false)

Voila ce que ça donne :

Pour finir, une bonne petite astuce : la création de raccourcis, très utiles pour éviter d’avoir à chaque fois de réouvrir le menu.
Il vous suffira juste d’indiquer quel est le raccourci dans le menu, et de créer votre fonction on.charIn(touche) :

Ici, notre raccourci seront les touches « r », « v » ou « b » :

function on.charIn(touche)
    if touche=="r" then
        couleurdufond = {255,0,0}
    elseif touche=="v" then
        couleurdufond = {0,255,0}
    elseif touche=="b" then
        couleurdufond = {0,0,255}
    end
    platform.window:invalidate()
end

Et voila, c’est finit, vous maîtrisez les menus maintenant. C’est à vous de libérer votre créativité et coder des programmes epoustouflants en Lua, amusez vous bien 😉

Téléchargement du fichier : Classeur1

Un nouveau Screen Manager plus intelligent

Ca ne fait pas si longtemps que Jim Bauwens nous a agréablement surpris avec une façon élégante de personnaliser l’objet « gc » avec vos propres fonctions. Mais il frappe encore !

En effet, il a trouvé/créé un nouveau moyen, plus intelligent qu’avant, de créer un Screen Manager. Si vous n’êtes pas familier avec ce superbe concept, allez-donc vous renseigner 😉
Pour ceux qui le sont, par contre, vous devez savoir qu’une partie du code nécessaire est le listing/lien complet des fonctions à utiliser, des événements vers les screens.

Par exemple :

function on.arrowKey(key) activeScreen:arrowKey(key) end
function on.timer() activeScreen:timer() end
 
-- long list of all handled events...       Then, somewhere else :
 
function myScreen:arrowKey(key)
    -- Your awesome code goes here
end
function myScreen:timer()
    print("I'm the myScreen timer, ticking....")
end
 
-- etc.

Ca marche, certes, mais … c’est un peu barbant.

Allons voir ce que Jim a créé.
Je définis cette nouvelle façon comme « plus intelligente » parce qu’avec ce code, vous n’aurez même plus besoin explicitement de vous tracasser sur la gestion des événements comme vous en avez l’habitude (avec des on.arrowKey, on.enterKey… ou le bon vieux on.paint ). Oui, vous avez bien lu : plus besoin de « function on.paint(gc) … » etc. !

« Qu’est-ce que c’est que cette magie noire », me direz-vous ?
Hé bien, encore une fois, tout repose sur l’utilisation futée des métatables Lua.
En gros, les métatables sont un ensemble de propriétés définissant le comportement d’un objet (généralement une table).
Il existe une propriété « __index » que vous pouvez définir, qui va décrire comment la table réagira quand le script appelle un élément non-défini de celle-ci. Assez utile, croyez-moi 😉
Bref, le truc c’est que quand vous écrivez « function on.paint(gc) », tout ce que vous faites, c’est implémenter la méthode « paint » de la table « on » (d’où le point).
Ce que l’on souhaite pouvoir faire, c’est de se débarasser de cette décalration explicite, et de rediriger l’événement « paint » vers tel ou tel Screen (et les autres événements aussi…)
Bref, nous allons donc utiliser une méthode « eventDistributer » qui prend comme arguments tout ce qui lui est passé, grâce à l’utilisation de « … » (l’événement suivi de ses paramètres, si c’est le cas), et qui les « passe » au Screen que l’on veut (tout en vérifiant que cela est bien possible pour le screen en question):

local triggeredEvent = "paint"  -- first declaration, it will be overwritten anyway
local eventDistributer = function (...)
     local currentScreen = GetScreen()
     if currentScreen[triggeredEvent] then
         currentScreen[triggeredEvent](currentScreen, ...)
     end
end

Ce code devrait vous paraître assez clair.

Ce qu’il nous reste désormais à faire, c’est de lier cette fonction à l’__index de la métatable de « on » (vous remarquerez l’intelligente utilisation des closures) :

local eventCatcher = {}
 
eventCatcher.__index = function (tbl, event)
    triggeredEvent = event
    return eventDistributer
end
 
setmetatable(on, eventCatcher)

Ce code permet au script de comprendre que quand un événement, sans handler explicite, est déclenché, il devra exécuter cette fonction (qui retourne (closure) une fonction qui prendra les arguments de l’événement et qui redirigera le tout via l’eventDistributer, donc vers l’event handler approprié du bon Screen). Je conçois que ceci peut vous paraître un peu confus/complexe, mais vous finirez par comprendre 🙂

Bref, voila le code complet du Screen Manager et de l’eventDistributer.
(Comme d’habitude, vous aurez à créer (et push) vos Screens, par la suite)

local screens = {}
local screenLocation = {}
local currentScreen = 0
 
function RemoveScreen(screen)
    screen:removed()
    table.remove(screens, screenLocation[screen])
    screenLocation[screen] = nil
    currentScreen = #screens -- sets the current screen as the top one on the stack.
    if #screens<=0 then print("Uh oh. This shouldn't have happened ! You must have removed too many screens.") end
end
 
function PushScreen(screen)
    -- if already activated, remove it first (so that it will be on front later)
    if screenLocation[screen] then
        RemoveScreen(screen)
    end
 
    table.insert(screens, screen)
    screenLocation[screen] = #screens
 
    currentScreen = #screens
    screen:pushed()
end
 
function GetScreen()
    return screens[currentScreen] or RootScreen
end
 
Screen = class()
 
function Screen:init() end
 
function Screen:pushed() end
function Screen:removed() end
 
RootScreen = Screen() -- "fake", empty placeholder screen.
 
local eventCatcher = {}
local triggeredEvent = "paint"
 
local eventDistributer = function (...)
     local currentScreen = GetScreen()
     if currentScreen[triggeredEvent] then
         currentScreen[triggeredEvent](currentScreen, ...)
     end
end
 
eventCatcher.__index = function (tbl, event)
    triggeredEvent = event
    return eventDistributer
end
 
-- finally linking everything
setmetatable(on, eventCatcher)

Je suis sur que parmi vous lecteurs, nombreux sont ceux intéressés par un .tns d’exemple directement, « tout prêt » 😉 Je vous ai préparé ceci : cliquez ici pour télécharger le fichier. C’est un exemple de base avec quelques événements, mais sans une seule fonction on.xxx définie explicitement, et dans 2 screens. J’ai plutôt assez bien commenté le code pour que vous puissiez comprendre rapidement 🙂

Pour un exemple plus terre-à-terre, montrant en même temps cette technique et celle de la personnalisation du gc, je vous suggère de télécharger le jeu Memory de Jim, ici.

Comment ajouter vos propres fonctions à « gc »

Salut à tous,

Comme vous le savez, afin de réaliser des opérations graphiquesen Lua sur la plateforme Nspire, vous devez utiliser gc et ses méthodes que TI a créé, comme drawString, drawRect, fillArc, etc.

Et bien, comment feriez-vous pour avoir votre propre fonction « gc », comme drawRoundRect ?

Vous pourriez très bien faire quelque chose comme :

function drawRoundRect(gc, x, y, wd, ht, rd)
        if rd > ht/2 then rd = ht/2 end
        gc:drawLine(x + rd, y, x + wd - (rd), y)
        gc:drawArc(x + wd - (rd*2), y + ht - (rd*2), rd*2, rd*2, 270, 90)
        gc:drawLine(x + wd, y + rd, x + wd, y + ht - (rd))
        gc:drawArc(x + wd - (rd*2), y, rd*2, rd*2,0,90)
        gc:drawLine(x + wd - (rd), y + ht, x + rd, y + ht)
        gc:drawArc(x, y, rd*2, rd*2, 90, 90)
        gc:drawLine(x, y + ht - (rd), x, y + rd)
        gc:drawArc(x, y + ht - (rd*2), rd*2, rd*2, 180, 90)
end
 
function on.paint(gc)
        drawRoundRect(gc, 100, 50, 20, 15, 5)
        ...
end

Certes, ceci fonctionne.

Mais ne serait-ce pas plus sympa de pouvoir écrire quelque chose du genre : « gc:drawRoundRect(100,50,20,15,5) », qui fait bien plus naturel et qui ne requiert pas le passage explicite de gc en premier argument ? 😉
Well, here is the definitive solution to this, by Jim Bauwens 😉

function AddToGC(key, func)
        local gcMetatable = platform.withGC(getmetatable)
        gcMetatable[key] = func
end
 
local function drawRoundRect(gc, x, y, wd, ht, rd)
        -- the code above
end
 
AddToGC("drawRoundRect", drawRoundRect)
 
function on.paint(gc)
        gc:drawRoundRect(100, 50, 20, 15, 5)
        ...
end

 

A noter :

Vous avez pu remarqué l’utilisation de platform.withGC, qui est une fonction de l’API « 2.0 »+ (OS 3.2+). VOici comment la « recréer » pour les versions plus anciennes :

if not platform.withGC then
        platform.withGC = function(func, ...)
            local gc = platform.gc()
            gc:begin()
            func(..., gc)
            gc:finish()
        end
    end

 
[Update] : John Powers from TI commented that this definition of platform.withGC has some limitations, and proposed this better version (thanks !) :

if not platform.withGC then
    function platform.withGC(f)
        local gc = platform.gc()
        gc:begin()
        local result = {f(gc)}
        gc:finish()
        return unpack(result)
    end
end

Comment convertir un fichier .lua en .tns ?

Retour à la partie 1

Vous avez deux possibilités :

1) Utiliser le logiciel Nspire pour ordinateur

Depuis la version 3.2 (mi-2012), le logiciel Nspire (« TINCS ») inclut un éditeur de script, pour directement créer, éditer, tester, et débugger vos codes Lua à l’intérieur des documents Nspire 🙂

Il est disponible depuis le menu Insérer > Editeur de script.

2) Utiliser « Luna »

Luna est un outil communautaire et open-source, créé par Olivier « ExtendeD » Armand, permettant de créer des fichiers .tns (TI-Nspire) à partir notamment d’un code source Lua.

Vous pouvez télécharger un exécutable Windows ici sur TI-Planet.

Pour les autres plateformes, vous pouvez compiler son code source, disponible au même lien.

Pour l’utiliser, il suffit d’écrire, dans un terminal / invite de commandes, le chemin vers le fichier Lua puis vers celui du .tns que vous voulez créer :

1
luna.exe myscript.lua mydocument.tns

Partie 3 !

Tester le type d’un objet avec l’opérateur is()

Le Lua sur TI-Nspire permet de créer facilement des classes avec la fonction class() qui permet également de créer des classes héritées, basées sur le modèle de la classe mère avec quelques différences. Malheureusement, étant donné que le concept de classes n’est que superficiel, il n’existe ni fonction ni syntaxe spécifique pour travailler avec certains types de classes. Dans ce tutoriel nous allons voir comment reproduire le fonctionnement de l’opérateur is() présent dans d’autres langages de programmation orienté objet, tel que le C#.

L’opérateur is() permet de distinguer un objet qui a été créée à partir d’une classe spécifique, mais aussi des classes mères (Etant donné que l’héritage en Lua est une pure copie de la classe mère, il n’est pas possible de gérer l’héritage avec l’opérateur is que nous allons créer).

Voici un petit exemple de ce qu’on désire faire :

function is(obj, class)
  -- we are going to create it
end
 
A = class()
function A:init(x)
  self.x = x
end
 
B = class(A)
function B:init(x)
  self.x = x * 2
end
 
t = { A(1),  B(1) }
for k, v in pairs(t) do
  if is(v, A) then
    print("A class, value = "..v.x)
  elseif is(v, B) then
    print("B class, value = "..(v.x / 2))
  end
end

Nous voyons ici très bien l’intérêt : si nous stockons dans un tableau des objets issus de classes héritées donc portant certaines propriétés en commun, nous voulons dans certains cas distinguer si l’objet est issu de telle ou telle classe.

Mais alors comment écrire la fonction is() dans ce cas ? Rajouter un champ ? Non ! En effet, le fonction class() affecte déjà certains champs sans que vous ne le sachiez. class() affecte le champ __index de la table (ou objet), et il contient un pointeur vers la classe mère. Lorsque vous faites a = A() , a.__index est égal à A.__index (en terme de références).

Ainsi, voici la fonction is()

function is(obj, class)
  return obj.__index == class.__index
end

NB : Comme la limite entre classes et objet est assez floue en Lua (contrairement à d’autres langages où un objet est instancié par une classe), il est possible de tester si deux objets appartiennent à la même classe mère de la même manière :

A = class()
function A:init() end
a = A()
b = A()
is(a, b) -- returns true

Appel au constructeur de la classe mère

Vous êtes très certainement nombreux à avoir remarqué que l’utilisation des classes en Lua sur TI-Nspire était pratique, mais trop simple pour optimiser le code dans certains cas de figure.

Ce cas particulier est la possibilité d’appeler le constructeur de la classe mère dans le constructeur de la classe fille pour éviter de répéter les instructions. En effet, au moindre changement de champs dans la classe mère impliquait qu’il faille changer toutes les classes filles pour les modifier en conséquence.

Voici un exemple :

A = class()
function A:init(x)
  self.t = 0
  self.x = x
end
 
B = class(A)
function B:init(x, y)
  self.t = 0
  self.x = x
  self.y = y
end

Ici, si on veut changer t = 0 du constructeur de la class mère, il faudrait alors changer également le constructeur de la classe B pour rester cohérent. Une première approche consiste à procéder comme suit :

A = class()
function A:init(x)
  self.t = 0
  self.x = x
end
 
B = class(A(0, 0)) -- construction of the object before inheriting
function B:init(x, y)
  self.x = x
  self.y = y
end

Mais là encore, le champ x est répété.

Une chose que nous avons oublié de considérer ici est que B est une classe héritée de A, donc les champs de A sont forcément répétés dans B. Pourtant le constructeur de B, on est justement en train de le redéfinir en écrasant celui venant de A. N’y a-t-il pas moyen d’appeler le constructeur de A tout en passant l’objet B en paramètre ?

Et bien oui !

Une picure de rappel pour bien comprendre est cependant nécessaire. En effet, faire ceci :

dummy = class()
function dummy:doSomething()
  self.mark = 0
end
dummy:doSomething()

revient exactement au même que faire cela :

dummy = class()
function dummy.doSomething(self)
  self.mark = 0
end
dummy.doSomething(dummy)

ou encore :

dummy = class()
function dummy:doSomething()
  self.mark = 0
end
dummy.doSomething(dummy)

La présence des doubles points permet de passer automatiquement en 1er paramètre l’objet portant la méthode. Donc rien de nous empêche de ne pas utiliser ce sucre syntaxique pour faire passer un autre objet à la place de l’objet portant la propriété !

Ce qui revient à faire comme suit :

A = class()
function A:init(x)
  self.t = 0
  self.x = x
end
 
B = class(A)
function B:init(x, y)
  A.init(self, x) -- call the A class constructor in order to avoir repetitions
  self.y = y
end

Comment avoir une petite fonction « input » bien sympa en Lua…

(improved version of Nick Steen’s website’s example)

Vous pourriez être très surpris par le fait qu’il n’y ait pas de manière native d’avoir une fonction d’entrée clavier (input) texte sur l’API Lua Nspire. En effet, c’est un peu bizarre car ce sont souvent utilisés pour de nombreux types de programmes (que ce soit dans les jeux pour taper le nom d’utilisateur, ou d’autres applications pour entrer des données utilisateur, etc.)

Quoi qu’il en soit, ne vous inquiétez pas, vous pouvez le programmer vous-même avec un petit bout de code à ajouter à votre script! Il capture en fait les touches appuyées avec la fonction on.charIn , et les enregistre ce que l’utilisateur tape dans une chaîne (concaténation). Il suffit ensuite d’afficher cette chaîne à l’écran, et c’est tout !

Si vous voulez que l’utilisateur sera en mesure de supprimer quelques lettres, il suffit d’ajouter du code pour la fonction on.backspaceKey, et c’est tout!

Dans l’exemple ci-dessous, nous avons fixé une limite de caractères à 25, mais vous pouvez ajuster cette valeur vous-même.

Bref, voilà le code dans sa totalité :

input = ""   
 
function on.paint(gc)
    gc:drawString(input,5,5,"top")  -- display string
end
 
function on.charIn(char)
    if string.len(input) <= 25 then   -- limit of 25 chars
        input = input..char   -- concatenate
        platform.window:invalidate()   --screen refreh
    end
end
 
function on.backspaceKey()
    input = string.usub(input,0,-2)  -- deleting last char
    platform.window:invalidate()  
end

Enregistre un record (ou autre) sans possibilité de triche !

Dans ce tutoriel nous allons voir une notion assez intéressante du framework de la TI-Nspire en Lua : enregistrer et restaurer des données dans un format quelconque directement en Lua ce qui permet par exemple de se souvenir d’un record pour un jeu, ou une quelconque configuration pour un programme plus évolué.

Mise en situation : Nous venons de réaliser un jeu génial. Nous voulons que nos camarades puissent se mesurer à notre record, mais comment faire ?

La première méthode consiste à utiliser l’API var et d’appeler var.store(). Ainsi, on sauvegarde le record dans le classeur comme variable globale.

Votre système de record est en place ! Hum, pas si vite : un de vos camarades a réussi à faire un score de 10 000 000 ! Comment est-ce possible ?? Il a simplement édité la variable qui vous sert de record. Etant donné qu’elle est accessible via une application Calcul et modifiable à souhait cela n’est pas compliqué !

La deuxième méthode utilise la première, à ceci près que nous disposons de math.eval() qui nous permet d’exécuter n’importe quelle commande du TI-Basic, comme la commande Lock par exemple. Ainsi, si nous faisons :

math.eval("Unlock highscore")
var.store("highscore", highscore)
math.eval("Lock highscore")

Notre variable est protégée contre l’écriture. Seulement, étant donné que Lock est une commande présente dans le TI-Basic, elle peut très bien être exécutée à l’extérieur du programme Lua. Donc cela ne fait que repousser le problème.

Et si nous nous tournions vers le Lua en lui même ? En effet, il existe var.monitor() qui permet de poser une sentinelle qui vérifie si la variable pointée va être changée. Si oui, alors l’évènement on.varChange() est appelé. L’intérêt d’utiliser cette technique est qu’elle permet de contrôler la modification de la variable. En effet :

function on.create()
  if not var.recall("highscore") then
    highscore = 0
    var.store("highscore", highscore)
  end
  var.monitor("highscore")
end
 
function on.varChange(list)
  for k, v in pairs(list) do
    if k == "highscore" then
      if var.recall(k) ~= highscore then
        return 1 -- it is an external change, block modifications
      else
        return 0 -- it is an internal change, allow modifications
      end
    end
  end
  return 0 -- allow modifications of other monitored variables if any
end

Ce code empêchera toute modification extérieure de record par un message d’erreur :

Changement non autorisé : Entrée non valide.

Seulement voilà … C’est une méthode lourde et répétitive si on doit le faire pour plusieurs données, telle des données de configuration … C’est là que l’on regarde la documentation et qu’on aperçoit deux évènements qui pourraient paraître banals à première vue : on.save() et on.restore(). En réalité, ce couple d’évènement gère ce que nous essayons de faire depuis le début !

En effet, lorsque le widget est fermé (on ferme le classeur, on copie/coupe le widget), l’évènement on.save() est appelé. on.save() doit être écrit de telle sorte qu’il puisse renvoyer une donnée quelconque (booléen, nombre, chaîne de caractères, table etc …).
Lorsque le widget sera ouvert la prochaine fois (on ouvre le classeur, on colle le widget), l’évènement on.restore() sera appelé avec en paramètre cette donnée que nous avions renvoyé depuis on.save() !

Ainsi, plus de prise de tête :

function on.save()
  return highscore
end
 
function on.restore(data)
  if type(data) == "number" then
    highscore = data
  end
end
 
function on.create()
  if not highscore then
    highscore = 0 -- first time we initialize highscore
  end
end