Archives de catégorie : Tutoriels

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

Des tutoriaux Nspire Lua de A-à-Z

Cette page lie les excellents tutoriaux de Steve Arnold, sur CompassTech.

Ils sont tous en anglais, cependant.
Vous pouvez trouvez quelque de nos tutoriaux en français ici.

 





The lessons in this next section require OS 3.2 functionality.




Leçon 3 : Varier le contenu d’une table et opérateur conditionnel

Dans cette leçon, nous allons continuer notre travail sur les tables et sur les variable issues de TI-Nspire. Nous allons facilement customiser notre document, et avec un changement d’une ligne, nous allons créer une page qui peut être utilse à ceux qui veulent tirer avantage du Lua dans leur programmes TI-Nspire.
Mais avant, nous allons quitter Oclua pour TI-Nspire Scripting Tools, ou pour Luna. Tous deux sont légers et faciles à utilser, mais différents de Oclua. Pour cela, je vous envoie vers ce lienqui présente les façons d’exécuter un script écrit en Lua.

 

Leçon 3.1 : Modifions le contenu de nos lignes d’affichage

Un simple changement dans notre script transforme notre affichage de plusieurs lignes en un outils plus…utile.
Souvenez-vous : la ligne du script qui créait chacune des lignes d’affichage était celle-ci :

table[k] = "Ligne#"..k
Elle concaténait la valeur actuelle de k avec le string « Ligne# », ce qui faisait de la première « Ligne#1 ».
Souvenez-vous : la commande var.recallva chercher la valeur d’une variable TI-Nspire et l’attribue à une variable du script lua. Dans notre exemple, nous l’avions utilisé pour saisir le nombre de lignes que l’on voulait afficher.Maintenant, supposons que nous ayons une variable appelée ligne1, contenant ce que l’on veut afficher en première ligne. Puis une autre appelée ligne2 et ainsi de suite ? On pourait facilement contrôler le contenu de notre affichage, et les modifier n’importe quand depuis la Ti-Nspire. Pour cela, on pourrait simplement changer la ligne ci-dessus par :
table[k] = (var.recall("line"..k) or "Line #"..k)

Etudiez ces nouvelles lignes et essayez de comprendre ce qui se passe. Lorsque k = 1, var.recall recherche une variable appelée ligne1 et la range dans table[1] (La première entrée de la table, « table »). Si on ne la trouve pas, alors elle est stockée en tant que « ligne #1 ». (On aurait très bien pu laisser un blanc, avec juste «  ».) Et ainsi de suite pour les différentes valeurs de k.

Donc, en stockant ces variables dans un document TI-Nspire, elles sont immédiatemment affichées dans la page du script Lua.

Leçon 3.2 : Les conditions

Pour notre introduction au lua, nous avons besoin d’autres ingrédients : les structures conditionnelles. Ces dernières se ressemblent fortement en Lua et en TI-Nspire.
if condition then
  instructions
else
  instructions
end

Seul le endif se transforme en end !

Prenons un exemple : on veut que la première et la dernière ligne soient de la même couleur. Reprenons notre script.

for k = 1, linecount do
  gc:setFont("sansserif", "r", 10)
  gc:setColorRGB(158, 5, 7)
  table[k] = (var.recall("line"..k) or "Line #"..k)
  strwidth = gc:getStringWidth(table[k])
  strheight = gc:getStringHeight(table[k])
  gc:drawString(table[k], w/2 - strwidth/2 ,h*k/(linecount+1) + strheight/2)
end
Maintenant, il suffit d’ajouter une condition dans le bloc For. Si la ligne actuelle est la première / dernière, alors elle sera colorée.
if k == 1 or k == linecount then
  gc:setFont("sansserif", "b", 10)
  gc:setColorRGB(20, 20, 137)
else
 gc:setFont("sansserif", "r", 10)
 gc:setColorRGB(158, 5, 7)
 
end
Il faut bien entendu mettre le tout dans la fonction on.paint(gc). Aussi, notez l’emploi du double signe égal que l’on utilise pour tester une égalité. Un simple signe égal est utilisé pour une définition. (k = 1)
A part cela, vous êtes largement en mesure de comprendre ce morceau de script. Sinon, retournez voir les leçons précédentes !Et de 3 ! Vous venez de finir la troisième leçon de ce tutoriel. Maintenant vous pouvez afficher du texte (un peu plus intéressant) et utiliser les conditions ! Prenez un peu de temps avant de passer à la suite ! ;)Dans le prochain cours, nous apprendrons à écrire du texte directement dans notre fenêtre Lua, ce qui est la base pour les jeux de type quizz !

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 !

