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

Custom button api, attempt to index? (a nil value)

Started by Skillz, 11 May 2014 - 11:01 PM
Skillz #1
Posted 12 May 2014 - 01:01 AM
I've recently been trying to create my own custom button api, I know there are others available but I want the experience of creating one myself.
due to quite an unavoidable situation of complicated data structures, I decided to use the privacy approach for my objects, their values and methods. An example can be found here.

Spoiler

function newAccount (initialBalance)
	  local self = {balance = initialBalance}
  
	  local withdraw = function (v)
						 self.balance = self.balance - v
					   end
  
	  local deposit = function (v)
						self.balance = self.balance + v
					  end
  
	  local getBalance = function () return self.balance end
  
	  return {
		withdraw = withdraw,
		deposit = deposit,
		getBalance = getBalance
	  }
	end

acc1 = newAccount(100.00)
acc1.withdraw(40.00)
print(acc1.getBalance())	 --> 60

This example works inside computercraft, the problem i'm having is that if I store the function newAccount in an api, and load that api, if I try to make reference to that function then I get an error 'test:21: attempt to index? (a nil value)'


The same goes for my button api.
Spoiler

function aType(n) -- extends type() to include integer's and float's as data types.
  if type(n) == 'number' then
	if n == math.floor(n) then
	  return 'integer'
	else
	  return 'float'
	end
  else
	return type(n)
  end
end
local function split(s, sep)
  sep = sep or "%s"
  words = {}
  pattern = string.format("[^%s]+", sep)
  for word in string.gmatch(tostring(s), pattern) do
	table.insert(words, word)
  end
  return words
