First attempts: Redirecting print() to the viewer

(Latest update) This discussion relates to the first attempts to develop code to redirect print() output from the output pane to the viewer. The most recent version of the code has grown and has been decamped to a new discussion here.

Very cool. I bet this is much faster than the built in output window, too.

I am going to edit my initial post, because I believe that pushStyle() preserves more than is currently documented in the Codea in-app reference, based on the definition of GraphicsStyle() in the Codea Runtime Library, including text and font-related matters.

very cool @mpilgrem !

@mpilgrem that sounds like a documentation bug. I should have documented that pushStyle() preserves font and text settings, I overlooked it.

I may start using this as it allows me to test in full screen and still get my debug text that I print out. Thanks for sharing.

Thank you! I’ve updated the code in my original comment above to allow the font to be set, allow wrapping, tolerate changes of orientation (text already redirected does not re-wrap on changed orientation) and change slightly the appearance of the scroll bar.

(Update) A more elaborate example of its use has been moved to this discussion here.

This is really awesome @mpilgrem. I’ve been debugging a plist parser and printing out the parsed output to the console was an annoying wait at best, and caused Codea to crash at the worst. Thanks to this I was able to print out the output from really long plists to debug them without issue.

I have further updated the code in the original comment to correct an error - I forgot to apply the clean function to string l before processing it. Apologies. (Update) In addition, I have now corrected clear and process to deal with empty lines and added a visual cue to the region captured by the scroll bar.

This is really great @mpilgrem. I love this functionality, because I want to see my logs in fullscreen.

I have made two changes to it for my usage which might be interesting to others, so I post them:

  • Draw the text first in black then in white with 1 pixel shift in x and y → Better readabilty on light backgrounds.

  • Added a close button to hide the log. When the next log is added, it is shown again.

I have marked all changes with -->, see below.

Kilam.

...

local scrollBarTop = 0
-- New variable to define if the log should be shown:
--> local showOutput = false
local output = {}

...

        l = clean(l).."\
" -- Enhanced
        process(l)
        resetFirstLine()
-- Whenever the print function is called, set the log to be shown:
-->        showOutput = true
    end

    local olddraw = draw
    draw = function ()
        olddraw()
-- Jump out of the draw function, if it should not be shown currently:
-->        if not showOutput then
-->            return
-->        end
        pushStyle()

...

        for i = fli, lli  do
            local l = output[i]
            h = h - lineHeight
-- Draw the text black and 1 pixel shifted before drawing it in white. Gives better contrast on different colors.
-->            fill(0, 0, 0, 113)
-->            text(l, 1, h - 1)
            fill(255, 255, 255, 255)
            text(l, 0, h)
        end

...

        if scrollBarVisible then
            local l = scrollBarLength
            fill(127, 127, 127, 255)
            noStroke()
            local t = scrollBarTop
            rectMode(CORNER)
            rect(WIDTH - 9, t - l, 8, l)
            ellipseMode(CENTER)
            ellipse(WIDTH - 5, t, 8, 8)
            ellipse(WIDTH - 5, t - l, 8, 8)
        end

-- Draw the close button:
-->        fill(127, 127, 127, 127)
-->        rectMode(CORNER)
-->        rect(WIDTH - 30, HEIGHT - 20, 20, 20)
-->        stroke(229, 27, 27, 127)
-->        strokeWidth(5)
-->        lineCapMode(SQUARE)
-->        smooth()
-->        line(WIDTH - 30, HEIGHT, WIDTH - 10, HEIGHT - 20)
-->        line(WIDTH - 30, HEIGHT - 20, WIDTH - 10, HEIGHT)
        popStyle()
    end

...

    touched = function (touch)
-->        local state = touch.state    -- moved up two lines
        if #output > heightInLines then
            if state == ENDED and scrollBarVisible then
                scrollBarVisible = false
                return
            end
...
        end

-- Set the show flag to false if the close button is touched:        
-->        if math.abs((WIDTH - 20) - touch.x) < 10 and
-->           math.abs((HEIGHT - 10) - touch.y) < 10 then
-->            showOutput = false
-->            return
-->        end
        if oldtouched then oldtouched(touch) end          
    end
end

...

Thank you, @KilamMalik. I have updated the code in my first post for your excellent extensions, with some minor variations. (Update) The updated code is available at the start of this discussion here.

Cool, I like that close button. Thanks. Maybe I will have another addition to set the position and size of the logging. Then I can combine it with my watcher view.

Hi @mpilgrem, I have made another addition to your code. As I want to view your logging overlay next to another overlay from me, I need to pass the position and size of the logging view. It works as far as I can see, but I did not fully understand the scrollbar calculations, so I had to trial and error a bit.
Also I have added a transparent rectangle as background and a title bar. Maybe you like to add these additions. I will post my watch overlay soon, so people could use both at the same time.

