Variable Watch Overlay and Logging with log levels

After seeing the print redirect from @mpilgrem I got the idea to do the same with the variable watches: I would like to work fullscreen, but when debugging I need some variables. So I created an overlay where you could set global variables as watch, like in the standard watch. The overlay can be activated and deactivated with a touchable button. I used the method of @mpilgrem to overload the draw() and touched() functions, so you only need to add the source to a new tab in your editor and you can start using it :slight_smile:

I have also added a logger with log levels to the dbg. It works similar to log4cxx, but only logs to the print. See the source for documentation and I will also attach a sample program to use the watch and the logger.

If @mpilgrem will add a rectangle area to his logger too, then you will be able to use both in parallel to see print() on the screen next to the watches and the prints will be dependent of the log level :slight_smile:

Have fun,
Kilam Malik.

Video of the variable watch in action:

Here we go with the code. Just copy and paste it into a new tab of your project. Documentation is in the code. I will also post a sample later.

Have to split because posts have limit on characters.

Part 1/2:

--
-- Debugging helpers
-- -----------------
--
-- You just need to include this file into your project. There is no need to
-- call draw() or touch() etc. because they are overwritten.
--
-- a) Logging with loglevels
-- -------------------------
-- This works similar to log4cxx in other languages. You define the level which should
-- be logged and you pass the loglevel in each of your logging statements. Then you
-- could easily switch between the loglevels depending of e.g. deep debugging (TRACE),
-- only errors (ERROR) or disabled for relase (NODBG).
--
-- E.g. in your code you have some prints like this:
--    dbg.log(ERROR, "No network connection.")
--    dbg.log(WARN, "No camera found, disabling function.")
--    dbg.log(DEBUG, "Calculation took 1,2 seconds.")
--    dbg.log(TRACE, "Touch position:", x, y)
--
-- All parameters are simply passed to the Codea print function, but only if the log
-- level is lower or same then set with dbg.on:
--    dbg.on(WARN)
--
-- This would show WARN and ERROR, but not DEBUG and TRACE. Now you are able to fill
-- your code with debug outputs, but simply control how much is shown in your init function.
-- For normal programming you would maybe use WARN and when you have a problem you switch to
-- DEBUG or TRACE for a short time.
--
--
-- b) Variable watch as screen overlay
-- -----------------------------------
--
-- After seeing the print overlay from @mpilgrem I had the idea to do the same with the
-- watches. Then it would be possible to work without the standard Codea watches and with
-- the full resolution.
--
-- This watch opens an overlay with the size and transparency of your choice and you could
-- add variables like in the standard watch. If you add a table, then the whole table will
-- be shown including subtables. So be careful not to pass to big tables ;-)
--
-- You can close and reopen the watch by clicking the arrow in the top left corner. This
-- makes sense because the watch drops your frame rate.
--
-- After switching the dbg on with dbg.on(<loglevel>) you can set the position on screen:
--    dbg.setWatchRect(WIDTH / 2, HEIGHT / 2 - 20, WIDTH / 2 - 30, HEIGHT / 2)
--
-- You can set the transparency of the background of the watcher:
--    dbg.setWatchAlpha(60)
--
-- You can set the font size. Default is 16:
--    dbg.setFontSize(14)
--
-- Finally you add the variables to be watched. They need to be global variables, otherwise
-- they cannot be reached by the watcher. It can watch number, string, boolean and table:
--    dbg.watch("frameRateAvg")
--    dbg.watch("tbl")
--    dbg.watch("touchPos")
--
-- ---------------------------------------------------------------------------------------
--
-- Have fun,
-- Kilam Malik.
--
-- Next ToDo's
-- -----------
-- Scrollbar for more lines than visible.
-- Watch items of tables. Currently watch("tbl.x") does not work. Not sure if that is possible.
-- Line break for long strings or horizontal scrollbar.

-- Log levels:
NODBG = 0    -- Dont log anything.
ERROR = 1    -- Log errors.
WARN = 2     -- Log warnings and errors.
DEBUG = 3    -- Log debug, warnings and errors.
TRACE = 4    -- Log trace, debug, warnings and errors.

-- Need to,use a fixed width font for faster calculations of space:
local outputFont = "Inconsolata"
local outputFontSize = 16
local charWidth, charHeight

local dbgLevel = NODBG
local showWatch = false
local watchTable = {}
local watchRect = {x=0, y=0, w=WIDTH, h=HEIGHT}
local watchAlpha = 127
local cols = 1
local rows = 1

local xPosText = 0
local yPosText = 0

local imgClosed = image(20,16)
local imgOpened = image(20,16)

-- Do some size precalculations:
local function initFont()
    pushStyle()
        font(outputFont)
        fontSize(outputFontSize)
        charWidth, charHeight = textSize("A")
        rows = math.floor(watchRect.h / charHeight)
        cols = math.floor(watchRect.w / charWidth)
    popStyle()
end