end
local buttonTemplate  = {
  parent			  = term.native(),
  args				= {},
  width			   = 10,
  height			  = 5,
  textColor		   = colors.white,
  backgroundColorOn   = colors.green,
  backgroundColorOff  = colors.red,
  active			  = false,
  visible			 = true,
  align			   = 'centre',
}
local alignment = {
  'left'	= true,
  'right'   = true
  'centre'  = true,
}
function newButton()
  local self = {}
  local add = function(o)
	local o = o or {}
	setmetatable(o, {__index = buttonTemplate})
	if aType(o.width)			  ~= 'integer'  then error('[\"width\"] expected integer, got ' .. aType(o.width)) end
	if aType(o.height)			 ~= 'integer'  then error('[\"height\"] expected integer, got ' .. aType(o.height)) end
	if aType(o.textColor)		  ~= 'integer'  then error('[\"textColor\"] expected integer, got '.. aType(o.textColor)) end
	if aType(o.backgroundColorOn)  ~= 'integer'  then error('[\"backgroundColorOn\"] expected integer, got ' .. aType(o.width)) end
	if aType(o.backgroundColorOff) ~= 'integer'  then error('[\"backgroundColorOff\"] expected integer, got ' .. aType(o.height)) end
	if type(o.active)			  ~= 'boolean'  then error('[\"active\"] expected boolean, got ' .. type(o.active)) end
	if type(o.visible)			 ~= 'boolean'  then error('[\"visible\"] expected boolean, got ' .. type(o.active)) end
	if type(o.align)			   ~= 'string'   then error('[\"align\"] expected string, got ' .. type(o.align)) end
	if type(o.label)			   ~= 'string'   then error('[\"label\"] expected string, got ' .. type(o.align)) end
	if type(o.args)				~= 'table'	then error('[\"args\"] expected table, got ' .. type(o.args)) end
	if type(o.parent)			  ~= 'table'	then error('[\"parent\"] expected table, got ' .. type(o.args)) end
	if type(o.func)				~= 'function' then error('[\"args\"] expected function, got ' .. type(o.args)) end
	if not alignment[o.align]					then error('[\"align\"] expected \"left\", \"right\" or \"centre\", got '.. o.align) end
	o.backgroundColor = backgroundColorOff
	o.window = window.create(o.parent, o.x, o.y, o.width, o.height)
	self[o.label] = o
	draw(o.label)
  end
  local getActive = function(label)
	return self[label].active
  end
  local toggleButton = function(label)
	self[label].active = not self[label].active
	if self[label].active then
	  self.[label].backgroundColor = self[label].backgroundColorOn
	else
	  self[label].backgroundColor = self[label].backgroundColorOff
	end
	self.draw(label)
  end
  local flashButton = function(label, time)
	time = time or 0.1
	self.toggleButton(label)
	sleep(time)
	self.:toggleButton(label)
  end
  local draw = function(label)
	self[label].window.setBackgroundColor(self[label].backgroundColor)
	self[label].window.clear()
	self.writeText(label)
  end
  local handleButtonPress = function(x, y)
	for _, button in pairs(self) do
	  if (y >= button.y) and
	  (y <= (button.y + button.height -1)) and
	  (x >= button.x) and
	  (x <= (button.width + button.x -1)) then
		button.func(unpack(button.args))
		return true
	  end
	end
	return false
  end
  local writeText = function(label)
	local x, y
	local w, h = self[label].width, self[label].height
	local lines = split(string.gsub(label, ' \n'))
	if (#lines > h) then
	  error('[\"label\"] the number of text lines exceed the height of the button')
	end
	for y, string in pairs(lines) do
	  if (string.len(string) - w) > w then
		error('[\"label\"] text line length longer than button width')
	  end
	  if self[label].align == "left" then
		x = 1
	  elseif self[label].align == "centre" then
		x = math.floor(w / 2) - math.floor(string.len(string) / 2)
	  elseif self[label].align == "right" then
		x = w - string.len(string) + 1
	  end
	  self[label].window.setCursorPos(x, y)
	  self[label].window.write(string)
	end
  end
  local drawButtons = function ()
	for _, button in self do
	  button.draw(button.label)
	end
  end
  return {
	add			= add,
	getActive	  = getActive,
	toggleButton   = toggleButton,
	flashButton	= flashButton,
	drawButtons	= drawButtons,
	handleButtonPress = handleButtonPress
  }
end

and this is my script that runs in a test file:

os.loadAPI("buttongui")
local function getPeripheralsByType(p)
	local TabDetected = {}
		for _, v in ipairs(peripheral.getNames()) do
			if peripheral.getType(v) == p then
		table.insert(TabDetected, v)
		end
	end
	return TabDetected
end
for _, v in ipairs(getPeripheralsByType('modem')) do
  if peripheral.wrap(v).isWireless() then
	  rednet.open(v)
  end
end
term.clear()
a1 = buttongui.newButton()

Any help would be appreciated :)/>
Edited on 13 May 2014 - 03:23 PM
Dog #2
Posted 12 May 2014 - 03:35 AM
I'm not the one to help you with this, but I did notice an error in your API (in function newButton()) that will probably cause you problems

if type(o.func) ~= 'fucntion' then error('[\"args\"] expected function, got ' .. type(o.args)) end

the first 'function' is misspelled as 'fucntion' - it should read…

if type(o.func) ~= 'function' then error('[\"args\"] expected function, got ' .. type(o.args)) end

Hope that helps in some way :)/>
Edited on 12 May 2014 - 07:36 AM
Skillz #3
Posted 12 May 2014 - 08:09 AM
ah thank you very much, the product of a rather late night hehe :)/>
Skillz #4
Posted 13 May 2014 - 04:03 PM
Solved now :)/>, took a fresh look at this this morning and there were some silly mistakes that weren't being errored correctly. bit disappointed in the lack of support for object oriented programming but I guess that's to be expected :)/>

The important thing now is that i've got a working solution to privacy within objects, which means that you can only access the methods and variables you return back, any other data is still available, but only to the methods returned to the calling script.

I might make a tutorial on it, amongst other things, it's a great way to keep object data structures from spiraling out of control :P/>
CometWolf #5
Posted 13 May 2014 - 04:28 PM
There is much easier ways to handle OOP in Lua, using metatables.
http://www.lua.org/pil/16.1.html
Skillz #6
Posted 13 May 2014 - 05:18 PM
This was the code before I changed to the final method:
Spoiler

local buttonTemplate	  = {
  parent			  = term.native(),
  args				= {},
  width			   = 10,
  height			  = 5,
  textColor		   = colors.white,
  backgroundColorOn   = colors.green,
  backgroundColorOff  = colors.red,
  backgroundColor	 = colors.red,
  active			  = false,
  align			   = 'centre',
}
local alignment = {
  'left'	= true,
  'right'   = true
  'centre'  = true,
}
local button = {}
function new() --Creates a new button object
  local buttonHandler = {buttonContainer = {}}
  setmetatable(buttonHandler, {__index = button})
  return buttonHandler
end
function button.add (self, o)
  local o = o or {}
  setmetatable(o, {__index = buttonTemplate})
  o.text = o.text or {o.label}
  if type(o.args) ~= 'table' then error('function arguments must be passed in a table') end
  if not alignment[o.align] then error('align can only take values: \"left\", \"right\" and \"centre\"') end
  if o.func	   == nil then error('function not specified') end
  if o.x		  == nil then error('x coordinate not specified') end
  if o.y		  == nil then error('y coordinate not specified') end
  if o.label	  == nil then error('label not specified') end
  o.text = o.text or o.label
  o.window = window.create(o.parent, o.x, o.y, o.width, o.height)
  self.buttonContainer[o.label] = o
  self:draw(o.label)
end
function button.getActive(self, label)
  return self.buttonContainer[label].active
end
function button.toggleButton(self, label)
  self.buttonContainer[label].active = not self.buttonContainer[label].active
  if self.buttonContainer[label].active then
	self.buttonContainer[label].backgroundColor = self.buttonContainer[label].backgroundColorOn
  else
	self.buttonContainer[label].backgroundColor = self.buttonContainer[label].backgroundColorOff
  end
  self:draw(label)
end  

function button.flashButton(self, label, time)
  time = time or 0.1
  self:toggleButton(label)
  sleep(time)
  self:toggleButton(label)
end
function button.draw(self, label)
  self.buttonContainer[label].window.setBackgroundColor(self.buttonContainer[label].backgroundColor)
  self.buttonContainer[label].window.clear()
  self:writeText(label, 2)
end
function button.getButtonPress(self, x, y)
  for _, button in pairs(self.buttonContainer) do
	if (y >= button.y) and
	(y <= (button.y + button.height -1)) and
	(x >= button.x) and
	(x <= (button.width + button.x -1)) then
	  button.func(unpack(button.args))
	  return true
	end
  end
  return false