Sample call:

    redirect.setLogRect(10, 35, 500, 450)
    redirect.setLogAlpha(60)

Hmmm, I cant post the full Source, it says body 2300 characters too long. How did you add your code then, because I did not add that much lines :slight_smile:

Splitting into two comments:

--
-- Redirect, version 2012.07.09.23.00
--
-- Use redirect.on() to turn redirection on, and
-- redirect.off() to turn it off. Use redirect.font(name, size) to
-- set the font used, before first use.
--

-- These variables hold the font used. For the tab function to
-- make sense, a fixed-width font is best.
local outputFont = "Inconsolata"
local outputFontSize = 17

-- This variable determines how much of the right of the
-- viewer will take over the scroll bar touches.
local scrollBarTolerance = 10

local initialised = false
local stream = false
local isWrap = false
local firstLine = 0
local heightInLines = 1
local lineHeight = 0
local scrollBarVisible = false
local scrollBarLength = 0
local scrollBarTop = 0
local showOutput = false
local logRect = {x=0, y=0, w=WIDTH, h=HEIGHT} -- Viewing rectangle.
local logAlpha = 127 -- Transparency of viewing rectangle
local output = {}

-- A private function to assist with setting the line height
local setLineHeight = function ()
   pushStyle()
   font(outputFont)
   fontSize(outputFontSize)
   dummy, lineHeight = textSize("text")
   popStyle()
   heightInLines = math.floor(logRect.h/lineHeight)-1 -- -1 because of title.
end

-- A private function to assist with setting the first line on screen
local resetFirstLine = function ()
   if #output > heightInLines then
       firstLine = #output - heightInLines
       scrollBarLength =  (logRect.h - lineHeight) *
           heightInLines/#output
       scrollBarTop = logRect.y + logRect.h - lineHeight - (logRect.h - lineHeight) * firstLine/#output
   end
end

-- A private function to clean up a string 's'
local clean = function (s)
   -- Remove all control characters other than carriage returns (\\r),
   -- new lines (\
) and tabs (\\t)
   s = string.gsub(s, "[^%C\\r\
\\t]", "")
   -- Replace \\r\
 by \
, and eliminate any leading spaces
   s = string.gsub(s, "[% \\t]*\\r\
", "\
")
   -- Replace \
\\r by \
, and eliminate any leading spaces
   s = string.gsub(s, "[% \\t]*\
\\r", "\
")
   -- Replace remaining \\r by \

   s = string.gsub(s, "\\r", "\
")
   -- Eliminate any leading spaces before \

   s = string.gsub(s, "[% \\t]*\
", "\
")
   return s
end

-- A private function to check if a string 's' will fit if rendered to
-- the screen
local tooWide = function (s)
   pushStyle()
   font(outputFont)
   fontSize(outputFontSize)
   local tooWide = textSize(s) > logRect.w
   popStyle()
   return tooWide
end