-- Create sprites for open and close:
local function initGfx()
    local x, y
    local pl = 10
    local pr = 10

    for y = 1,20 do
        for x = 1,20 do
            -- imgClosed
            if x == math.floor(pl) or x == math.floor(pr) then
                imgClosed:set(x, y, 0, 0, 0, 100)  -- Full black border.
            elseif x > pl and x < pr then
                if y == 16 then
                    imgClosed:set(x, y, 0, 0, 0, 100)  -- Top border.
                else
                    imgClosed:set(x, y, 0, 255, 0, 255)  -- Inside.
                end
            else
                imgClosed:set(x, y, 0,0,0,0)  -- Outside.
            end
            -- imgOpened
            if x == math.floor(pl) or x == math.floor(pr) then
                imgOpened:set(x, 17-y, 0, 0, 0, 100)  -- Full black border.
            elseif x > pl and x < pr then
                if y == 16 then
                    imgOpened:set(x, 17-y, 0, 0, 0, 100)  -- Top border.
                else
                    imgOpened:set(x, 17-y, 255, 0, 0, 255)  -- Inside.
                end
            else
                imgOpened:set(x, 17-y, 0,0,0,0)  -- Outside.
            end
        end

        pl = pl - 0.5
        pr = pr + 0.5
    end
end

Part 2/2:

-- Draw one line of text:
local function drawLine(s)
    if yPosText < watchRect.y then
        return
    end

    local s2
    if string.len(s) > cols then
        s2 = string.sub(s, 1, cols - 3) .. "..."
    else
        s2 = s
    end
        
    fill(0, 0, 0, 255)
    text(s2, xPosText + 1, yPosText - 1)
    fill(255, 255, 255, 255)
    text(s2, xPosText, yPosText)

    yPosText = yPosText - charHeight
end

-- Draw a variable. Recurse tables:
local function drawVariable(varName, var, indent)
    local typ = type(var)
    local s = indent .. varName .. " (" .. typ .. "): "
    if typ == "number" or typ == "string" then 
        s = s .. var
        drawLine(s)
    elseif typ == "boolean" then
        if var then
            s = s .. "true"
        else
            s = s .. "false"
        end
        drawLine(s)
    elseif typ == "table" then
        drawLine(s)
        for k, v in pairs(var) do
            drawVariable(k, v, indent .. "  ")
        end
    else
        s = s .. "Cant watch this type."
        drawLine(s)
    end
end

-- Initialize and overload draw and touched:
local init = function()
    local olddraw
    
    if not olddraw then
        olddraw = draw
    end

    initFont()
    initGfx()

    draw = function()
    olddraw()
    pushStyle()
        fill(0, 0, 0, watchAlpha)
        noStroke()
        if showWatch then
            font("Inconsolata")
            fontSize(outputFontSize)
            textMode(CORNER)
            textAlign(LEFT)
            textWrapWidth(0)
            rect(watchRect.x, watchRect.y, watchRect.w, watchRect.h)
            rect(watchRect.x, watchRect.y + watchRect.h - charHeight, watchRect.w, charHeight)
            yPosText = watchRect.y + watchRect.h - charHeight
            xPosText = watchRect.x
            local h = watchRect.h - charHeight
            fill(255, 255, 255, 255)
            text("Watches", xPosText, yPosText)

            yPosText = yPosText - charHeight
            for i = 0, table.maxn(watchTable) do
                if watchTable[i] ~= nil then
                    local varName = watchTable[i]
                    local var = _G[varName]
                    drawVariable(varName, var, "")
                end
            end
        end

        -- Draw a minimize/maximize button:
        local ex = watchRect.x + watchRect.w - 12
        local ey = watchRect.y + watchRect.h - charHeight/2
        spriteMode(CENTER)
        if showWatch then
            sprite(imgOpened, ex, ey)
        else
            fill(0,0,0,(255-watchAlpha)/4 + watchAlpha) -- Darker header.
            rect(watchRect.x + watchRect.w - 24, watchRect.y + watchRect.h - charHeight, 24, charHeight)
            sprite(imgClosed, ex, ey)
        end
    popStyle()
    end

    local oldtouched
    if not oldtouched then
        oldtouched = touched
    end

    touched = function(touch)
        local state = touch.state

        if state == BEGAN then
            -- Touch area is larger than the small button to touch it easier (24x24):
            local ex = watchRect.x + watchRect.w - 8
            local ey = watchRect.y + watchRect.h - 8
            if math.abs(ex - touch.x) <= 12 and math.abs(ey - touch.y) <= 12 then
                showWatch = not showWatch
                return
            end
        end

        if oldtouched then
            oldtouched(touch)
        end
    end
end

local setWatchRect = function(x, y, w, h)
    watchRect.x = x
    watchRect.y = y
    watchRect.w = w
    watchRect.h = h
    initFont()
end

local setFontSize = function(s)
    outputFontSize = s
    initFont()
end

local setWatchVisibility = function(b)
    showWatch = b
end

local on = function(dbgl)
    dbgLevel = dbgl
    showWatch = true
    init()
