This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
surferpup's profile picture

The Complete Monitor Buttons and Control Tutorial Part III

Started by surferpup, 16 February 2014 - 06:29 PM
surferpup #1
Posted 16 February 2014 - 07:29 PM
The Complete Monitor Buttons and Control Tutorial


Part III – Monitor Touch and Other Events


This is the third part of a four-part tutorial series on touch screen monitor buttons and controls. The second part covered creating buttons and should have been completed before beginning this part. We will be using the button clousre object we created int Part II as the foundation for our work here in Part III.

Credits
  • Tutorial and Code Samples: [member="surferpup"]
  • Editing and Proof Reading: [member="awsmazinggenius"] and [member="ingie"]

Part II covered a tremendous amount of ground and dealt with programming an interface (a clousre object) to create our buttons. We are going to extend that interface a bit in this Part and learn how to use our buttons to create a user interface that can do things for us depending on what button the user presses. We will accomplish the following proramming tasks in this part of the tutorial:

  • Create a Button Toggle function to chane the button from a pressed to an unpressed state and vice-versa.
  • Set up our test environment.
  • Capture user mouse_click or monitor_touch events
  • Create a funtion to see if the mouse_click or monitor_touch event is directed at our button.
  • Create a function to do something if a button is pressed
  • Capture user key presses from the computer
  • Create a quit button, and force a key press event to quit


World Setup

