luati-nspire

TI Nspire lua crashes and throws 'break' error


I was working on a script to make little windows that I could open and close on my TI Nspire CX CAS 2 calculator. Adding new windows worked just fine, but trying to remove them, would cause a my software to crash when the function that was supposed to remove the window finished. I also tried it on my calculator and it still got stuck but I was able to close the script and then reopen it. When I reopened it there was a 'break' error on line 465 a picture of my calculator with the error

screen = platform.window
screenx,screeny = screen.width(),screen.height()

-- system stuff

local password = nil
local passVerify = nil
local drawObjects = {}
local processes = {}
local rectangle = class()
local backgroundObj = class()
local taskbar = class()
local window = class()
local process = class()
local taskbarHeight = 20
local hasBeenSetup = false
local customSetupProcedures = {}
local isLogedIn = false
local draggingMode = {
move = 0,
left = 1,
top = 2,
right = 3,
bottom = 4
}
local readyToDraw = true
local currentDraggingMode = draggingMode.both
local selectedWindowForDragging = nil
local textures = {}
local processStatus = {
waiting = 0,
running = 1,
halted = 2,
dead = 3
}
local specialColors = {
taskbarColor = 0x00969E,
windowBackgroundColor = 0xF7F7F7,
windowTopBarColor = 0xA2A2A2,
windowCloseButtonColor = 0xFF5100,
windowMaximizeButtonColor = 0x00FF00,
windowMinimizeButtonColor = 0xFFFF00
}

function process:init(run,onKill)
    self.status = processStatus.waiting
    self.co = coroutine.create(run)
    self.onKill = onKill
    coroutine.resume(self.co,self)
end

function process:update()
    if self.status == processStatus.running then
        coroutine.resume(self.co)
    end
end

function process:halt()
    self.status = processStatus.halted
end

function process:resume()
    self.status = processStatus.waiting
end

function process:kill()
    readyToDraw = false
    self.status = processStatus.dead
    self.co = nil
    self.onKill()
    readyToDraw = true
end

function rectangle:init(x,y,width,height)
    self.x = x
    self.y = y
    self.width = width
    self.height = height
end

function rectangle:contains(x,y)
    return self.x<x and x<self.x+self.width and self.y<y and y<self.y+self.height
end

function taskbar:init()

end

function taskbar:draw(gc)
    gc:setColorRGB(specialColors.taskbarColor)
    gc:fillRect(0,screeny-taskbarHeight,screenx,screeny)
    --gc:drawImage(textures[1],0,screeny-20)
end

function taskbar:isWindow()
    return false
end