Comment réaliser un Gestionnaire d’Ecrans

Lorsque l’on crée un jeu ou un outil quelconque de manière « complète », on désire souvent réaliser un panel d’écrans dans lequel l’utilisateur naviguera. Il existe plusieurs manières de réaliser un moteur qui nous permettra de gérer tous ces écrans, du plus simple à coder mais le moins pratique, au plus difficile à comprendre mais facile d’utilisation. L’objectif de ce tutoriel n’est bien évidemment pas de vous donner une idée fixe de ce à quoi ressemble un gestionnaire d’écran (il en existe une multitude de types), mais de vous présenter un panel de choix. Ce tutoriel est long car exhaustif en exemples et théorie, ne lâchez pas le fil!

Tout d’abord, ce que l’on entend par « gestionnaire d’écrans » c’est une manière d’organiser le code afin que l’on puisse isoler les parties de dessin des parties fonctionnelles. Plus cette frontière est marquée, plus le code est propre et lisible, et permettra donc d’intégrer plus facilement notre fameux « gestionnaire d’écrans ».

Prenons un exemple, nous souhaitons réaliser un jeu simple avec un menu, le jeu, une page d’aide et une page de records. A première vue, le nombre d’écrans est limité et fixé avant la création par notre cahier des charges. Ce même nombre est faible (4) et il est donc facile d’envisager quelque chose en ce sens :

function on.create()
  screen = 0 -- 0=menu, 1=game, 2=help, 3=highscores
end
 
function on.paint(gc)
  if screen == 0 then -- we draw the menu
  elseif screen == 1 then -- we draw the game
  elseif screen == 2 then -- we draw the help
  elseif screen == 3 then -- we draw the highscores
  end
end
 
function on.enterKey()
  if screen == 0 then
    screen == 1
  elseif screen == 1 then
....

Ok ! inutile d’aller plus loin ! On a compris. Pour créer un code moche, long à écrire et j’en passe, c’est la meilleure solution. Il est avant tout destiné aux applications de très petite taille car facilement implémentable.

Il y a cependant moyen de faire moins moche, en utilisant des tableaux de fonctions.

function on.create() -- or on.construction with OS >= 3.2
  menu, game, help, highscores = 1, 2, 3, 4
  screen = menu
  paints = {}
  enterKeys = {}
end
 
function on.paint(gc)
  paints[screen](gc)
end
 
function on.enterKey(gc)
  enterKeys[screen](gc)
end
 
paints[menu] = function(gc)... end -- we draw the menu here
paints[jeu] = function(gc) ... end -- we draw the game here
...
enterKeys[menu] = function(gc)... end -- menu handling here
enterKeys[jeu] = function(gc) ... end -- game handling here
....

Ici, il est évident que la lecture du code sera bien plus simple qu’auparavant. Par ailleurs, l’utilisation d’étiquettes au lieu de valeurs propres améliore considérablement la lecture du code ! N’hésitez pas à en abuser ! Vous pouvez, si vous avez trop de « constantes », utiliser un tableau de constantes afin de les organiser, comme ceci :

screen = {menu=1, jeu=2, aide=3, records=4}
--> screen.menu == 1

Ou encore pour faire quelque chose d’automatisé (plus besoin de compter/décaler les chiffres) :

function createEnv(t)
  local env = {}
  for i, v in ipairs(t) do
    env[v] = i
  end
  return env
end
screen = createEnv({"menu", "game", "help", "highscores"})
--> screen.menu == 1

Ceci étant, la gestion des différents écrans reste archaïque pour des usages autres que les jeux. Il est bien entendu possible de créer un système plus élaboré qui facilite l’utilisation et le coding. Quoi demander de plus ?

Vous l’aurez deviné, il faut utiliser … les classes !

Ce que l’on va s’apprêter à coder est en fait une surcouche de l’API TI-Nspire. Vous connaissez les évènements on.paint(), on.arrowKey() etc … et bien nous allons by-passer leur utilisation par notre gestionnaire d’écrans. L’intérêt de coder une classe mère ici est que nous sommes sûr qu’un écran a telles ou telles méthodes; méthodes que l’on appellera depuis les évènements standards sans risquer d’en oublier lorsque l’on code un nouvel écran.

Tout d’abord, il faut créer notre classe Screen :

Screen = class()
function Screen:init() end

Pour commencer, un écran n’a pas besoin d’initialisation spéciale. Rappelons le encore une fois, ce que nous définissons est une surcouche de l’API TI-Nspire, nous allons donc écrire (sans en définir le contenu !!) exhaustivement la liste des fonctions évènementielles qui sont disponibles :