-- A private function to write out a string 'l' with wrapping. It
-- returns any tail that does not fit.
local write = function (l)
   local p = #l
   while (tooWide(string.sub(l, 1, p))) do
       p = p - 1
   end
   output[#output + 1] = string.sub(l, 1, p)
   return string.sub(l, p + 1)
end

-- A private function to process a string 's' that may contain new
-- line (\
) or tab (\\t) characters
local process = function (s)
   -- Separate s into lines 'l' ending in \

   for l in string.gmatch(s, "([^\
]*)\
") do
       local b = ""
       local p1 = 1
       while p1 <= #l do
           if string.sub(l, p1, p1)=="\\t" then
               local temp = b..string.rep(" ", 8 - #b % 8)
               if tooWide(temp) then
                   write(b)
               else
                   b = temp
               end
               p1 = p1 + 1
           else
               local p2
               p1, p2 = string.find(l, "[^\\t]+", p1)
               b = b..string.sub(l, p1, p2)
               while tooWide(b) do
                   b = write(b)
               end
               p1 = p2  + 1
           end
       end
       write(b)
   end
end

local init = function ()
   local dummy
   initialised = true
   setLineHeight()

   local oldprint = print
   print = function (...)
       if not stream then oldprint(unpack(arg)) return end
       local l = ""
       for i, v in ipairs(arg) do
           if i > 1 then
               l = l.."\\t"
           end
           l = l .. tostring(v)
       end
       l = clean(l).."\
"
       process(l)
       resetFirstLine()
       showOutput = true
   end

Part two:


   local olddraw = draw
   draw = function ()
       olddraw()
       if not showOutput then return end
       pushStyle()
       rectMode(CORNER)
       ellipseMode(CENTER)
       noStroke()
       -- Draw background
       fill(0, logAlpha)
       rect(logRect.x, logRect.y, logRect.w, logRect.h)
       -- Draw title
       rect(logRect.x, logRect.y + logRect.h - lineHeight, logRect.w, lineHeight)
       font(outputFont)
       fontSize(outputFontSize)
       textMode(CORNER)
       textAlign(LEFT)
       textWrapWidth(0)
       fill(255)
       text("Logging", logRect.x, logRect.y + logRect.h - lineHeight)

       if #output > heightInLines then
           fill(127, 127)
           rect(logRect.x + logRect.w - scrollBarTolerance, logRect.y,
               scrollBarTolerance, logRect.h - lineHeight)
       end

       local h = logRect.y + logRect.h - lineHeight
       local fli = firstLine + 1
       local lli = math.min(#output, firstLine + heightInLines)
       for i = fli, lli  do
           local l = output[i]
           h = h - lineHeight
           fill(0, 127)
           text(l, logRect.x + 1, h - 1)
           fill(255)
           text(l, logRect.x, h)
       end
       if scrollBarVisible then
           local l = scrollBarLength
           fill(127, 255)
           local t = scrollBarTop + logRect.y
           rect(logRect.x + logRect.w - 9, t - l, 8, l)
           ellipse(logRect.x + logRect.w - 5, t, 8, 8)
           ellipse(logRect.x + logRect.w - 5, t - l, 8, 8)
       end
       -- Draw an OSX-like red close button
       local ex = logRect.x + logRect.w
       local ey = logRect.y + logRect.h
       fill(0)
       ellipse(ex - 8, ey - 8, 16, 16)
       fill(255, 0, 0)
       ellipse(ex - 8, ey - 8, 14, 14)
       fill(255)
       ellipse(ex - 8, ey - 4, 7, 4)
       fill(255, 127)
       ellipse(ex - 8, ey - 12, 9, 6)
       popStyle()
   end

   local oldorientationchanged = orientationChanged
   orientationChanged = function(orientation)
       if oldorientationchanged then
           oldorientationchanged(orientation)
       end
       heightInLines = math.floor(logRect.h/lineHeight)-1
       resetFirstLine()
   end

   local oldtouched = touched
   touched = function (touch)
       local state = touch.state
       if #output > heightInLines then
           if state == ENDED and scrollBarVisible then
               scrollBarVisible = false
               return
           end
           if state == BEGAN then
               local sbxCenter = (logRect.x + logRect.w - scrollBarTolerance / 2)
            if touch.x < sbxCenter + 5 and touch.x > sbxCenter - 5 and
               touch.y < (logRect.y + logRect.h - lineHeight) and
               touch.y > logRect.y then
                   scrollBarLength =  (logRect.h - 26) *
                       heightInLines/#output
                   scrollBarTop = logRect.h - 21 - (logRect.h - 26) *
                       firstLine/#output
                   scrollBarVisible = true
                   return
               end
           end
           if state == MOVING and scrollBarVisible then
               local t = scrollBarTop + touch.deltaY
               t = math.min(t, logRect.h - 21)
               t = math.max(t, scrollBarLength + 5)
               scrollBarTop = t
               firstLine = #output - math.floor(#output * (t - 5)
                   /(logRect.h - 26))
               return
           end
       end
       -- This also has == ENDED because if there is a dbg.print in the main touch, then
       -- this stops the reopen of the dialog because of the return:
       if (state == BEGAN or state == ENDED) then
           -- Touch area is larger than the small button to touch it easier (24x24):
           local ex = logRect.x + logRect.w - 8
           local ey = logRect.y + logRect.h - 8
           if math.abs(ex - touch.x) <= 12 and math.abs(ey - touch.y) <= 12 then
               showOutput = false
               return
           end
       end
       if oldtouched then oldtouched(touch) end
   end
end

local on = function (w)
   if not initialised then init() end
   stream = true
   isWrap = w or false
end

local off = function ()
   if not initialised then init() end
   stream = false
end

local font = function (fName, fSize)
   if not fName then return outputFont, outputFontSize end
   if not fSize then fSize = 17 end
   if not initialised then
       outputFont = fName
       outputFontSize = fSize
       init()
   end
end

local setLogRect = function(x, y, w, h)
   logRect.x = x
   logRect.y = y
   logRect.w = w
   logRect.h = h
end

local setLogAlpha = function(a)
   logAlpha = a
end

redirect = {
   on = on,
   off = off,
   font = font,
   setLogRect = setLogRect,
   setLogAlpha = setLogAlpha
}

Sorry, had a bug in the changes. Just fixed the two comments above ( last line was not printed).

Hello @KilamMalik. I also hit the limit for comments - so my commentary became shorter as the code grew longer. I am going to try to fix that by decamping to a new discussion here. What I have in mind will take more than one step, so please bear with me; I’ve not yet incorporated your most recent suggestions.