function window:init(x,y,width,height)
    self.x = x
    self.y = y
    self.draggedOnX = 0
    self.draggedOnY = 0
    self.width = width
    self.height = height
    self.icon = nil
    self.components = {}
    self.focusLevel = 0
    newWindowFocused(#drawObjects+1)
    self.resizable = true
    self:subclassInit(x,y,width,height)
end

function window:subclassInit(x,y,width,height)

end

function window:getFocusLevel()
    return self.focusLevel
end

function window:draw(gc)
    gc:setColorRGB(specialColors.windowBackgroundColor)
    gc:fillRect(self.x,self.y,self.width,self.height)
    gc:setColorRGB(specialColors.windowTopBarColor)
    gc:fillRect(self.x,self.y,self.width,10)
    gc:setColorRGB(specialColors.windowMinimizeButtonColor)
    gc:fillRect(self.x+self.width-29,self.y+1,8,8)
    gc:setColorRGB(specialColors.windowMaximizeButtonColor)
    gc:fillRect(self.x+self.width-19,self.y+1,8,8)
    gc:setColorRGB(specialColors.windowCloseButtonColor)
    gc:fillRect(self.x+self.width-9,self.y+1,8,8)
    self:drawComponents(gc)
end

function window:drawComponents(gc)
    for i in ipairs(self.components) do
        self.components[i]:draw(gc,self)
    end
end

function window:addComponent(component)
    i = #self.components+1
    self.components[i] = component
    return i
end

function window:decreaseFocus()
    self.focusLevel = self.focusLevel+1
end

function window:isWindow()
    return true
end

function window:contains(x,y)
    return self.x<x and self.y < y and self.width+self.x>x and self.height+self.y>y
end

function window:checkForMouse(x,y)
    if y-self.y <= 3 and self.resizable then
        cursor.set('resize row')
    elseif y-self.y>=self.height-3 and self.resizable then
        cursor.set('resize row')
    elseif x-self.x<=3 and self.resizable then
        cursor.set('resize column')
    elseif x-self.x>=self.width-3 and self.resizable then
        cursor.set('resize column')
    end
end

function window:click(x,y,n)
    self:focus(n)
    if y-self.y <= 3 and self.resizable then
       selectedWindowForDragging = self
       currentDraggingMode = draggingMode.top
    elseif y-self.y>=self.height-3 and self.resizable then
       selectedWindowForDragging = self
       currentDraggingMode = draggingMode.bottom
    elseif x-self.x<=3 and self.resizable then
       selectedWindowForDragging = self
       currentDraggingMode = draggingMode.left
    elseif x-self.x>=self.width-3 and self.resizable then
        selectedWindowForDragging = self
        currentDraggingMode = draggingMode.right
    elseif y-self.y <= 10 then
        if x>=self.x+self.width-10 then
            self:close()
        elseif x>=self.x+self.width-20 then
            self:maximize()
        elseif x>=self.x+self.width-30 then
            self:minimize()
        else
            selectedWindowForDragging = self
            currentDraggingMode = draggingMode.move
            self.draggedOnX = x - self.x
            self.draggedOnY = y - self.y
        end
    end
    self:clickComponents(x,y)
end

function window:clickComponents(x,y)
    
end

function newWindowFocused(n)
    for window in ipairs(drawObjects) do
        window = drawObjects[window]
        if window:isWindow() then
            if window:getFocusLevel() < n then
                window:decreaseFocus()
            end
        end
    end
end

function window:focus(n)
    self.focusLevel = -1
    newWindowFocused(n)
end

function window:minimize()
    
end

function window:maximize()

end

function window:close()
    processes[1]:kill()
end

function on.resize()
    screenx = screen:width()
    screeny = screen:height()
end

function on.mouseDown(x,y)
    minFocus = 0
    for i = 0,#drawObjects,1 do
        for windown in ipairs(drawObjects) do
            window=drawObjects[windown]
            if window:isWindow() then
                if window:getFocusLevel() == minFocus then
                    if window:contains(x,y) then
                        window:click(x,y,minFocus)
                        return
                    end
                    minFocus = minFocus+1
                end
            end
        end
    end
end

function on.mouseMove(x,y)
    cursor.set('default')
    if selectedWindowForDragging ~= nil then
        if currentDraggingMode == draggingMode.move then
            selectedWindowForDragging.x = x - selectedWindowForDragging.draggedOnX
            selectedWindowForDragging.y = y - selectedWindowForDragging.draggedOnY
            if selectedWindowForDragging.x+selectedWindowForDragging.draggedOnX<5 then selectedWindowForDragging.x = 5-selectedWindowForDragging.draggedOnX end
            if selectedWindowForDragging.x+selectedWindowForDragging.draggedOnX>screenx-5 then selectedWindowForDragging.x = screenx-selectedWindowForDragging.draggedOnX-5 end
            if selectedWindowForDragging.y<0 then selectedWindowForDragging.y=0 end
            if selectedWindowForDragging.y>screeny-taskbarHeight-5 then selectedWindowForDragging.y = screeny-taskbarHeight-5 end
            elseif currentDraggingMode == draggingMode.top then
                i = selectedWindowForDragging.draggedOnY + y
                selectedWindowForDragging.height = math.max(selectedWindowForDragging.height - selectedWindowForDragging.draggedOnY - y + selectedWindowForDragging.y,20)
                selectedWindowForDragging.y = i
            elseif currentDraggingMode == draggingMode.bottom then
                selectedWindowForDragging.height = math.max(y - selectedWindowForDragging.y,20)
            elseif currentDraggingMode == draggingMode.left then
                i = selectedWindowForDragging.draggedOnX + x
                selectedWindowForDragging.width = math.max(selectedWindowForDragging.width - selectedWindowForDragging.draggedOnX - x + selectedWindowForDragging.x,20)
                selectedWindowForDragging.x = i
            elseif currentDraggingMode == draggingMode.right then
                selectedWindowForDragging.width = math.max(x - selectedWindowForDragging.x,20)
        end
    end
    
        minFocus = 0
        for i = 0,#drawObjects,1 do
            for windown in ipairs(drawObjects) do
                window=drawObjects[windown]
                if window:isWindow() then
                    if window:getFocusLevel() == minFocus then
                        if window:contains(x,y) then
                            window:checkForMouse(x,y)
                            return
                        end
                        minFocus = minFocus+1
                    end
                end
            end
        end
end

function on.mouseUp(x,y)
    selectedWindowForDragging = nil
end

local calculator = class(window)

function calculator:subclassInit()
    self.width = 60
    self.height = 90
    self.resizable = false
    self.number = ''
    print(self.number)
    self.button1 = rectangle(5,22,13,10)
    self.button2 = rectangle(24,22,14,10)
    self.button3 = rectangle(44,22,13,10)
    self.button4 = rectangle(5,34,13,10)
    self.button5 = rectangle(24,34,14,10)
    self.button6 = rectangle(44,34,13,10)
    self.button7 = rectangle(5,45,13,10)
    self.button8 = rectangle(24,45,14,10)
    self.button9 = rectangle(44,45,13,10)
    self.button0 = rectangle(24,57,14,10)
    self.buttonC = rectangle(5,57,13,10)
    self.buttonDot = rectangle(44,57,13,10)
    self.buttonPlus = rectangle(6,70,7,7)
    self.buttonMinus = rectangle(15,70,7,7)
    self.buttonMultiply = rectangle(40,70,7,7)
    self.buttonDivide = rectangle(49,70,7,7)
    self.buttonEquals = rectangle(24,69,14,8)
end

function calculator:drawComponents(gc)
    --gc:drawImage(textures[2],self.x,self.y+10)
    gc:setColorRGB(0x000000)
    gc:setFont('sansserif','r',6)
    if #self.number < 10 then
        gc:drawString(string.sub(self.number,0,#self.number),self.x+5,self.y+13)
    else
        gc:drawString(string.sub(self.number,#self.number-9,#self.number),self.x+5,self.y+13)
        print(self.number)
    end
end

function calculator:clickComponents(x,y)
    x = x-self.x
    y = y-self.y-10
    if self.button1:contains(x,y) then self.number = self.number..'1' elseif
    self.button2:contains(x,y) then self.number = self.number..'2' elseif
    self.button3:contains(x,y) then self.number = self.number..'3' elseif
    self.button4:contains(x,y) then self.number = self.number..'4' elseif
    self.button5:contains(x,y) then self.number = self.number..'5' elseif
    self.button6:contains(x,y) then self.number = self.number..'6' elseif
    self.button7:contains(x,y) then self.number = self.number..'7' elseif
    self.button8:contains(x,y) then self.number = self.number..'8' elseif
    self.button9:contains(x,y) then self.number = self.number..'9' elseif
    self.button0:contains(x,y) then self.number = self.number..'0' elseif
    self.buttonPlus:contains(x,y) then self.number = self.number..'+' elseif
    self.buttonMinus:contains(x,y) then self.number = self.number..'-' elseif
    self.buttonMultiply:contains(x,y) then self.number = self.number..'*' elseif
    self.buttonDivide:contains(x,y) then self.number = self.number..'/' elseif
    self.buttonDot:contains(x,y) then self.number = self.number..'.' elseif
    self.buttonEquals:contains(x,y) then
        if not pcall(function()self.number = ''..math.eval(self.number)end) then self.number = 'error' end
    elseif
    self.buttonC:contains(x,y) then self.number = '' end
end


function backgroundObj:draw(gc)
    gc:setColorRGB(0xC4C4C4)
    gc:fillRect(0,0,screenx,screeny-taskbarHeight)
    --gc:drawImage(textures[0],screenx/2-50,(screeny-taskbarHeight)/2-50)
end

function backgroundObj:isWindow()
    return false
end

function loadTextures()
    --textures[0]=image.new(_R.IMG.logo)
    --textures[1]=image.new(_R.IMG.startMenuButton)
    --textures[2]=image.new(_R.IMG.calculator)
end

function on.construction()
    timer.start(1/30)
    checkForSetup()
    checkForLogin()
end

function on.timer()
    screen:invalidate()
end

function encrypt(str,key)
    result = ''
    for i = 1,#str,1 do
        result = result..string.char(math.abs(string.byte(str:sub(i,i))+key+i)%422)
    end
    return result
end
function decrypt(str,key)
    result = ''
    for i = 1,#str,1 do
       result = result..string.char(math.abs((string.byte(str:sub(i,i))-key-i)%422))
    end
    return result
end

function checkForLogin()
    
end

function checkForSetup()
    if not hasBeenSetup then
        print('loading textures...')
        loadTextures()
        print('setting up graphics...')
        setupGraphics()
        print('executing default setup procedure...')
        print('setting up password...')
        setupPassword()
        print('executing custom setup procedures...')
        for funct in ipairs(customSetupProcedures) do
            funct()
        end
    end
    hasBeenSetup = true
end

function setupGraphics()
    processes[1] = process(
    function(proc)
        drawObjects[1] = backgroundObj()
    end,
    function()
       drawObjects[1] = nil
    end)
    drawObjects[2] = taskbar()
end

function setupPassword()
    w1 = calculator(50,100,100,100)
    drawObjects[3] = w1
    w2 = window(0,50,100,100)
    drawObjects[4] = w2
end

--system stuff

--graphics stuff

function on.paint(gc)
    if readyToDraw then
    if drawObjects[1]~=nil then
    drawObjects[1]:draw(gc)
    end
    focus = 0
    for i in ipairs(drawObjects) do
        window = drawObjects[i]
        if window:isWindow() then
            focus = math.max(focus,window:getFocusLevel())
        end
    end
    while focus>=0 do
        for i in ipairs(drawObjects) do
            window = drawObjects[i]
            if window:isWindow() and window:getFocusLevel()==focus then
                window:draw(gc)
                focus = focus-1
                break
            end
        end
    end
    drawObjects[2]:draw(gc)
    end
end

--graphics stuff

--default programs

--default programs


Edit: I just found out that if you are running a script in the student software and it gets stuck in a loop, pressing F12 would break the loop (just like the home button on the actual calculator).


Solution

  • Your problem is you have a chance to enter an infinite loop in the on.paint function.

    You do a check here:

        if drawObjects[1]~=nil then
            drawObjects[1]:draw(gc)
        end
    

    if this check is false it implies that the following for loop will run 0 times, as ipairs wants to see index 1 first or bails out:

        focus = 0
        for i in ipairs(drawObjects) do -- Will run 0 times.
            window = drawObjects[i]
            if window:isWindow() then
                focus = math.max(focus,window:getFocusLevel())
            end
        end
    

    this mean your while loop condition of focus >= 0 will run indefinitely as the same issue with ipairs(drawObjects) will happen in the loop body and focus will never change from 0

        while focus>=0 do
            for i in ipairs(drawObjects) do -- Will run 0 times.
                window = drawObjects[i]
                if window:isWindow() and window:getFocusLevel()==focus then
                    window:draw(gc)
                    focus = focus-1
                    break
                end
            end
        end
    

    you can fix this by defaulting focus to -1 additionally you would need to check if drawObjects[2] is nil before using it.


    or, what might be a better option, changing your check to

        if drawObjects[1] == nil then
           return -- bail out of function we dont have anything to draw
        end
        drawObjects[1]:draw(gc)