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

[Resolved] Making "buttons" and menu drawing using tables

Started by Nhorr, 14 April 2016 - 11:57 AM
Nhorr #1
Posted 14 April 2016 - 01:57 PM
Plenty of threads have been done on the subject, but I haven't found one that I fully understood to adapt into my code (not completely new, but still got plenty to learn).

To be more clear, here's basically what's up in a nutshell.

The problem: The UI I've constructed has menus that expand (draw menus underneath) when selected via either a click or key press. I understand the principal of these, have tons of them in my code, but they're an absolute pain to edit (have to dig into the code and make various edits). It's also (as could be guessed) a bit unclean.

Solution wanted: One that allows at least the buttons themselves as well as menu handling to be cleaner, probably through the use of tables (which I understand themselves, but I don't have a complete understanding in terms of all possible uses).

Link to the program heaviest with these "buttons": http://pastebin.com/JUhsDLDd
Link to the UI install code: http://pastebin.com/qExuNAUg (or just copy paste this into the terminal: pastebin run qExuNAUg)

The main functions to keep in mind here would be menuBar(), drawMenus(), runMenu(), and runDesktop().

How would you recommend going about and making these a bit more… I guess "modular" would be the word - clean and easy to shift around (through table usage)?

As always, any questions and critiques/tips are always welcome.

Thanks in advance!
Edited on 16 April 2016 - 01:44 PM
The_Cat #2
Posted 14 April 2016 - 09:48 PM
I made this program: http://www.computerc...877#entry232877
pastbin: http://pastebin.com/7j3q4qlp

It has what you might be looking for. So I store the button data in a table then print it out. (Look at the top of the program with all the tables of button info)
In this program each 'page' has its own table and the buttons are tables with in this 'page'.
Look at the functions 'makeButtons' and 'checkButtons' and at the end of the program where os.pullEvent() is called.
Basically when you click on a button it returns the name of the button (or you could make it a unique id, would have to be specified in the table with the button)
So if you wanted a list of button to appear when you click you could specify another table of buttons within the table to show those.

sorry my explanations are not that great :/
If you have any questions just ask.
Edited on 14 April 2016 - 07:49 PM
Nhorr #3
Posted 14 April 2016 - 11:51 PM
I made this program: http://www.computerc...877#entry232877
pastbin: http://pastebin.com/7j3q4qlp

It has what you might be looking for. So I store the button data in a table then print it out. (Look at the top of the program with all the tables of button info)
In this program each 'page' has its own table and the buttons are tables with in this 'page'.
Look at the functions 'makeButtons' and 'checkButtons' and at the end of the program where os.pullEvent() is called.
Basically when you click on a button it returns the name of the button (or you could make it a unique id, would have to be specified in the table with the button)
So if you wanted a list of button to appear when you click you could specify another table of buttons within the table to show those.

sorry my explanations are not that great :/
If you have any questions just ask.

I'll be sure to take a look at it when I can. Many thanks! :)/>
Bomb Bloke #4
Posted 15 April 2016 - 02:42 AM
As it stands, your menu structure is what we call "hardcoded": you've written actual code that specifies how the menu should appear and how it should react. Hardcoding is fine for very simple projects, but it becomes less practical as things get more complex. So the idea is to separate the data outlining the menu away from the code, sticking it into a bunch of variables instead, such that changes to that data don't require changes to the code which processes it. Tables, of course, tend offer the best method of handling any set of data larger than "one single value".

Let's say we build a table like this (spacing optional):

local menus = {
	{["header"] = "Start",   {"CraftOS",  "Shutdown",  "Restart"}, {runShell,         os.shutdown,       os.reboot}},
	{["header"] = "Apps",    {"Clock"},                            {runClock}},
	{["header"] = "Options", {"Password"},                         {runPasswordSetup}},
	{["header"] = "NhUI",    {"Versions", "Uninstall", "About"},   {runUpdater,       runUninstallCheck, runAbout}}
}

Note that values we don't assign keys get numeric indexes automatically. Lua reads the above table as if it were written like this:

local menus = {
	[1] = {["header"] = "Start",   [1] = {[1] = "CraftOS",   [2] = "Shutdown",  [3] = "Restart"}, [2] = {[1] = runShell,         [2] = os.shutdown,       [3] = os.reboot}},
	[2] = {["header"] = "Apps",    [1] = {[1] = "Clock"},                                         [2] = {[1] = runClock}},
	[3] = {["header"] = "Options", [1] = {[1] = "Password"},                                      [2] = {[1] = runPasswordSetup}},
	[4] = {["header"] = "NhUI",    [1] = {[1] = "Versions",  [2] = "Uninstall", [3] = "About"},   [2] = {[1] = runUpdater,       [2] = runUninstallCheck, [3] = runAbout}}
}

So for example, if we refer to menus[4][2][1], we get the "runUpdater" function pointer. Also note that non-numeric keys are ignored by most of Lua's table-related functions and keywords; for example, #menus[1] gives us the number two, as the "header" key is ignored when counting the values, and the numerically indexed values in menus[1] consist of two tables (menus[1][1] and menus[1][2]).

With all this info condensed into one place, we can then have the script itself generate more useful info in the table for us during init (the point before you begin your main program loop) - specifically, info about where each menu item should be rendered, and hence where the user must click to activate it. The rows related to each item are already fairly obvious (if "Shutdown" is the second menu item on the "Start" menu, then it should be rendered two down from the top row, placing it on the third), but columns are a bit of a different story.

local colBump = 1

for i = 1, #menus do
	local thisMenu = menus[i]
	
	thisMenu.xStart = colBump
	thisMenu.xEnd = colBump + #thisMenu.header + 1
	colBump = colBump + #thisMenu.header + 5
end

So now we know that, for example, the column to write the header for the "Options" menu is menus[3].xStart. We also know that a click where x >= menus[3].xStart and x <= menus[3].xEnd and y == 1 is targetting that Options menu, too.

For the entries inside the menus, we need to know their lengths as well - and we can even go through and pad them with spaces and numbers, making all the entries within the same menu the same length and ready to draw straight to the screen later. Let's say we expand our above loop:

local colBump = 1

for i = 1, #menus do
	local thisMenu = menus[i]
	
	thisMenu.xStart = colBump
	thisMenu.xEnd = colBump + #thisMenu.header + 1
	
	local thisSubMenu, maxStringLen = menus[i][1], 0
	
	for j = 1, #thisSubMenu do
		if #thisSubMenu[j] > maxStringLen then maxStringLen = #thisSubMenu[j] end
	end
	
	maxStringLen = maxStringLen + 2
	thisMenu.xSubMenuEnd = colBump + maxStringLen - 1
	
	for j = 1, #thisSubMenu do
		thisSubMenu[j] = tostring(j) .. "|" .. thisSubMenu[j] .. string.rep(" ", maxStringLen - #thisSubMenu[j])
	end
	
	colBump = colBump + #thisMenu.header + 5	
end

menus[?].xSubMenuEnd now acts as menus[?].xEnd does, but for the items in the menu instead of for the menu header.

Let's say we want to draw our menus. We'll scrap the menuBar() function, and combine its functionality into the drawMenus() function. Bearing in mind that "s" indicates the index of the opened menu (or is set to 0 if no menu is open at all):

local function drawMenus()
	term.setTextColor(deskColors.menuTextColor)
	term.setBackgroundColor(deskColors.menuColor)
	
	for i = 1, #menus do
		local thisMenu = menus[i]
		term.setCursorPos(thisMenu.xStart, 1)
		term.write((s == 0 and tostring(i) or "-") .. "|" .. thisMenu.header)  --# See http://lua-users.org/wiki/TernaryOperator
	end
	
	if s > 0 then
		local thisSubMenu, x = menus[s][1], menus[s].xStart
		
		for i = 1, #thisSubMenu do
			term.setCursorPos(x, i + 1)
			term.write(thisSubMenu[i])
		end
	end
end

That only leaves user input to deal with:

local function runDesktop()
	s=0  --# selection rule
	
	while true do
		local event,b,x,y=os.pullEventRaw()
		
		if event == "mouse_click" and b == 1 then
			if y == 1 then
				--# A click to the top row:
				s = 0
				
				for i = 1, #menus do
					if x >= menus[i].xStart and x <= menus[i].xEnd then
						s = i
						break
					end
				end
			elseif s > 0 and y <= #menus[s][1] + 1 and x >= menus[s].xStart and x <= menus[s].xSubMenuEnd then
				menus[s][2][y - 1]()  --# Run the function for the selected menu item.
				s = 0
			else
				s = 0
			end
			
			drawMenus()
		elseif event == "char" then
			b = string.byte(b) - 48  --# "1" is character 49, "2" is character 50, etc
			
			if s == 0 and b > 0 and b <= #menus then
				s = b
				drawMenus()
			elseif s > 0 and b > 0 and b <= #menus[s][1] then
				menus[s][2][b]()
				s = 0
				drawMenus()
			end
		end
	end
end

At this point, just about all hardcoding is removed - it's pretty trivial to add or shuffle menu items (or entire new menus) simply by messing with the table.
Nhorr #5
Posted 15 April 2016 - 03:53 AM
Thanks Bomb Bloke for the (very appreciatively detailed and typed out) reply - it was really insightful.

And The_Cat, I got a chance to look at your code and I pretty much saw the general workings of your code.

I'll decide what I end up doing with my code and mess with it sometime in the next few days. If I have any more questions I'll be sure to ask them.

Thanks again for the tremendous amount of help. You guys have not only pointed me in the right direction, but gave me insight on it as well :)/>

EDIT: I'm going to go the hardcoded way for now. As beneficial as it would be to have tables, I just can't wrap my head around this process and I'd rather stick to what I know till I can fully understand the unknown. Thanks again for all the help!
Edited on 16 April 2016 - 01:46 PM