end
function button.writeText(self, label)
  local x, y
  local w, h	= self.buttonContainer[label].width, self.buttonContainer[label].height
  if (#self.buttonContainer[label].text > h) then
	error('the number of text lines exceed the number of button lines')
  end
  for y, string in pairs(self.buttonContainer[label].text) do
	if (string.len(string) - w) > w then
	  error('text line length longer than button width')
	end
	if self.buttonContainer[label].align == "left" then
	  x = 1
	elseif self.buttonContainer[label].align == "centre" then
	  x = math.floor(w / 2) - math.floor(string.len(string) / 2)
	elseif self.buttonContainer[label].align == "right" then
	  x = w - string.len(string) + 1
	end
	self.buttonContainer[label].window.setCursorPos(x, y)
	self.buttonContainer[label].window.write(string)
  end
end
function button.drawButtons(self)
  for _, button in self.buttonContainer do
	button.draw(button.label)
  end
end

and the final version is:
Spoiler

function aType(n) -- extends type() to include integer's and float's as data types.
  if type(n) == 'number' then
	if n == math.floor(n) then
	  return 'integer'
	else
	  return 'float'
	end
  else
	return type(n)
  end
end
local function split(s, sep)
  sep = sep or "%s"
  words = {}
  pattern = string.format("[^%s]+", sep)
  for word in string.gmatch(tostring(s), pattern) do
	table.insert(words, word)
  end
  return words
end
function new()
  local self = {}
  local repository = {}
  function self.add(o)
	local buttonTemplate  = {
	  parent			  = term.native(),
	  args				= {},
	  width			   = 10,
	  height			  = 5,
	  textColor		   = colors.white,
	  backgroundColorOn   = colors.green,
	  backgroundColorOff  = colors.red,
	  active			  = false,
	  visible			 = true,
	  align			   = 'centre'
	}
	local alignment = {
	  left	= true,
	  right   = true,
	  centre  = true
	}
	local o = o or {}
	setmetatable(o, {__index = buttonTemplate})
	if aType(o.width)			  ~= 'integer'  then error('[width] expected integer, got ' .. aType(o.width)) end
	if aType(o.height)			 ~= 'integer'  then error('[height] expected integer, got ' .. aType(o.height)) end
	if aType(o.textColor)		  ~= 'integer'  then error('[textColor] expected integer, got '.. aType(o.textColor)) end
	if aType(o.backgroundColorOn)  ~= 'integer'  then error('[backgroundColorOn] expected integer, got ' .. aType(o.width)) end
	if aType(o.backgroundColorOff) ~= 'integer'  then error('[backgroundColorOff] expected integer, got ' .. aType(o.height)) end
	if type(o.active)			  ~= 'boolean'  then error('[active] expected boolean, got ' .. type(o.active)) end
	if type(o.visible)			 ~= 'boolean'  then error('[visible] expected boolean, got ' .. type(o.active)) end
	if type(o.align)			   ~= 'string'   then error('[align] expected string, got ' .. type(o.align)) end
	if type(o.label)			   ~= 'string'   then error('[label] expected string, got ' .. type(o.align)) end
	if type(o.args)				~= 'table'	then error('[args] expected table, got ' .. type(o.args)) end
	if type(o.parent)			  ~= 'table'	then error('[parent] expected table, got ' .. type(o.args)) end
	if type(o.func)				~= 'function' then error('[args] expected function, got ' .. type(o.args)) end
	if not alignment[o.align]					then error('[align] expected \'left\', \'right\' or \'centre\', got '.. o.align) end
	o.backgroundColor = o.backgroundColorOff
	o.window = window.create(o.parent, o.x, o.y, o.width, o.height)
	repository[o.label] = o
	self.draw(o.label)
  end
  function self.getActive(label)
	return repository[label].active
  end
  function self.toggleButton(label)
	repository[label].active = not repository[label].active
	if repository[label].active then
	  repository[label].backgroundColor = repository[label].backgroundColorOn
	else
	  repository[label].backgroundColor = repository[label].backgroundColorOff
	end
	self.draw(label)
  end
  function self.handleButtonPress(x, y)
	for _, button in pairs(repository) do
	  if (y >= button.y) and
	  (y <= (button.y + button.height -1)) and
	  (x >= button.x) and
	  (x <= (button.width + button.x -1)) then
		button.func(unpack(button.args))
		return true
	  end
	end
	return false
  end
  function self.flashButton(label, time)
	time = time or 0.1
	self.toggleButton(label)
	sleep(time)
	self.toggleButton(label)
  end
  function self.draw(label)
	repository[label].window.setBackgroundColor(repository[label].backgroundColor)
	repository[label].window.clear()
	self.writeText(label)
  end
  function self.writeText(label)
	local x, y
	local w, h = repository[label].width, repository[label].height
	local lines = split(string.gsub(label, '\n', ' \n'), '\n')
	if (#lines > h) then
	  error('[\"label\"] the number of text lines exceed the height of the button')
	end
	for y, string in pairs(lines) do
	  if (string.len(string) - w) > w then
		error('[\"label\"] text line length longer than button width')
	  end
	  if repository[label].align == "left" then
		x = 1
	  elseif repository[label].align == "centre" then
		x = math.floor(w / 2) - math.floor(string.len(string) / 2)
	  elseif repository[label].align == "right" then
		x = w - string.len(string) + 1
	  end
	  repository[label].window.setCursorPos(x, y)
	  repository[label].window.write(string)
	end
  end
  function self.drawButtons()
	for _, button in repository do
	  button.draw(button.label)
	end
  end
  return self
end

In my case this method of creating objects was far more suited.
firstly, the second code is far easier to understand because it's data structures are far less complicated. making the code more maintainable. and easier to pick up by anyone who hasn't read the code before.
secondly, because you can't access variables directly (you can only access them through methods), there is a much less chance of people breaking the api.
you can read more about privacy in objects here and there are some good examples of implementation here.
Edited on 13 May 2014 - 03:30 PM