function Screen:paint(gc) end
function Screen:arrowKey(key) end
function Screen:timer() end
function Screen:charIn(ch) end
....

Ces fonctions sont qualifiées de « virtuelles » qui devrons être redéfinies . Parce que la classe ne comprend ici que des fonctions virtuelles, elle est également qualifiée de virtuelle (un peu de vocabulaire ne fait pas de mal). Le gestionnaire d’écrans passe par la création d’une classe virtuelle, un modèle, un tampon qui nous garanti l’existence des fonctions lorsque l’on va coder la liaison évènementielle :

activeScreen = Screen()
function on.paint(gc) activeScreen:paint(gc) end
function on.arrowKey(key) activeScreen:arrowKey(key) end
function on.timer() activeScreen:timer() end
function on.charIn(ch) activeScreen:charIn(ch) end

Ainsi, lorsque l’on veut rajouter le code pour un écran, on code comme si on ne codait qu’un écran ! Il suffit de penser que « Menu: » est « on. »

Menu = class(Screen)
function Menu:init() end
function Menu:paint(gc) gc:drawRectangle(0, 0, 50, 50) end
function Menu:arrowKey(key) ... end
activeScreen = Menu()

Cette technique est très utilisée mais elle a cependant un point faible : à chaque changement d’écran on recréer un objet d’écran. Dans certains ça peut être très utile (réinitialisation du jeu), dans d’autres c’est inutile (le menu ne bouge pas). On peut toujours, stocker ça dans des variables, ou bien passer uniquement la classe elle même en paramètre. Mais il y a encore mieux.

En effet, il est souvent d’usage de coupler cette technique avec un pile. Une pile est liste spéciale : une structure de donnée surnommée « first in, first out » (ou encore FIFO), littéralement, « premier rentré, premier sorti ». L’utilisation d’une pile ici permet de stocker les écrans initialisés dans une liste ordonnée, tout en conservant le fonctionnement précédent.

Nous allons donc rajouter la gestion de la pile. Pour cela il faut changer les accès à activeScreen par activeScreen(), remplacer les assignations de activeScreen par une fonction qui va rajouter l’écran à la pile (PushScreen()) et ne pas oublier de dépiler les écrans une fois qu’on ne s’en sert plus.

Voici ce que cela donne :

------ Screen Manager
Screen = class()
 
function Screen:init() end
 
-- virtual functions to override
function Screen:paint(gc) end
function Screen:timer() end
function Screen:charIn(ch) end
function Screen:arrowKey(key) end
function Screen:escapeKey() end
function Screen:enterKey() end
function Screen:tabKey() end
function Screen:contextMenu() end
function Screen:backtabKey() end
function Screen:backspaceKey() end
function Screen:clearKey() end
function Screen:mouseMove(x, y) end
function Screen:mouseDown(x, y) end
function Screen:mouseUp() end
function Screen:rightMouseDown(x, y) end
function Screen:help() end
 
local Screens = {}
 
function PushScreen(screen)
    table.insert(Screens, screen)
    platform.window:invalidate()
end
 
function PullScreen()
    if #Screens > 0 then
        table.remove(Screens)
        platform.window:invalidate()
    end
end
 
function activeScreen()
    return Screens[#Screens] and Screens[#Screens] or Screen
end
 
-- Link events to ScreenManager
function on.paint(gc)
   for _, screen in pairs(Screens) do
        screen:paint(gc)
    end
end
 
function on.timer()
    for _, screen in pairs(Screens) do
        screen:timer()
    end
end
 
function on.charIn(ch) activeScreen():charIn(ch) end
function on.arrowKey(key) activeScreen():arrowKey(key) end
function on.escapeKey()	activeScreen():escapeKey() end
function on.enterKey() activeScreen():enterKey() end
function on.tabKey() activeScreen():tabKey() end
function on.contextMenu() activeScreen():contextMenu() end
function on.backtabKey() activeScreen():backtabKey() end
function on.backspaceKey() activeScreen():backspaceKey() end
function on.clearKey() activeScreen():clearKey() end
function on.mouseDown(x, y) activeScreen():mouseDown(x, y) end
function on.mouseUp() activeScreen():mouseUp() end
function on.mouseMove(x, y) activeScreen():mouseMove(x, y) end
function on.rightMouseDown(x, y) activeScreen():rightMouseDown(x, y) end
function on.help() activeScreen():help() end
 
function on.create() PushScreen(Menu()) end
function on.resize() end

NB : le fait d’être exhaustif à ce point n’est pas obligé. Il faut par contre que le nombre de fonctions dans la classe Screen corresponde au nombre d’évènements définis.

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

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