The world setup for this part is slighly different than ussed in Part II. At this point in the turtorial, we are going to use an Advanced Computer, an Advanced Monitor, and three resdtone lamps (or three pistons if you haven't obtained the materials for redstone lamps yet). Your world setup should look like one of the following screenshots.

World Setup Screenshot

Setup with Redstone Lamps



Setup with Pistons instead of Redstone Lamps



Beginning Code

Our code will startout with the same code for the button closure object from the preivious part. You can download the code with this pastebin code. A listing of our starting code follows.

Starting Code


local function Button(
                                width,
                                height,
                                label,
                                backgroundColorNormal,                                
                                backgroundColorPressed,
                                textColorNormal,
                                textColorPressed,
                                hasBorder,
                                borderColorNormal,
                                borderColorPressed,
                                startColumn,
                                startRow,
                                isPressed,
                                defaultBackgroundColor,
                                defaultTextColor
                            )
    local button = {}
    button.height=height or 1
    button.width=width or 1
    button.label=label or ""
    button.backgroundColorNormal=backgroundColorNormal or colors.black
    button.backgroundColorPressed=backgroundColorPressed or colors.white
    button.textColorNormal=textColorNormal or colors.white
    button.textColorPressed=textColorPressed or colors.black
    button.hasBorder = hasBorder or false
    button.borderColorNormal = borderColorNormal or backGroundColorNormal
    button.borderColorPressed = borderColorPressed or backGroundColorPressed
    button.defaultBackgroundColor = defaultBackgroundColor or colors.black
    button.defaultTextColor = defaultTextColor or colors.white
    button.startColumn = startColumn or 1
    button.startRow = startRow or 1
    button.isPressed=isPressed or false

    function button.draw(display,isPressed,startColumn,startRow)

        button.startColumn = startColumn or button.startColumn
        button.startRow = startRow or button.startRow
        display = display or term
        if isPressed == false or isPressed then
            button.isPressed = isPressed
        else isPressed = button.isPressed
        end
        local width = button.width
        local height = button.height
        startRow = button.startRow
        startColumn = button.startColumn

        local label = button.label
        local labelPad = 2

        -- set border params and draw border if hasBorder
        if button.hasBorder == true then
            -- button must be at least 3x3, if not, make it so
            if width < 3 then
                width = 3
            end
            if height < 3 then
                height = 3
            end

            -- set border colors
            if not isPressed then
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorNormal)
                end
            else
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorPressed)
                end
            end

            -- draw button border (inset)
            display.setCursorPos(startColumn,startRow)
            display.write(string.rep(" ",width))
            for row = 2,height-1 do
                display.setCursorPos(startColumn,button.startRow+row -1)
                display.write(" ")
                display.setCursorPos(startColumn+width -1 ,startRow + row-1)
                display.write(" ")
            end
            display.setCursorPos(startColumn,startRow+height-1)
            display.write(string.rep(" ",width))

            -- reset startColumn,startRow,width,column to inset button and label
            startColumn=startColumn+1
            startRow = startRow +1
            width = width - 2
            height = height - 2
        end

        --set button background and text colors
        if not isPressed then
            if not display.isColor() then
                display.setBackgroundColor(colors.black)
                display.setTextColor(colors.white)
            else
                display.setBackgroundColor(button.backgroundColorNormal)
                display.setTextColor(button.textColorNormal)
            end
        else
            if not display.isColor() then
                display.setBackgroundColor(colors.white)
                display.setTextColor(colors.black)
            else
                display.setBackgroundColor(button.backgroundColorPressed)
                display.setTextColor(button.textColorPressed)
            end
        end

        -- draw button background (will be inside border if there is one)
        for row = 1,height do
            --print(tostring(startColumn)..","..tostring(startRow-row))
            display.setCursorPos(startColumn,startRow + row -1)
            display.write(string.rep(" ",width))
        end

        -- prepare label, truncate label if necessary

        -- prepare label, truncate label if necessary
        if width < 3 then
            labelPad = 0
        end
        if #label > width - labelPad then
            label = label:sub(1,width - labelPad)
        end

        -- draw label
        display.setCursorPos(startColumn + math.floor((width - #label)/2),startRow + math.floor((height-1)/2))
        display.write(label)
        display.setBackgroundColor(button.defaultBackgroundColor)
        display.setTextColor(button.defaultTextColor)
    end

    return button
end


Here is the pastebin code.

Adding a Button Toggle

Before we really get started, we need a function to toggle the state of our button. Users expect that when a button is pressed, its state will change and something will happen. Let's handle the state change now.

A toggle is a simple programming concept. Essentially, when called, a toggle will change a variable's state from whatever state it was in to the opposite state: what was on becomes off, what was true becomes false. Because our button state is stored as a boolean in the button variable button.isPressed, we can easily design the code to toggle the button. What we need is a function whose execution does this:


button.isPressed = not button.isPressed

To do that, we are going to add a few lines to our Button function. At then end of the button function, immediately before the return, we will add:


funtion button.toggle()
  button.isPressed = not button.isPressed
  return button.isPressed
end

That's it. Now. whenever we call button.toggle(), the button.isPressed value will change state and the function will return the new value for button.isPressed.

Setting up Our Test Code

At present, all we have is a function called Button() in our code. We need to have a test code that we will use to continue this tutorial. For our purpose, we are going to create three buttons on our monitor. Two of the buttons will be 7 wide by 3 tall, and the third button will be 10 wide and 3 tall. We will label the buttons "<–" (for left),"–>" (for right) and "Bottom" for bottom. We will be using the monitor on top to display the buttons. Our color scheme for normal state will be lime background with white text, and for pressed state red background with yellow text. We will use border colors of colors.green for normal and colors.pink for pressed. Our buttons will all start in their normal (not pressed) states. You can use whatever color scheme you like, however all of our screenshots will be using the color code we have chosen here.

After the code for the Button() function, add the following code:


monitor1 = peripheral.wrap("top")
monitor1.setTextScale(0.5)
buttons = {}

buttons.left= Button(7,3,"<--",colors.lime,colors.pink,colors.white,colors.yellow,true,colors.green,colors.red,1,1,false,nil,nil)

buttons.right = Button(7,3,"-->",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,9,1,false,nil,nil)

buttons.bottom = Button(10,3,"Bottom",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,3,5,false,nil,nil)

We used nil for both defaultBackroundColor and defaultTextColor because nil will cause our function to use the built-in defaults of black and white. We could have left these arguments off altogether because they are at the end of our function call and we intend them to be nil.

Displaying Our Buttons

We chose to put our buttons in a table, and because of this we can display them easily with a for loop. To diplay our buttons, we iterate through the buttons table and call our button.draw() function for each button in the buttons table, passing in the handle for the monitor we want them to display on. Add this code to the program and execute your program.


monitor1.clear()
for i,button in ipairs(buttons) do
  button.draw(monitor1)
end


If you did everything the same as we have done here, your monitor should look the same as our screenshot.

Button Screenshot


If you are having problems with your code, make sure it is identical to our code so far.

Current Code

local function Button(
                                width,
                                height,
                                label,
                                backgroundColorNormal,                                
                                backgroundColorPressed,
                                textColorNormal,
                                textColorPressed,
                                hasBorder,
                                borderColorNormal,
                                borderColorPressed,
                                startColumn,
                                startRow,
                                isPressed,
                                defaultBackgroundColor,
                                defaultTextColor
                            )
    local button = {}
    button.height=height or 1
    button.width=width or 1
    button.label=label or ""
    button.backgroundColorNormal=backgroundColorNormal or colors.black
    button.backgroundColorPressed=backgroundColorPressed or colors.white
    button.textColorNormal=textColorNormal or colors.white
    button.textColorPressed=textColorPressed or colors.black
    button.hasBorder = hasBorder or false
    button.borderColorNormal = borderColorNormal or backGroundColorNormal
    button.borderColorPressed = borderColorPressed or backGroundColorPressed
    button.defaultBackgroundColor = defaultBackgroundColor or colors.black
    button.defaultTextColor = defaultTextColor or colors.white
    button.startColumn = startColumn or 1
    button.startRow = startRow or 1
    button.isPressed=isPressed or false

    function button.draw(display,isPressed,startColumn,startRow)

        button.startColumn = startColumn or button.startColumn
        button.startRow = startRow or button.startRow
        display = display or term
        if isPressed == false or isPressed then
            button.isPressed = isPressed
        else isPressed = button.isPressed
        end
        local width = button.width
        local height = button.height
        startRow = button.startRow
        startColumn = button.startColumn

        local label = button.label
        local labelPad = 2

        -- set border params and draw border if hasBorder
        if button.hasBorder == true then
            -- button must be at least 3x3, if not, make it so
            if width < 3 then
                width = 3
            end
            if height < 3 then
                height = 3
            end

            -- set border colors
            if not isPressed then
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorNormal)
                end
            else
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorPressed)
                end
            end

            -- draw button border (inset)
            display.setCursorPos(startColumn,startRow)
            display.write(string.rep(" ",width))
            for row = 2,height-1 do
                display.setCursorPos(startColumn,button.startRow+row -1)
                display.write(" ")
                display.setCursorPos(startColumn+width -1 ,startRow + row-1)
                display.write(" ")
            end
            display.setCursorPos(startColumn,startRow+height-1)
            display.write(string.rep(" ",width))

            -- reset startColumn,startRow,width,column to inset button and label
            startColumn=startColumn+1
            startRow = startRow +1
            width = width - 2
            height = height - 2
        end

        --set button background and text colors
        if not isPressed then
            if not display.isColor() then
                display.setBackgroundColor(colors.black)
                display.setTextColor(colors.white)
            else
                display.setBackgroundColor(button.backgroundColorNormal)
                display.setTextColor(button.textColorNormal)
            end
        else
            if not display.isColor() then
                display.setBackgroundColor(colors.white)
                display.setTextColor(colors.black)
            else
                display.setBackgroundColor(button.backgroundColorPressed)
                display.setTextColor(button.textColorPressed)
            end
        end

        -- draw button background (will be inside border if there is one)
        for row = 1,height do
            --print(tostring(startColumn)..","..tostring(startRow-row))
            display.setCursorPos(startColumn,startRow + row -1)
            display.write(string.rep(" ",width))
        end

        -- prepare label, truncate label if necessary

        -- prepare label, truncate label if necessary
        if width < 3 then
            labelPad = 0
        end
        if #label > width - labelPad then
            label = label:sub(1,width - labelPad)
        end

        -- draw label
        display.setCursorPos(startColumn + math.floor((width - #label)/2),startRow + math.floor((height-1)/2))
        display.write(label)
        display.setBackgroundColor(button.defaultBackgroundColor)
        display.setTextColor(button.defaultTextColor)
    end
    button.toggle = function ()
            button.isPressed = not button.isPressed
            return button.isPressed
        end             
    return button
end

--  Start of test Program

monitor1 = peripheral.wrap("top")
buttons = {}

buttons.left= Button(10,3,"Left",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,1,false,nil,nil)
buttons.right = Button(10,3,"Right",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,4,false,nil,nil)
buttons.bottom = Button(10,3,"Bottom",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,7,false,nil,nil)

-- Display buttons on monitor1
monitor1.clear()
for key,button in pairs(buttons) do
  button.draw(monitor1)
end


Here is the pastebin code.

Using an Event Loop to Get User Input

We are now displaying all of our buttons where we want to. Next, we need to get our users to do some work and start clicking. We are going to create an event loop to allow our click-happy users to click on our monitor. What this means is that we are going to create an infinite while loop and grab all "monitor_touch" events from os.pullEvent() inside of the loop. Each time the program encounters os.pullEvent, it will pause and wait for an event to occur. We are only interested in "monitor_touch" events, so we will tell our os.pullEvent() function to ignore all other events. Each time a "monitor_touch" event occurs, we will store all of the values that the os.pullEvent() returns into our event table and display them.


while true do -- infinite while loop
  event ={os.pullEvent("monitor_touch")}
  print ("Got a monitor_touch event, here are the values:")
  for index,returnValue in ipairs(event) do
    print(tostring(i)..": "..tostring(returnValue))
  end
  print()
end

Run the code and click in at least places on your monitor. Then, look at your computer terminal at what is displayed. When you are finished looking at it, go ahead and exit the program using Ctrl-T to terminate. If you typed in everything correctly, you should see something like what we saw on our computer terminal:



That's pretty neat. Everytime the loop encounters a monitor_touch event, we get a whole bunch of values back. In order, those values are:
  1. The event (in this case a "monitor_touch" event).
  2. For "monitor_touch" events, the second return value is side the monitor is attached to the computer (in this case "top", however, in the case of a monitor attached via wired modem, it would be the monitor handle ("monitor_0").
  3. For "monitor_touch" events, the third return value is always the column where the user clicked.
  4. For "monitor_touch" events, the fourth return value is always the row where the user clicked.

Notice that we keep emphasing "for 'monitor_touch' events…" That is because each event type has its own set of reutrn values. If you are interested in learning more about events, check out the sources in the spoiler.

More on EventsIf you are new to events, or you would like to learn more about events, here are two resources which will help you explore the topic more thoroughly.

So now we know where the user clicks on our monitor when the user chooses to click. In order for that information to become useful to us, we need to know whether a user clicked on a button or not, and if so, which button the user clicked on.

Checking if a Button was Clicked by a User

We already know just about everything there is to know about each one of our buttons, and the os.pullEvent("monitor_touch") function will tell us where the user clicked. We can use the information we have to see if that location was within the bounds of any of our buttons. In order for a button to have been clicked, the column AND row clicked must be within the bounds of the button. We know the starting column and row for our buttons, and we know the width and height of our buttons. Let's use that inforation to test whether the click was within the bounds of our button. We'll set up an if statement to check.



local columnClicked = event[3]
local rowClicked = event[4]
if columnClicked >= button.startingColumn and columnClicked < button.startingColum + width and rowClicked >= button.startingRow and rowClicked < button.startingRow + button.height then
    -- our button has been pressed
end

Because we have all of our buttons in a table, everytime we receive a "monitor_touch" event, we can iterate through our buttons and check if any of them have been clicked and do something if they were. We already created a button.toggle() function, so let's use it now. We'll modify our while loop to tell it to do something if a button was pressed.


while true do
  event ={os.pullEvent("monitor_touch")}
  print ("Got a monitor_touch event, here are the values:")
  for key,button in pairs(buttons)
    local columnClicked = event[3]
    local rowClicked = event[4]
    if columnClicked >= button.startColumn and columnClicked < button.startColumn + width and rowClicked >= button.startRow and rowClicked < button.startRow + button.height then
        print ("Button "..key.."was pressed.  It is now "..tostring(button.toggle())
        button.draw(monitor1)
        break -- we found on, so we don't need to keep looking
    end
  end
end

Exectue the code and play around with it for a bit. If everything was typed in correctly, when you click on a button, it will change its state, and it will note the event on the computer display. Clicking where there are no buttons will result in no activity on the display. Use Ctrl-T to terminate when you are finished.

Adding clicked() to Our Button() Function

Knowing if a user pressed a button is so fundamental that we should just add it to our Button() function and make it a part of every button. That's pretty simple to do. We can send in the column and row clicked to our button.clicked() function and get back a true or false depending on whether the click was within our button's bounds. We pretty much have it already. To our button function, after our button.toggle assignment, we will add:


function button.clicked(column,row)
    return (column >= button.startColumn and column < button.startColumn + button.width and row >= button.startRow and row < button.startRow + button.height)
end 

We can simplify our event loop significantly and make it much more readable.


while true do
  event ={os.pullEvent("monitor_touch")}
  print ("Got a monitor_touch event, here are the values:")
  for key,button in pairs(buttons)
    if button.clicked(event[3],event[4]) then -- column,row
        print ("Button "..key.."was pressed.  It is now "..tostring(button.toggle())
        button.draw(monitor1)
        break -- we found one, so we don't need to keep looking
    end
  end
end

You can test the code and see if it working for you. If not, look it over carefully and correct your error. Use Ctrl-T to terminate when you are finished.

Controlling Devices Based on Button States

The whole purpose for most Touch Screen Monitors is to control things based on user touch events. When we set up our test world, we placed three redstone lamps (or pistons) to the left, right and bottom sides of our advanced computer. Let's control those now. We are going to turn the redstone lamps on or off depending on whether their corresponding buttons are pressed or normal.

We already print out to our computer terminal when a button is pressed and what state the button is in. Instead of printing, let's control the redstone output of the corresponding side of our advanced computer. We used "left","right" and "bottom" for key values in our buttons table. That was not an accident. They happen to correspond to the "left", "right" and "bottom" sides of our advanced computer. We can use the values of the key as the string for the corresponding side. All we need to do is control the redstone output for the key side based on the value of the isPressed state of the button. We will use the redstone.setOutput() function to do so. You may recall that redstone.setOutput() takes two arguments: the first a string representing the side of the computer (which conveniently will be our value for our button key), and the second a boolean representing an "on" state (true) and an "off" state ("false"). If you are completely unfamiliar with the basics of the Redstone API, please review the tutorial Redstone Basics 1 -- getInput, getOutput and setOutput or the Redstone API in the ComputerCraft Wiki.

We know that when the button is in a pressed state, isPressed is true. That is perfect, because when our button is pressed, we want the corresponding redstone output to be true as well. Our button.toggle() function returns the value of isPressed after it changes the same. So, all we have to do is use the return values from our button.toggle function to set our redstone output.

Let's get rid of the print statement right before our for loop inside of our event loop. Replace the print statement inside of the "then" portion of our if statement with:


redstone.setOutput(key,button.toggle())

Give it a try. Your program should toggle the redstone lamp associated with a button each time you press the button, and your button colors should change each time you press a button. If not, check your code to the current state of the code thus far:

Current Code

local function Button(
                                width,
                                height,
                                label,
                                backgroundColorNormal,                                
                                backgroundColorPressed,
                                textColorNormal,
                                textColorPressed,
                                hasBorder,
                                borderColorNormal,
                                borderColorPressed,
                                startColumn,
                                startRow,
                                isPressed,
                                defaultBackgroundColor,
                                defaultTextColor
                            )
    local button = {}
    button.height=height or 1
    button.width=width or 1
    button.label=label or ""
    button.backgroundColorNormal=backgroundColorNormal or colors.black
    button.backgroundColorPressed=backgroundColorPressed or colors.white
    button.textColorNormal=textColorNormal or colors.white
    button.textColorPressed=textColorPressed or colors.black
    button.hasBorder = hasBorder or false
    button.borderColorNormal = borderColorNormal or backGroundColorNormal
    button.borderColorPressed = borderColorPressed or backGroundColorPressed
    button.defaultBackgroundColor = defaultBackgroundColor or colors.black
    button.defaultTextColor = defaultTextColor or colors.white
    button.startColumn = startColumn or 1
    button.startRow = startRow or 1
    button.isPressed=isPressed or false
    function button.press()
        button.isPressed = not button.isPressed
    end

    function button.draw(display,isPressed,startColumn,startRow)

        button.startColumn = startColumn or button.startColumn
        button.startRow = startRow or button.startRow
        display = display or term
        if isPressed == false or isPressed then
            button.isPressed = isPressed
        else isPressed = button.isPressed
        end
        local width = button.width
        local height = button.height
        startRow = button.startRow
        startColumn = button.startColumn

        local label = button.label
        local labelPad = 2

        -- set border params and draw border if hasBorder
        if button.hasBorder == true then
            -- button must be at least 3x3, if not, make it so
            if width < 3 then
                width = 3
            end
            if height < 3 then
                height = 3
            end

            -- set border colors
            if not isPressed then
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorNormal)
                end
            else
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorPressed)
                end
            end

            -- draw button border (inset)
            display.setCursorPos(startColumn,startRow)
            display.write(string.rep(" ",width))
            for row = 2,height-1 do
                display.setCursorPos(startColumn,button.startRow+row -1)
                display.write(" ")
                display.setCursorPos(startColumn+width -1 ,startRow + row-1)
                display.write(" ")
            end
            display.setCursorPos(startColumn,startRow+height-1)
            display.write(string.rep(" ",width))

            -- reset startColumn,startRow,width,column to inset button and label
            startColumn=startColumn+1
            startRow = startRow +1
            width = width - 2
            height = height - 2
        end

        --set button background and text colors
        if not isPressed then
            if not display.isColor() then
                display.setBackgroundColor(colors.black)
                display.setTextColor(colors.white)
            else
                display.setBackgroundColor(button.backgroundColorNormal)
                display.setTextColor(button.textColorNormal)
            end
        else
            if not display.isColor() then
                display.setBackgroundColor(colors.white)
                display.setTextColor(colors.black)
            else
                display.setBackgroundColor(button.backgroundColorPressed)
                display.setTextColor(button.textColorPressed)
            end
        end

        -- draw button background (will be inside border if there is one)
        for row = 1,height do
            --print(tostring(startColumn)..","..tostring(startRow-row))
            display.setCursorPos(startColumn,startRow + row -1)
            display.write(string.rep(" ",width))
        end

        -- prepare label, truncate label if necessary

        -- prepare label, truncate label if necessary
        if width < 3 then
            labelPad = 0
        end
        if #label > width - labelPad then
            label = label:sub(1,width - labelPad)
        end

        -- draw label
        display.setCursorPos(startColumn + math.floor((width - #label)/2),startRow + math.floor((height-1)/2))
        display.write(label)
        display.setBackgroundColor(button.defaultBackgroundColor)
        display.setTextColor(button.defaultTextColor)
    end
    button.toggle = function ()
            button.isPressed = not button.isPressed
            return button.isPressed
        end             
    return button
end

--  Start of test Program

monitor1 = peripheral.wrap("top")
buttons = {}
buttons.left= Button(10,3,"Left",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,1,false,nil,nil)
buttons.right = Button(10,3,"Right",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,4,false,nil,nil)
buttons.bottom = Button(10,3,"Bottom",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,7,false,nil,nil)

-- Display buttons on monitor1
monitor1.clear()
for key,button in pairs(buttons) do
  button.draw(monitor1)
end

-- this is our event loop
while true do 
  event ={os.pullEvent("monitor_touch")}
  for key,button in pairs(buttons)
    if button.clicked(event[3],event[4]) then -- column,row
        redstone.setOutput(key,button.toggle())
        button.draw(monitor1)
        break -- we found on, so we don't need to keep looking
    end
  end
end

Here is the pastebin code.


Corner Case – Initialize Buttons

Try this: Run your program and set at least one restone lamp on and one redstone lamp off (you may have already seen this problem). Now terminate your program and run it again. The redstone lamps which were on are still on. This is because we are not synchronizing the button states with the redstone output states, or the redstone output states with the button states at the beginning of the program. However, the button states will not be synchronized to the state of the redstone output. Synchronization is a common problem with any control program. As the programmer, you have to determine what you are going to synchronize with what so that when your program starts, it is in a state that you desire. Let's make it so that when our program starts, our button states will synchronize to whatever states the redstone outputs are when the program begins.

After we declare our buttons, let's change there states and redraw them depending on the redstone output. To get the redstone output state, we will use the redstone.getOutput() function.



for key,button in pairs(buttons) do
  button.isPressed = redstone.getOutput(key) -- synch button to current redstone output
  button.draw(monitor1)
end

And that's it. We iterate through our buttons table, obtaining each key and button. We use the key to get the redstone output, and we assign the output to our button.isPressed variable. Now whenever our program starts up, the buttons will be set to whatever the redstone output is.

Adding Events – Using a "key" Event

In our current state, we can respond to "monitor_touch" events only. In order to terminate the program, we have to open up the terminal window and hold down the Ctrl-T key for three seconds each time we want to stop. One of the nice things of having an event loop is that you can respond to more than one event. Let's add a feature so that in addition to responding to "monitor_touch" events, our event loop will also check "key" events and quit the program if the "key" event is the "q" key.

The os.pullEvent function is currently only responding to events of type "monitor_touch". If we do not give the os.pullEvent any function, it will respond to all events. The trick will be to sort out "monitor_touch" events from "key" events. As you may recall, the first return value returned by os.pullEvent is a string representing the event type. It should come as little surprise to you then that if the event is a pressing of a key on the keyboard, the return value of the first value returned by os.pullEvent will be the string "key".


For "monitor_touch" events, we already know what the rest of the values are. For "key" events, the second value returned by os.pullEvent is the numeric value of the key that was pressed. To make things easier, there is a keys enumerator so that the "1" key is keys.one, and the "q" key is keys.q (believe it or not, the actual numeric value for the "1" key is 2, whereas the numeric value for the "q" key is 16). You can see all of the enumerated keys in the wiki page for the Keys API. You can find the numerical values for keyboard keys on the Key (event) wiki page.

What we are going to do is change our event loop to listen for all events, but only respond to those events if they are "monitor_touch" events or if they are "key" events and the key is "q."


while true do
  event ={os.pullEvent()}
  if event[1] =="monitor_touch" then
    for key,button in pairs(buttons)
      if button.clicked(event[3],event[4] then -- column,row
          redstone.setOutput(key,button.toggle())
          button.draw(monitor1)
          break -- we found on, so we don't need to keep looking
      end
    end
  elseif event[1] =="key" and event[2]==keys.q then
    break
  end
end

After executing the program (and fixing any errors that might have crept in), you should be able to click on buttons and have them turn the Redstone Lamps on or off, and when you tire of doing so, simply open up the computer terminal window and press the letter q.


Adding a "Quit" Button

In most Touch Screen setups, we are going to want to have our monitor as the user input and display function (say at our door) and we will want our computer someplace else and secure so that it cannot be tampered with easily. Consequently, using the "q" key to quit (though useful at the computer terminal) is not very useful elsewhere. For purposes of our example, let's add a "Quit" button to our display which will terminate the program when the user presses the button.

Currently, we have created our buttons with the Button() function and stored them in our buttons table. We need to add a button to our program. We could add it to our buttons table, but let's keep that table the way it is. Instead, we will just create a new button and call it "quit".

Following our button declarations, add the following line of code which will create an 8 wide by 1 tall gray button with black text labeled "Quit."


local quit = Button(8,1,"Quit",colors.gray,colors.black,colors.black,colors.white,false,nil,nil,4,10,false,nil,nil)

We will place the button at the bottom of our monitor display. We'll need to add a line of code to draw the button on our monitor. We add it right after our loop where we initially draw our buttons.


quit.draw()

We already listen for a key event, where if the "q" key is pressed, we exit the program. Let's leverage that functionality. You may not know that in addition to listening for events, we can create events as well. If we create a key event and give it the proper values, the part of our code that listens for key events will pick up on the event and take action. To create an event, we will use the os.queueEvent() function, and we will pass it an event name of "key" and an additional argument of keys.q – exactly what our listener is looking for.

But there is a slight rub in all of this: we check for which buttons were clicked by iterating through the buttons table. If one of the buttons in the buttons table was clicked, we toggle the button and set a redstone output. We do not want to do that with our quit button. Afterall – we are not going to toggle it, nor are we going to be setting any redstone with it. What we could do is check if any of the buttons in our button table were clicked and then check if our quit button was clicked. To do that, all we need to do is add an additional test following our for loop.


if quit.clicked(event[3],event[4]) == true then
  os.queueEvent("key",keys.q)
end

Now, when the user presses the quit button, a key event will be generated, and on the next portion of our while loop, we will catch it and end the program.

(Hey ... isn't that inefficient?)You may have noticed that we actually are doing way more tests than we need to do in this code. Each time a user generates a "monitor_touch," we loop through button and check if it is clicked(). Only one button can be clicked at a time. And if the clicked button is found, there is no need to continue looping through once we have performed the tasks associated with the button. We break out of the loop at that time. However, because we are also checking whether the quit button was clicked immediately after the loop, that too is uncecessary if a button had been previously found to have been the one that was clicked. To remedy this, we could change the the event loop as follows:


while true do
  event ={os.pullEvent()}
  if event[1] =="monitor_touch" then
    local found = false 
    for key,button in pairs(buttons)
      if button.clicked(event[3],event[4] then -- column,row
          redstone.setOutput(key,button.toggle())
          button.draw(monitor1)
          found = true -- we found one
          break -- break out of the for loop
      end
    end
    if found == false and quit.clicked(event[3],event[4]) == true then
       os.queueEvent("key",keys.q)
    end
  elseif event[1] =="key" and event[2]==keys.q then
    break -- break out of the while loop
  end
end


This is all true, however, in our program, we are only using a few buttons, and we are only dealing with our slight inefficiency when a monitor_touch event occurs. And even though the technique of using a "found" variable to avoid executing unecessary code is valid in some circumstances, we think it over complicates the code in our case. So, we are going to leave our code as is. You are certainly free to make a different choice in your code.

Once we extend the quit functionality in our event loop, the final version of our code for this part of the turtorial should look as follows:

Part III Final Code

local function Button(
                                width,
                                height,
                                label,
                                backgroundColorNormal,                                
                                backgroundColorPressed,
                                textColorNormal,
                                textColorPressed,
                                hasBorder,
                                borderColorNormal,
                                borderColorPressed,
                                startColumn,
                                startRow,
                                isPressed,
                                defaultBackgroundColor,
                                defaultTextColor
                            )
    local button = {}
    button.height=height or 1
    button.width=width or 1
    button.label=label or ""
    button.backgroundColorNormal=backgroundColorNormal or colors.black
    button.backgroundColorPressed=backgroundColorPressed or colors.white
    button.textColorNormal=textColorNormal or colors.white
    button.textColorPressed=textColorPressed or colors.black
    button.hasBorder = hasBorder or false
    button.borderColorNormal = borderColorNormal or backGroundColorNormal
    button.borderColorPressed = borderColorPressed or backGroundColorPressed
    button.defaultBackgroundColor = defaultBackgroundColor or colors.black
    button.defaultTextColor = defaultTextColor or colors.white
    button.startColumn = startColumn or 1
    button.startRow = startRow or 1
    button.isPressed=isPressed or false
    function button.press()
        button.isPressed = not button.isPressed
    end

    function button.draw(display,isPressed,startColumn,startRow)

        button.startColumn = startColumn or button.startColumn
        button.startRow = startRow or button.startRow
        display = display or term
        if isPressed == false or isPressed then
            button.isPressed = isPressed
        else isPressed = button.isPressed
        end
        local width = button.width
        local height = button.height
        startRow = button.startRow
        startColumn = button.startColumn

        local label = button.label
        local labelPad = 2

        -- set border params and draw border if hasBorder
        if button.hasBorder == true then
            -- button must be at least 3x3, if not, make it so
            if width < 3 then
                width = 3
            end
            if height < 3 then
                height = 3
            end

            -- set border colors
            if not isPressed then
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorNormal)
                end
            else
                if not display.isColor() then
                    display.setBackgroundColor(colors.white)
                else
                    display.setBackgroundColor(button.borderColorPressed)
                end
            end

            -- draw button border (inset)
            display.setCursorPos(startColumn,startRow)
            display.write(string.rep(" ",width))
            for row = 2,height-1 do
                display.setCursorPos(startColumn,button.startRow+row -1)
                display.write(" ")
                display.setCursorPos(startColumn+width -1 ,startRow + row-1)
                display.write(" ")
            end
            display.setCursorPos(startColumn,startRow+height-1)
            display.write(string.rep(" ",width))

            -- reset startColumn,startRow,width,column to inset button and label
            startColumn=startColumn+1
            startRow = startRow +1
            width = width - 2
            height = height - 2
        end

        --set button background and text colors
        if not isPressed then
            if not display.isColor() then
                display.setBackgroundColor(colors.black)
                display.setTextColor(colors.white)
            else
                display.setBackgroundColor(button.backgroundColorNormal)
                display.setTextColor(button.textColorNormal)
            end
        else
            if not display.isColor() then
                display.setBackgroundColor(colors.white)
                display.setTextColor(colors.black)
            else
                display.setBackgroundColor(button.backgroundColorPressed)
                display.setTextColor(button.textColorPressed)
            end
        end

        -- draw button background (will be inside border if there is one)
        for row = 1,height do
            --print(tostring(startColumn)..","..tostring(startRow-row))
            display.setCursorPos(startColumn,startRow + row -1)
            display.write(string.rep(" ",width))
        end

        -- prepare label, truncate label if necessary

        -- prepare label, truncate label if necessary
        if width < 3 then
            labelPad = 0
        end
        if #label > width - labelPad then
            label = label:sub(1,width - labelPad)
        end

        -- draw label
        display.setCursorPos(startColumn + math.floor((width - #label)/2),startRow + math.floor((height-1)/2))
        display.write(label)
        display.setBackgroundColor(button.defaultBackgroundColor)
        display.setTextColor(button.defaultTextColor)
    end
    button.toggle = function ()
            button.isPressed = not button.isPressed
            return button.isPressed
        end             
    return button
end

--  Start of test Program

monitor1 = peripheral.wrap("top")

buttons = {}
buttons.left= Button(10,3,"Left",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,1,false,nil,nil)
buttons.right = Button(10,3,"Right",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,4,false,nil,nil)
buttons.bottom = Button(10,3,"Bottom",colors.lime,colors.red,colors.white,colors.yellow,true,colors.green,colors.pink,1,7,false,nil,nil)

local quit = Button(8,1,"Quit",colors.gray,colors.black,colors.black,colors.white,false,nil,nil,4,10,false,nil,nil)

-- Display buttons on monitor1
monitor1.clear()
for key,button in pairs(buttons) do
  button.draw(monitor1)
end
quit.draw()
-- this is our event loop
while true do
  event ={os.pullEvent()}
  if event[1] =="monitor_touch" then
    for key,button in pairs(buttons)
      if button.clicked(event[3],event[4] then -- column,row
          redstone.setOutput(key,button.toggle())
          button.draw(monitor1)
          break -- we found on, so we don't need to keep looking
      end
    end
    if quit.clicked(event[3],event[4]) == true then
     os.queueEvent("key",keys.q)
    end
  elseif event[1] =="key" and event[2]==keys.q then
    break
  end
end
end

Here is the pastebin code.

Now, whenever you press any of the "<–","–>" or "Bottom" buttons, the redstone lamps associated with the buttons will change states and the buttons themselves will change states. If you want to exit the program, you can simply click on the "Quit" button, or you can open up the computer terminal window and press the "q" key. Our code is now fully functional for the purposes of this part of the tutorial series. It can be easily modified to do other things as well, and it is up to you to think of the possibilities moving forward.

Wrapping Up Part III

We've already done quite a bit with buttons: You can create them in any size and color and with whatever label, and you add the code to do so easily to any program. We can toggle button states and catch monitor_touch events to both change their states and effect a redstone output. We can corntol simple redstone outputs with our buttons, and synchronie our buttons to those outputs. We can listen for key events on the computer terminal and respond as necessary. It has been a long road, but we have covered a tremendous amount of ComputerCraft and Lua ground. So if you quit here and went on your merry programming way, no one would fault you (we certainly wouldn't).

On the other hand, if you want to see some true flexibility – you need to move on to the last part of this tutorial series, Part IV – Advanced Buttons and Control Actions, where we will explore using multiple monitors with either the same or different buttons, we will create custom button actions and propertues and we will use the parallel API to control unique processes with our touch screen buttons. Part IV will be available for viewing seven days from the posting of this Part III.
Edited on 16 February 2014 - 10:03 PM
Molinko #2
Posted 17 February 2014 - 02:51 AM
A very thorough tutorial my lad! If I may make a small suggestion for the initializing of button colors..Just because it can be lengthy as well…
You could change it so when inputting the color instead of colors.red the use can just choose his/her color like so 'white' .
In your code it could be something like..

-- target is your wrapped monitor object or term pointer
target.setTextColor( colors[ button.textColorNormal ] )
Upon initialization you can easily check and throw and error like so,

button.textColorNormal = colors[ textColorNormal ] and textColorNormal or error( "Tried to set an invalid color: ' "..tostring(textColorNormal).." ' !", 1 )
Overall, I love the tut! keep em' comin'!
Edited on 17 February 2014 - 01:57 AM
surferpup #3
Posted 17 February 2014 - 12:19 PM
If I may make a small suggestion for the initializing of button colors..Just because it can be lengthy as well…
You could change it so when inputting the color instead of colors.red the use can just choose his/her color like so 'white' .

I think that is an excellent suggestion for programming, especially if one is to turn this into an API. However, I can only focus on so many things when writing a tutorial to members and guests each of whom starts with whatever level of expertise they bring to the tutorial. This would be a good subject for a colors tutorial – tips and tricks on handling colors in your programs.

Some people institute a color table or enumerator in their programs and use the string names of a color (i.e. "white") as key values. Others write out the full colors API call. Some would find your suggestion useful. I think what I will do is point out – through this reply – that you have an excellent idea, and I certainly encourage people to give your idea a try as they develop their skills in Lua. Thank you for the comment.
Splitfingers #4
Posted 24 March 2014 - 02:56 AM
Hello there, I downloaded your pastebin code and tried to run it EXACTLY like you have in your tutorial. But I'm getting an error message at line number 170 "bios:339: do expected. You are also missing a right bracket on the end of this line of code.
if button.clicked(event[3],event[4] <–should be a bracket there.

Thank you, this tutorial is really nice and I hope to be able to put it to great use.
RustyDagger #5
Posted 24 April 2014 - 09:39 AM
I have to agree with the post above it does indeed need the bracket but oddly enough it still insists that its expecting a 'do' on line 170.

In this case its reporting the error on the wrong line Line 169 is infact the line it needs a do added to the fixed section looks like this.


	for key,button in pairs(buttons) [b]do[/b]   -- do was missing from here.
	  if button.clicked(event[3],event[4]) then -- column,row Right Bracket ")" missing here before then
		  redstone.setOutput(key,button.toggle())
		  button.draw(monitor1)
		  break -- we found on, so we don't need to keep looking
	  end
	end

also on the very last line of the code is "end" its not needed and causes an eof error. after fixing what is above. ^^

In the final code you try to call a function button.clicked on line 170 but it is not in the final code only mentioned above inside another spoiler easy to miss.
Edited on 24 April 2014 - 09:15 AM
Lua.is.the.best #6
Posted 26 April 2014 - 07:40 PM
The code:

funtion button.toggle()
  button.isPressed = not button.isPressed
  return button.isPressed
end
should be:

function button.toggle(mode) //Fixed
  if mode = off then //When the player wants it to be off, then
  button.isPressed = not button.isPressed //Disable the button.isPressed event
  return button.isPressed //Return the value of button.isPressed
  elseif mode = on then //When the player wants it to be on, then
  if button.isPressed = not button.isPressed //If button.isPressed is disabled, then
	then button.isPressed = button.isPressed //enable button.isPressed
	return button.isPressed //Return the value of button.isPressed
	else print("button.isPressed event is already enabled.") //Else, print button.isPressed event is already enabled.
  end //End the if inside the elseif.
  end //End the real if.
end //End the function.
Note: Read the comments (the stuff with //) if you are a beginner.
Edited on 26 April 2014 - 05:42 PM
Lyqyd #7
Posted 26 April 2014 - 07:58 PM
Wrong. The code you "corrected" was actually correct. Your code wouldn't work at all, but the way you seem to think it would work, passing off would toggle it and passing on would do nothing.
Lua.is.the.best #8
Posted 26 April 2014 - 08:43 PM
Wrong. The code you "corrected" was actually correct. Your code wouldn't work at all, but the way you seem to think it would work, passing off would toggle it and passing on would do nothing.
Well..
Look closely..
All they really needed to correct was the misspelling of function..
Do this:

function button.toggle()
  button.isPressed = not button.isPressed
  return button.isPressed
end
compared to

funtion button.toggle()
  button.isPressed = not button.isPressed
  return button.isPressed
end
Does the 2nd one error?
Yes.
sEi #9
Posted 23 May 2014 - 10:48 PM
Very nice article and well written. - Keep up the good work.

I have tested the code but it does not work. So i have implemented the fixes from above and added the missing function (button.clicked). I also added
  • monitor1.setTextScale(0.5)
Here is the pastebin with the fixed code.

Looking forward to more tuts - they are gold!!!

/sEi
Kizz #10
Posted 11 September 2014 - 03:59 PM
Great tutorial.

Pointed me in the right direction and solidified my understanding of events. This makes not only touch panel consoles much easier, but also networking. Events are the key to unlocking advanced systems in Computer Craft, and the wiki tutorial can be kind of confusing to understand.

I suggest anyone trying to build any type of system poke around with these tutorials, and try messing with events in different ways. It can be really rewarding.
Edited on 11 September 2014 - 02:00 PM
Hyer #11
Posted 29 October 2014 - 10:20 AM
Wow! Your version works really well… Except… doesn't work on black and white monitors (in Minecraft 1.7.10), not sure if that matters. Same version of Lua so I didn't think it would matter. Thank you for your extra work on this.
Dragon53535 #12
Posted 29 October 2014 - 01:46 PM
Wow! Your version works really well… Except… doesn't work on black and white monitors (in Minecraft 1.7.10)
Your problem is that black and white monitors (AKA. Normal monitors/computers) Don't allow for mouse clicking and thus can't be used on anything other than advanced computers/monitors (The yellow ones)
Hyer #13
Posted 29 October 2014 - 03:12 PM
I didn't know that. Thank you for the info.