end

local off = function()
    dbgLevel = NODBG
    showWatch = false
end

local log = function(lv, ...)
    if lv <= dbgLevel then
        print(unpack(arg))
    end
end

local watch = function(variable)
    table.insert(watchTable, variable)
end

local setWatchAlpha = function(a)
    watchAlpha = a
end

dbg = {
    on = on,
    off = off,
    log = log,
    watch = watch,
    setWatchRect = setWatchRect,
    setWatchAlpha = setWatchAlpha,
    setFontSize = setFontSize,
    setWatchVisibility = setWatchVisibility
}

Reserved for later source extensions.

And finally a small sample to show the variable watches. Just touch the screen to see some changes. Create a new project with a main tab for this source and another tab for the code above.

-- Sample usage of debug helpers.
-- By Kilam Malik.
---------------------------------------------------------------------

-- Set screen options, outside of setup, otherwise screen coordinates
-- are sometimes wrong:
displayMode(FULLSCREEN)

-- global variables:
frameRateTbl = {}
frameRateTblPos = 1
frameRateMaxNr = 10
frame = 1
frameRateAcg = 0
tbl = { x=5, y=5, s="", o="StringInTable much too long for this so small input field."}
nestedTbl = { x=7, innerTable={a=5, b="SDCFJHBVGZRA"}}
bl = true
str = "This text..."
touchPos = {x=0, y=0}
circPos = {x=-1, y=-1}

-- Use this function to perform your initial setup
function setup()
    -- Initialize redirection of print:
    --redirect.on()
    --redirect.font("Inconsolata", 16)
    --redirect.setLogRect(10, 35, 500, 450)
    --redirect.setLogAlpha(60)

    -- Activate / deactivate debugging outputs here:
    dbg.on(DEBUG)
    dbg.setWatchRect(WIDTH / 2, HEIGHT / 2 - 120, WIDTH / 2 - 30, HEIGHT / 2 + 100)
    dbg.setWatchAlpha(60)
    dbg.setFontSize(18)
    dbg.watch("frame")
    dbg.watch("frameRateAvg")
    dbg.watch("tbl")
    dbg.watch("nestedTbl")
    dbg.watch("bl")
    dbg.watch("str")
    dbg.watch("touchPos")
    dbg.watch("circPos")

    dbg.log(DEBUG, "Initializing done.")
end

function draw()
    -- This sets a dark background color
    background(96, 143, 191, 255)

    fill(165, 193, 34, 255)
    noStroke()
    rect(tbl.x, tbl.y, 50, 50)

    frame = frame + 1

    -- Fill table with last <frameRateMaxNr> frame rates:
    frameRateTbl[frameRateTblPos] = 1.0 / DeltaTime
    frameRateTblPos = frameRateTblPos + 1
    if frameRateTblPos > frameRateMaxNr then frameRateTblPos = 1 end

    -- Calculate an average Framerate:
    frameRateAvg = 0
    for i = 1, frameRateMaxNr do
        local v = frameRateTbl[i]
        if v then -- at the first start some values are nil.
            frameRateAvg = frameRateAvg + v
        end
    end
    frameRateAvg = math.floor(frameRateAvg / frameRateMaxNr)

    tbl.x = WIDTH / 2 + WIDTH  / 3 * math.sin(ElapsedTime/1.19)
    tbl.y = HEIGHT / 2 + HEIGHT / 3 * math.sin(ElapsedTime/0.83)
    local c = math.random(65, 92)
    tbl.s = string.format("%c%c%c%c%c%c", c, c-5, c-2, c-9, c+5, c+9)
    if tbl.x > 500 then
        str = "This text ..."
    else
        str = "changes sometimes."
    end
    bl = tbl.x * tbl.y > 65870

    if (circPos.x > -1 and circPos.y > -1) then
        ellipseMode(CENTER)
        noFill()
        stroke(0, 0, 0, 255)
        strokeWidth(3)
        ellipse(circPos.x, circPos.y, 100, 100)
    end
end

function touched(touch)
    if touch.state == BEGAN or touch.state == ENDED then
        dbg.log(TRACE, string.format("Touch: %d, %d", touch.x, touch.y))
    end

    if (touch.state == BEGAN or touch.state == MOVING) then
        circPos = touchPos
    end

    if touch.state == ENDED then
            if math.abs(tbl.x - touch.x + 25) <= 50 and math.abs(tbl.y - touch.y + 25) <= 50 then
                dbg.log(DEBUG, "Hit yellow cube :-)")
            else
                dbg.log(DEBUG, "Missed yellow cube :-(")
            end
    end

    touchPos.x = touch.x
    touchPos.y = touch.y
end

Very cool!

Hello @KilamMalik. The fruits of your labours are looking good. Some other matters have had first call on my time, but I am close to providing a flexible, multi-pane version of print.redirect, applying also your recent thoughts.

It’ll be great to have both panes, the print and the watch on the screen as overlay in parallel :slight_smile: