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:
- The event (in this case a "monitor_touch" event).
- 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").
- For "monitor_touch" events, the third return value is always the column where the user clicked.
- 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 Events
If 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.- [member='Engineer']'s tutorial on Event Basics
- ComputerCraft Wiki on os.pullEvent
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.