Archives pour l'étiquette metatable

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