Difference between revisions of "ButtonMonster class"

From GiderosMobile
(Created page with "__TOC__ This button class is useful to build buttons which you can navigate using the mouse and the keyboard. It is optimised for menus and in game. '''Note: needs Gideros...")
 
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
__TOC__
 
__TOC__
  
This button class is useful to build buttons which you can navigate using the mouse and the keyboard. It is optimised for menus and in game.
+
This ButtonMonster class allows you to build any button, from the simplest (text) to the most complicated (tooltip, keyboard navigation). It is optimised for menus and in games.
  
'''Note: needs Gideros ''luashader'' Library'''
+
'''[[Media:buttonMonster.lua]]''' '''(tip: right click and save link as)'''
  
 
=== ButtonMonster '''Class''' ===
 
=== ButtonMonster '''Class''' ===
Line 9: Line 9:
 
--[[
 
--[[
 
-- ButtonMonster
 
-- ButtonMonster
-- A Button class with:
+
-- mokalux, cc0
-- a Pixel, Image 9patch, Text, Tooltip,
+
-- Pixel, Image, 9patch, Text, Tooltip,
-- Up state, Down state, Disabled state,
+
-- Up, Down, Disabled, Hover,
-- Hover, Sfx, Mouse and Keyboard navigation!
+
-- Sfx, Touch, Mouse and Keyboard navigation!
v 0.2.0: 2023-11-20 terminator, should be fine in games too now
+
-- Functions
 +
v 0.2.2: 2024-11-08 selectionSfx(): added channel checks before setting volume
 +
v 0.2.1: 2024-04-22 added function callback (functions with no parameters for now!)
 +
v 0.2.0: 2023-12-01 terminator, should be fine in games too now
 
v 0.1.0: 2021-06-01 total recall, this class has become a Monster! best used in menus but who knows?
 
v 0.1.0: 2021-06-01 total recall, this class has become a Monster! best used in menus but who knows?
 
v 0.0.1: 2020-03-28 init (based on the initial generic Gideros Button class)
 
v 0.0.1: 2020-03-28 init (based on the initial generic Gideros Button class)
 
]]
 
]]
 
-- Shader, please adapt the path!
 
--!NEEDS:../luashader/luashader.lua
 
 
function vshaderpixelslot(vVertex, vColor, vTexCoord) : Shader
 
local vertex = hF4(vVertex, 0.0, 1.0)
 
fTexCoord = vTexCoord
 
return vMatrix*vertex
 
end
 
function fshaderpixelslot() : Shader
 
local frag = texture2D(fTexture, fTexCoord) -- 0, not focused
 
if slot == 2.0 then frag = texture2D(fTexture3, fTexCoord) -- 2, disabled
 
elseif slot == 1.0 then frag = texture2D(fTexture2, fTexCoord) -- 1, focused
 
end
 
return lF4(fColor*fColor.a)*frag -- alpha
 
end
 
 
local shaderpixelslot=Shader.lua(vshaderpixelslot, fshaderpixelslot, 0,
 
{
 
{name="vMatrix", type=Shader.CMATRIX, sys=Shader.SYS_WVP, vertex=true},
 
{name="fColor", type=Shader.CFLOAT4, sys=Shader.SYS_COLOR, vertex=false}, -- 1st color slot
 
{name="fColor2", type=Shader.CFLOAT4, sys=Shader.SYS_COLOR, vertex=false}, -- 2nd color slot
 
{name="fColor3", type=Shader.CFLOAT4, sys=Shader.SYS_COLOR, vertex=false}, -- 3rd color slot
 
{name="fTexture", type=Shader.CTEXTURE, vertex=false},
 
{name="fTexture2", type=Shader.CTEXTURE, vertex=false},
 
{name="fTexture3", type=Shader.CTEXTURE, vertex=false},
 
{name="slot", type=Shader.CFLOAT, vertex=false},
 
},
 
{
 
{name="vVertex", type=Shader.DFLOAT, mult=2, slot=0, offset=0},
 
{name="vColor", type=Shader.DUBYTE, mult=4, slot=1, offset=0},
 
{name="vTexCoord", type=Shader.DFLOAT, mult=2, slot=2, offset=0},
 
},
 
{
 
{name="fTexCoord", type=Shader.CFLOAT2},
 
}
 
)
 
  
 
-- Class
 
-- Class
Line 87: Line 53:
 
self.params.pixelscalexdown = xparams.pixelscalexdown or self.params.pixelscalexup -- number
 
self.params.pixelscalexdown = xparams.pixelscalexdown or self.params.pixelscalexup -- number
 
self.params.pixelscaleydown = xparams.pixelscaleydown or self.params.pixelscaleyup -- number
 
self.params.pixelscaleydown = xparams.pixelscaleydown or self.params.pixelscaleyup -- number
self.params.pixelwidth = xparams.pixelwidth or 8*3 -- number (autoscale = x padding else width)
+
self.params.pixelwidth = xparams.pixelwidth or 24 -- 24, number (autoscale = x padding else width)
 
self.params.pixelheight = xparams.pixelheight or self.params.pixelwidth -- number (autoscale = y padding else height)
 
self.params.pixelheight = xparams.pixelheight or self.params.pixelwidth -- number (autoscale = y padding else height)
self.params.ninepatch = xparams.ninepatch or 8 -- 0, number
+
self.params.ninepatch = xparams.ninepatch or 16 -- 0, 8, number
 
-- text?
 
-- text?
 
self.params.text = xparams.text or nil -- string
 
self.params.text = xparams.text or nil -- string
Line 111: Line 77:
 
-- audio?
 
-- audio?
 
self.params.sound = xparams.sound or nil -- sound fx
 
self.params.sound = xparams.sound or nil -- sound fx
self.params.volume = xparams.volume or nil -- sound volume
+
self.params.volume = xparams.volume or 0.5 -- sound volume
-- warnings, errors?
+
-- EXTRAS
if self.params.pixelalphaup <= 0 then self.params.pixelalphaup = 0.01 end -- alpha <= 0 breaks shader!
+
self.params.fun = xparams.fun or nil -- function (please check function name if not working!)
if self.params.pixelalphadown <= 0 then self.params.pixelalphadown = 0.01 end -- alpha <= 0 breaks shader!
+
-- set warnings, errors
-- button sprite holder
+
if self.params.sound and (type(self.params.sound) ~= "table" or self.params.sound.sound == nil) then
self.sprite = Sprite.new()
+
print("*** ERROR ***", "ON BUTTON: "..self.selector or "?",
self:addChild(self.sprite)
+
"YOUR SOUND MUST BE A TABLE, SOMETHING LIKE: { sound=Sound.new(\"path/to/your/sound.wav\"), time=0, delay=0.2 }")
 +
self.params.sound = nil
 +
end
 +
if self.params.fun and type(self.params.fun) ~= "function" then
 +
print("*** ERROR ***", "ON BUTTON: "..self.selector or "?", "YOU ARE NOT PASSING A FUNCTION")
 +
if self.params.text then self.params.text = self.params.text.." (error)"
 +
else self.params.text = "error"
 +
end
 +
self.params.ttf = nil self.params.textscalexup = 3 self.params.textscaleyup = 3
 +
self.params.textcolorup = 0xff0000
 +
end
 
-- let's go!
 
-- let's go!
 
self:setButton()
 
self:setButton()
 
-- update visual state
 
-- update visual state
 +
self.focus = false
 +
self.hover = false
 +
self.disabled = false
 
self:updateVisualState()
 
self:updateVisualState()
self.hovered = nil
+
-- mouse event listeners
self.disabled = nil
 
self.ismouse = true
 
-- event listeners
 
 
self:addEventListener(Event.MOUSE_DOWN, self.onMouseDown, self)
 
self:addEventListener(Event.MOUSE_DOWN, self.onMouseDown, self)
 +
self:addEventListener(Event.MOUSE_MOVE, self.onMouseMove, self)
 
self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
 
self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
 
self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
 
self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
 +
-- touch event listeners
 +
self:addEventListener(Event.TOUCHES_BEGIN, self.onTouchesBegin, self)
 +
self:addEventListener(Event.TOUCHES_MOVE, self.onTouchesMove, self)
 +
self:addEventListener(Event.TOUCHES_END, self.onTouchesEnd, self)
 +
self:addEventListener(Event.TOUCHES_CANCEL, self.onTouchesCancel, self)
 
end
 
end
  
Line 136: Line 118:
 
local textwidth, textheight
 
local textwidth, textheight
 
if self.params.text then
 
if self.params.text then
self.text = TextField.new(self.params.ttf, self.params.text, self.params.text)
+
if self.tf then self:removeChild(self.tf) self.tf = nil end
self.text:setAnchorPoint(0.5, 0.5)
+
self.tf = TextField.new(self.params.ttf, self.params.text, self.params.text)
self.text:setScale(self.params.textscalexup, self.params.textscaleyup)
+
self.tf:setAnchorPoint(0.5, 0.5)
self.text:setTextColor(self.params.textcolorup)
+
self.tf:setScale(self.params.textscalexup, self.params.textscaleyup)
self.text:setAlpha(self.params.textalphaup)
+
self.tf:setTextColor(self.params.textcolorup)
textwidth, textheight = self.text:getWidth(), self.text:getHeight()
+
self.tf:setAlpha(self.params.textalphaup)
 +
textwidth, textheight = self.tf:getWidth(), self.tf:getHeight()
 
end
 
end
 
-- first add pixel
 
-- first add pixel
local pixelimg = false
+
if self.pixel then self:removeChild(self.pixel) self.pixel = nil end
 
if self.params.autoscale and self.params.text then
 
if self.params.autoscale and self.params.text then
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
Line 151: Line 134:
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
 
self.params.pixelwidth, self.params.pixelheight)
 
self.params.pixelwidth, self.params.pixelheight)
end
 
if self.params.pixelimgup then
 
self.pixel:setTexture(self.params.pixelimgup, 0)
 
pixelimg = true
 
end
 
if self.params.pixelimgdown then
 
self.pixel:setTexture(self.params.pixelimgdown, 1)
 
pixelimg = true
 
end
 
if self.params.pixelimgdisabled then
 
self.pixel:setTexture(self.params.pixelimgdisabled, 2)
 
pixelimg = true
 
 
end
 
end
 
self.pixel:setScale(self.params.pixelscalexup, self.params.pixelscaleyup)
 
self.pixel:setScale(self.params.pixelscalexup, self.params.pixelscaleyup)
 
self.pixel:setAnchorPoint(0.5, 0.5)
 
self.pixel:setAnchorPoint(0.5, 0.5)
if pixelimg then self.pixel:setShader(shaderpixelslot) end
+
self.pixel:setNinePatch(self.params.ninepatch)
self.sprite:addChild(self.pixel)
+
if self.params.pixelimgup then self.pixel:setTexture(self.params.pixelimgup)
 +
elseif self.params.pixelimgdown then self.pixel:setTexture(self.params.pixelimgdown)
 +
elseif self.params.pixelimgdisabled then self.pixel:setTexture(self.params.pixelimgdisabled)
 +
end
 +
self:addChild(self.pixel)
 
-- then add text?
 
-- then add text?
if self.params.text then self.sprite:addChild(self.text) end
+
if self.params.text then self:addChild(self.tf) end
 
-- finally add tooltip?
 
-- finally add tooltip?
 
if self.params.tooltiptext then
 
if self.params.tooltiptext then
self.ttiptext = TextField.new(self.params.tooltipttf, self.params.tooltiptext, self.params.tooltiptext)
+
if self.ttiptf then
self.ttiptext:setScale(self.params.tooltiptextscale)
+
if self.tooltiplayer then self.tooltiplayer:removeChild(self.ttiptf)
self.ttiptext:setTextColor(self.params.tooltiptextcolor)
+
else self:removeChild(self.ttiptf)
self.ttiptext:setVisible(false)
+
end self.ttiptf = nil
if self.tooltiplayer then self.tooltiplayer:addChild(self.ttiptext)
+
end
else self:addChild(self.ttiptext)
+
self.ttiptf = TextField.new(self.params.tooltipttf, self.params.tooltiptext, self.params.tooltiptext)
 +
self.ttiptf:setScale(self.params.tooltiptextscale)
 +
self.ttiptf:setTextColor(self.params.tooltiptextcolor)
 +
self.ttiptf:setVisible(false)
 +
if self.tooltiplayer then self.tooltiplayer:addChild(self.ttiptf)
 +
else self:addChild(self.ttiptf)
 
end
 
end
 
end
 
end
 
end
 
end
  
-- VISUAL STATE
 
 
function ButtonMonster:updateVisualState()
 
function ButtonMonster:updateVisualState()
 
local function visualState(btn, btnscalex, btnscaley, btnalpha, textcolor, textscalex, textscaley,
 
local function visualState(btn, btnscalex, btnscaley, btnalpha, textcolor, textscalex, textscaley,
pixtexslot, pixelcolor, pixelalpha, pixelscalex, pixelscaley)
+
pixeltex, pixelcolor, pixelalpha, pixelscalex, pixelscaley)
 
btn:setScale(btnscalex, btnscaley)
 
btn:setScale(btnscalex, btnscaley)
 
btn:setAlpha(btnalpha)
 
btn:setAlpha(btnalpha)
 
if btn.params.text then
 
if btn.params.text then
btn.text:setTextColor(textcolor)
+
btn.tf:setTextColor(textcolor)
btn.text:setScale(textscalex, textscaley)
+
btn.tf:setScale(textscalex, textscaley)
end
 
if btn.params.pixelimgup then -- texture
 
local r, g, b = (pixelcolor >> 16 & 0xff) / 255, (pixelcolor >> 8 & 0xff) / 255, (pixelcolor & 0xff) / 255
 
btn.pixel:setShaderConstant("slot", Shader.CFLOAT, 1, pixtexslot) -- set Pixel texture slot
 
btn.pixel:setShaderConstant("fColor", Shader.CFLOAT4, 1, r, g, b, pixelalpha) -- set Pixel color
 
else
 
btn.pixel:setColor(pixelcolor, pixelalpha)
 
 
end
 
end
 +
if pixeltex then btn.pixel:setTexture(pixeltex) end
 +
btn.pixel:setColor(pixelcolor, pixelalpha)
 
btn.pixel:setScale(pixelscalex, pixelscaley)
 
btn.pixel:setScale(pixelscalex, pixelscaley)
end
 
local function vtooltip(index, btn) -- keyboard tooltip visuals
 
-- if btn.ttiptext and not btn.disabled then -- OPTION 1: hides tooltip when button is Disabled
 
if btn.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
 
if index == btn.currselector then -- button is focused
 
if btn.disabled then btn.ttiptext:setText("("..btn.params.tooltiptext..")") -- extra!
 
else btn.ttiptext:setText(btn.params.tooltiptext)
 
end
 
btn.ttiptext:setVisible(true)
 
else -- button is not focused
 
btn.ttiptext:setVisible(false)
 
end
 
if not btn.hovered then -- reposition the tooltip when mouse is not hovering
 
if btn.tooltiplayer then
 
btn.ttiptext:setPosition(
 
btn:getX()+btn.params.tooltipoffsetx, btn:getY()+btn.params.tooltipoffsety)
 
else
 
btn.ttiptext:setPosition(btn:globalToLocal(
 
btn:getX()+btn.params.tooltipoffsetx, btn:getY()+btn.params.tooltipoffsety))
 
end
 
end
 
end
 
 
end
 
end
 
if self.btns then
 
if self.btns then
 
-- print("KEYBOARD NAVIGATION")
 
-- print("KEYBOARD NAVIGATION")
 
for k, v in ipairs(self.btns) do
 
for k, v in ipairs(self.btns) do
if v.disabled then -- button is Disabled
+
if v.disabled then -- disabledState
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
v.params.textcolordisabled, v.params.textscalexdown, v.params.textscaleydown,
 
v.params.textcolordisabled, v.params.textscalexdown, v.params.textscaleydown,
2.0, v.params.pixelcolordisabled, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
+
v.params.pixelimgdisabled, v.params.pixelcolordisabled,
vtooltip(k, v)
+
v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
elseif k == v.currselector then -- button is focused
+
-- if v.ttiptf and not v.disabled then -- OPTION 1: hides tooltip when button is Disabled
 +
if v.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
 +
v.ttiptf:setText("("..v.params.tooltiptext..")") -- extra!
 +
if k == v.currselector then v.ttiptf:setVisible(true)
 +
else v.ttiptf:setVisible(false)
 +
end
 +
end
 +
elseif k == v.currselector then -- downState
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
v.params.textcolordown, v.params.textscalexdown, v.params.textscaleydown,
 
v.params.textcolordown, v.params.textscalexdown, v.params.textscaleydown,
1.0, v.params.pixelcolordown, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
+
v.params.pixelimgdown, v.params.pixelcolordown,
vtooltip(k, v)
+
v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
else -- button is not focused
+
if v.ttiptf then
 +
v.ttiptf:setText(v.params.tooltiptext)
 +
if v.tooltiplayer then -- reset tooltip text position
 +
v.ttiptf:setPosition(
 +
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety)
 +
else
 +
v.ttiptf:setPosition(v:globalToLocal(
 +
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety))
 +
end
 +
v.ttiptf:setVisible(true)
 +
end
 +
else -- upState
 
visualState(v, v.params.btnscalexup, v.params.btnscaleyup, v.params.btnalphaup,
 
visualState(v, v.params.btnscalexup, v.params.btnscaleyup, v.params.btnalphaup,
 
v.params.textcolorup, v.params.textscalexup, v.params.textscaleyup,
 
v.params.textcolorup, v.params.textscalexup, v.params.textscaleyup,
0.0, v.params.pixelcolorup, v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
+
v.params.pixelimgup, v.params.pixelcolorup,
vtooltip(k, v)
+
v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
 +
if v.ttiptf then v.ttiptf:setVisible(false) end
 
end
 
end
 
end
 
end
elseif self.ismouse then
+
else
-- print("MOUSE NAVIGATION")
+
-- print("TOUCH, MOUSE NAVIGATION")
self.ismouse = false
+
if self.disabled then -- disabledState
if self.disabled then -- button is Disabled
 
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
self.params.textcolordisabled, self.params.textscalexdown, self.params.textscaleydown,
 
self.params.textcolordisabled, self.params.textscalexdown, self.params.textscaleydown,
2.0, self.params.pixelcolordisabled, self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
+
self.params.pixelimgdisabled, self.params.pixelcolordisabled,
-- if self.ttiptext and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
+
self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
if self.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
+
-- if self.ttiptf and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
if self.disabled then self.ttiptext:setText("("..self.params.tooltiptext..")") -- extra!
+
if self.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
else self.ttiptext:setText(self.params.tooltiptext)
+
self.ttiptf:setText("("..self.params.tooltiptext..")") -- extra!
end
+
if self.focus then self.ttiptf:setVisible(true)
if self.hovered then self.ttiptext:setVisible(true)
+
else self.ttiptf:setVisible(false)
else self.ttiptext:setVisible(false)
 
 
end
 
end
 
end
 
end
elseif self.hovered then -- button is focused
+
elseif self.focus or self.hover then -- downState
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
self.params.textcolordown, self.params.textscalexdown, self.params.textscaleydown,
 
self.params.textcolordown, self.params.textscalexdown, self.params.textscaleydown,
1.0, self.params.pixelcolordown, self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
+
self.params.pixelimgdown, self.params.pixelcolordown,
if self.ttiptext then self.ttiptext:setVisible(true) end
+
self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
else -- button is not focused
+
if self.ttiptf then
 +
self.ttiptf:setText(self.params.tooltiptext)
 +
self.ttiptf:setVisible(true)
 +
end
 +
else -- upState
 
visualState(self, self.params.btnscalexup, self.params.btnscaleyup, self.params.btnalphaup,
 
visualState(self, self.params.btnscalexup, self.params.btnscaleyup, self.params.btnalphaup,
 
self.params.textcolorup, self.params.textscalexup, self.params.textscaleyup,
 
self.params.textcolorup, self.params.textscalexup, self.params.textscaleyup,
0.0, self.params.pixelcolorup, self.params.pixelalphaup, self.params.pixelscalexup, self.params.pixelscaleyup)
+
self.params.pixelimgup, self.params.pixelcolorup,
if self.ttiptext then self.ttiptext:setVisible(false) end
+
self.params.pixelalphaup, self.params.pixelscalexup, self.params.pixelscaleyup)
 +
if self.ttiptf then self.ttiptf:setVisible(false) end
 
end
 
end
 
end
 
end
 
end
 
end
  
-- disabled
+
-- SOME UTILITY FUNCTIONS
 +
function ButtonMonster:changeText(xtext)
 +
self.params.text = xtext
 +
self:setButton()
 +
end
 +
function ButtonMonster:getText()
 +
return self.params.text
 +
end
 
function ButtonMonster:setDisabled(disabled)
 
function ButtonMonster:setDisabled(disabled)
 
if self.disabled == disabled then return end
 
if self.disabled == disabled then return end
Line 283: Line 264:
 
end
 
end
  
-- MOUSE LISTENERS
+
-- LISTENERS
function ButtonMonster:onMouseDown(ev)
+
-- MOUSE
if self.sprite:hitTestPoint(ev.x, ev.y, true) then
+
function ButtonMonster:onMouseDown(ev) -- both mouse and touch
-- you can dispatch mouse click event here
+
if self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
 +
self.focus = true
 +
if self.btns then -- update keyboard button id selector
 +
for k, v in ipairs(self.btns) do v.currselector = self.selector end
 +
end
 +
self:updateVisualState()
 +
-- self:selectionSfx() -- play sound fx, you choose!
 +
local e = Event.new("pressed")
 +
e.currselector = self.selector -- update button id selector
 +
e.disabled = self.disabled -- update button disabled
 +
self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
 +
-- if self.params.fun then self.params.fun(self:getParent()) end -- YOU CAN ADD THIS HERE
 +
if self.ttiptf then -- set tooltip initial position
 +
if self.tooltiplayer then
 +
self.ttiptf:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptf:setPosition(self:globalToLocal(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 +
end
 +
end
 
ev:stopPropagation()
 
ev:stopPropagation()
 
end
 
end
 
end
 
end
function ButtonMonster:onMouseUp(ev)
+
function ButtonMonster:onMouseMove(ev) -- both mouse and touch
if self.sprite:hitTestPoint(ev.x, ev.y, true) then
+
if self.focus then
 +
if self.ttiptf then -- tooltip follows position
 +
if self.tooltiplayer then
 +
self.ttiptf:setPosition(
 +
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 +
else
 +
self.ttiptf:setPosition(self:globalToLocal(
 +
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
 +
end
 +
end
 +
if not self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
 +
self.focus = false
 +
self:updateVisualState()
 +
end
 +
ev:stopPropagation()
 +
elseif self.ttiptf then -- reset tooltip text position
 +
if self.tooltiplayer then
 +
self.ttiptf:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptf:setPosition(self:globalToLocal(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 +
end
 +
end
 +
end
 +
function ButtonMonster:onMouseUp(ev) -- both mouse and touch
 +
if self.focus then
 +
self.focus = false
 +
self:updateVisualState()
 
local e = Event.new("clicked")
 
local e = Event.new("clicked")
 
e.currselector = self.selector -- update button id selector
 
e.currselector = self.selector -- update button id selector
 
e.disabled = self.disabled -- update button disabled
 
e.disabled = self.disabled -- update button disabled
 
self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
 
self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
 +
if self.params.fun then self.params.fun(self:getParent()) end -- OR EVEN HERE?
 
ev:stopPropagation()
 
ev:stopPropagation()
 
end
 
end
 
end
 
end
function ButtonMonster:onMouseHover(ev)
+
function ButtonMonster:onMouseHover(ev) -- mouse only
if self.sprite:hitTestPoint(ev.x, ev.y, true) then -- onenter
+
if self:hitTestPoint(ev.x, ev.y, true) then -- onenter
self.hovered = true
+
self.focus = true
self.ismouse = true
+
self.hover = true
if self.ttiptext then -- tooltip follows mouse position
+
if self.ttiptf then -- tooltip follows mouse position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
self.ttiptext:setPosition(
+
self.ttiptf:setPosition(
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 
else
 
else
self.ttiptext:setPosition(self.sprite:globalToLocal(
+
self.ttiptf:setPosition(self:globalToLocal(
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
 
end
 
end
Line 319: Line 349:
 
for k, v in ipairs(self.btns) do v.currselector = self.selector end
 
for k, v in ipairs(self.btns) do v.currselector = self.selector end
 
end
 
end
local e = Event.new("hovered") -- button is hovered, dispatch "hovered" event
+
local e = Event.new("hovered") -- dispatch "hovered" event
 
e.currselector = self.selector -- update button id selector
 
e.currselector = self.selector -- update button id selector
 +
e.disabled = self.disabled -- update button disabled
 
self:dispatchEvent(e)
 
self:dispatchEvent(e)
self:selectionSfx() -- play sound fx
+
self:selectionSfx() -- play sound fx? you choose!
 
-- trick to remove residuals when fast moving mouse
 
-- trick to remove residuals when fast moving mouse
local timer = Timer.new(100*1, 1) -- number of repetition, the higher the safer
+
local timer = Timer.new(100*1, 1) -- number of repetition, the higher the safer, (delay,repeatCount)
 
timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
 
timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
 
timer:start()
 
timer:start()
 +
-- if self.params.fun then self.params.fun(self:getParent()) end -- HERE COULD ALSO BE USEFUL?
 
else
 
else
 
self.onexit = true
 
self.onexit = true
Line 332: Line 364:
 
ev:stopPropagation()
 
ev:stopPropagation()
 
else -- onexit
 
else -- onexit
self.hovered = false
+
self.focus = false
 +
self.hover = false
 
self.onenter = false
 
self.onenter = false
 
self.moving = false
 
self.moving = false
if self.onexit then
+
if self.onexit then -- execute onexit code only once
-- execute onexit code only once
 
 
self.onexit = false
 
self.onexit = false
 
self:updateVisualState()
 
self:updateVisualState()
Line 343: Line 375:
 
end
 
end
  
-- audio
+
-- TOUCHES
 +
-- if button is on focus, stop propagation of touch events
 +
function ButtonMonster:onTouchesBegin(ev) -- touch only
 +
if self.focus then
 +
ev:stopPropagation()
 +
end
 +
end
 +
-- if button is on focus, stop propagation of touch events
 +
function ButtonMonster:onTouchesMove(ev) -- touch only
 +
if self.focus then
 +
ev:stopPropagation()
 +
end
 +
end
 +
-- if button is on focus, stop propagation of touch events
 +
function ButtonMonster:onTouchesEnd(ev) -- touch only
 +
if self.focus then
 +
ev:stopPropagation()
 +
elseif self.ttiptf then -- reset tooltip text position
 +
if self.tooltiplayer then
 +
self.ttiptf:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptf:setPosition(self:globalToLocal(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 +
end
 +
end
 +
end
 +
-- if touches are cancelled, reset the state of the button
 +
function ButtonMonster:onTouchesCancel(ev) -- app interrupted (phone call, ...), touch only
 +
if self.focus then
 +
self.focus = false
 +
if self.btns then -- update keyboard button id selector
 +
for k, v in ipairs(self.btns) do v.currselector = self.selector end
 +
end
 +
self:updateVisualState()
 +
elseif self.ttiptf then -- reset tooltip text position
 +
if self.tooltiplayer then
 +
self.ttiptf:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptf:setPosition(self:globalToLocal(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 +
end
 +
end
 +
end
 +
 
 +
-- AUDIO
 
function ButtonMonster:selectionSfx()
 
function ButtonMonster:selectionSfx()
 
if self.params.sound then
 
if self.params.sound then
Line 350: Line 428:
 
local prev = snd.time
 
local prev = snd.time
 
if curr - prev > snd.delay then
 
if curr - prev > snd.delay then
snd.sound:play():setVolume(self.params.volume)
+
local channel = snd.sound:play()
 +
if channel then
 +
channel:setVolume(self.params.volume)
 +
end
 
snd.time = curr
 
snd.time = curr
 
end
 
end
 
end
 
end
 +
end
 +
function ButtonMonster:setVolume(xvolume)
 +
self.params.volume = xvolume
 +
end
 +
function ButtonMonster:getVolume()
 +
return self.params.volume
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
Line 359: Line 446:
 
=== ButtonMonster '''Demos''' ===
 
=== ButtonMonster '''Demos''' ===
 
'''Demo 1'''
 
'''Demo 1'''
 +
 +
{{#widget:GApp|app=ButtonMonsterDemo1x.GApp|width=480|height=320}}
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
-- DEMO 1: keyboard and mouse navigation
+
-- ButtonMonster DEMO 1
 
-- bg
 
-- bg
application:setBackgroundColor(0x00007f)
+
application:setBackgroundColor(0x6c6c6c)
 
+
-- font
-- initial button selected
+
local myttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 20)
local selector = 1
+
-- textures
 +
local btnuptex = Texture.new("gfx/ui/btn_01_up.png")
 +
local btndowntex = Texture.new("gfx/ui/btn_01_down.png")
 +
local btndisabledtex = Texture.new("gfx/ui/btn_01_disabled.png")
 +
-- buttons sound
 +
local btnsound = {sound=Sound.new("audio/Braam - Retro Pulse.wav"), time=0, delay=0.5} -- delay=0.5, 0.05
 +
local volume = 0.3
 
-- buttons
 
-- buttons
local btn01 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 1)
+
local btn01 = ButtonMonster.new({
local btn02 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 2)
+
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
local btn03 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 3)
+
pixelscaleyup=0.7, pixelscaleydown=1,
local btn04 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 4)
+
text="button 1", ttf=myttf,
-- keyboard navigation
+
sound=btnsound, volume=volume,
local btns = {}
+
}, 1)
btns[#btns + 1] = btn01
+
local btn02 = ButtonMonster.new({
btns[#btns + 1] = btn02
+
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
btns[#btns + 1] = btn03
+
pixelscaleyup=0.7, pixelscaleydown=1,
btns[#btns + 1] = btn04
+
text="button 2", ttf=myttf,
 +
sound=btnsound, volume=volume,
 +
}, 2)
 +
local btn03 = ButtonMonster.new({
 +
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
pixelscaleyup=0.7, pixelscaleydown=1,
 +
text="button 3", ttf=myttf,
 +
sound=btnsound, volume=volume,
 +
}, 3)
 +
local btn04 = ButtonMonster.new({
 +
autoscale=false,
 +
pixelwidth=8*6, pixelheight=8*30,
 +
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
pixelscalexup=0.7, pixelscalexdown=1,
 +
pixelcolordown=0x00ff00,
 +
text="b\nt\nn\n \n4", ttf=myttf,
 +
sound=btnsound, volume=volume,
 +
}, 4)
 +
local btnexit = ButtonMonster.new({
 +
pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
 +
text="EXIT", ttf=myttf,
 +
})
 
-- position
 
-- position
 
btn01:setPosition(16*5, 16*3)
 
btn01:setPosition(16*5, 16*3)
btn02:setPosition(16*5, 16*6)
+
btn02:setPosition(16*5, 16*8)
btn03:setPosition(16*5, 16*9)
+
btn03:setPosition(16*5, 16*12.5)
btn04:setPosition(16*5, 16*12)
+
btn04:setPosition(16*12, 16*8)
 +
btnexit:setPosition(16*24, 16*16)
 
-- order
 
-- order
 
stage:addChild(btn01)
 
stage:addChild(btn01)
Line 387: Line 504:
 
stage:addChild(btn03)
 
stage:addChild(btn03)
 
stage:addChild(btn04)
 
stage:addChild(btn04)
 +
stage:addChild(btnexit)
  
-- listener function
+
-- add listeners
function clicked(input, btn)
+
function clicked(btn)
print(input .. " button " .. btn.currselector .. " clicked")
+
print(btn.currselector, btn.disabled)
end
+
if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled())
-- listeners
+
elseif btn.currselector == 5 then
for k, v in ipairs(btns) do
+
if not application:isPlayerMode() then application:exit()
v:addEventListener("clicked", clicked, "mouse", v)
+
else print("EXIT")
v:addEventListener("hovered", function(e) selector = e.currselector end) -- update button id selector
+
end
v.btns = btns -- list of navigatable buttons
 
end
 
 
 
-- keyboard handler (arrows + ENTER)
 
stage:addEventListener(Event.KEY_DOWN, function(e)
 
if e.keyCode == KeyCode.UP or e.keyCode == KeyCode.LEFT then
 
selector -= 1 if selector < 1 then selector = #btns end updateButton()
 
elseif e.keyCode == KeyCode.DOWN or e.keyCode == KeyCode.RIGHT then
 
selector += 1 if selector > #btns then selector = 1 end updateButton()
 
elseif e.keyCode == KeyCode.ENTER then
 
clicked("keyboard", btns[selector])
 
end
 
end)
 
function updateButton()
 
for k, v in ipairs(btns) do
 
v.currselector = selector
 
v:updateVisualState()
 
if k == selector then v:selectionSfx() end
 
 
end
 
end
 
end
 
end
 
+
btn01:addEventListener("clicked", clicked)
-- let's go!
+
btn02:addEventListener("clicked", clicked)
updateButton() -- highlight first button
+
btn03:addEventListener("clicked", clicked)
 +
btn04:addEventListener("clicked", clicked)
 +
btnexit:addEventListener("clicked", clicked)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
'''Demo 2'''
 
'''Demo 2'''
  
{{#widget:GApp|app=ButtonMonsterDemo2.GApp|width=480|height=320}}
+
Keyboard navigation (arrow keys + ENTER).
 +
 
 +
{{#widget:GApp|app=ButtonMonsterDemo2x.GApp|width=480|height=320}}
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
-- DEMO 2
+
-- ButtonMonster DEMO 2: keyboard navigation (arrow keys + ENTER)
 +
application:setBackgroundColor(0x6c6c6c)
 
-- a gradient bg
 
-- a gradient bg
 
local gradient = Pixel.new(0xffffff, 1, application:getContentWidth(), application:getContentHeight())
 
local gradient = Pixel.new(0xffffff, 1, application:getContentWidth(), application:getContentHeight())
Line 448: Line 553:
 
local btn01 = ButtonMonster.new({
 
local btn01 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
text="btn 1", ttf=myttf,
+
text="button 1", ttf=myttf,
tooltiptext="btn1", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*5,
+
tooltiptext="btn1", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 1, tooltiplayer)
 
}, 1, tooltiplayer)
 
local btn02 = ButtonMonster.new({
 
local btn02 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
text="btn 2", ttf=myttf,
+
text="button 2", ttf=myttf,
tooltiptext="click me!", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*1, tooltipoffsety=8*3,
+
tooltiptext="click me!", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 2, tooltiplayer)
 
}, 2, tooltiplayer)
 
local btn03 = ButtonMonster.new({
 
local btn03 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
text="btn 3", ttf=myttf,
+
text="button 3", ttf=myttf,
tooltiptext="btn3", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*5,
+
tooltiptext="btn3", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 3, tooltiplayer)
 
}, 3, tooltiplayer)
 
local btn04 = ButtonMonster.new({
 
local btn04 = ButtonMonster.new({
 
autoscale=false,
 
autoscale=false,
pixelwidth=8*3, pixelheight=8*24,
+
pixelwidth=8*5, pixelheight=8*24,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
ninepatch=32,
 
pixelcolordown=0x00ff00,
 
pixelcolordown=0x00ff00,
text="b\nt\nn\n4", ttf=myttf,
+
text="b\nt\nn\n \n4", ttf=myttf,
tooltiptext="btn4", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*4,
+
tooltiptext="btn4", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-4, tooltipoffsety=8*12,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 4, tooltiplayer)
 
}, 4, tooltiplayer)
 +
local btnexit = ButtonMonster.new({
 +
pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
 +
text="EXIT", ttf=myttf,
 +
}, 5, tooltiplayer)
 
-- keyboard navigation
 
-- keyboard navigation
 
local btns = {}
 
local btns = {}
Line 479: Line 589:
 
btns[#btns + 1] = btn03
 
btns[#btns + 1] = btn03
 
btns[#btns + 1] = btn04
 
btns[#btns + 1] = btn04
 +
btns[#btns + 1] = btnexit
 
-- position
 
-- position
 
gradient:setPosition(application:getContentWidth()/2, application:getContentHeight()/2)
 
gradient:setPosition(application:getContentWidth()/2, application:getContentHeight()/2)
Line 485: Line 596:
 
btn03:setPosition(16*5, 16*12.5)
 
btn03:setPosition(16*5, 16*12.5)
 
btn04:setPosition(16*12, 16*8)
 
btn04:setPosition(16*12, 16*8)
 +
btnexit:setPosition(16*24, 16*16)
 
-- order
 
-- order
 
stage:addChild(gradient)
 
stage:addChild(gradient)
stage:addChild(btn01)
+
for k, v in ipairs(btns) do
stage:addChild(btn02)
+
stage:addChild(v)
stage:addChild(btn03)
+
end
stage:addChild(btn04)
 
 
stage:addChild(tooltiplayer)
 
stage:addChild(tooltiplayer)
  
 
-- shared listener functions
 
-- shared listener functions
 
function clicked(input, btn)
 
function clicked(input, btn)
 +
selector = btn.currselector
 
print(input, btn.currselector, btn.disabled)
 
print(input, btn.currselector, btn.disabled)
if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled()) end
+
if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled())
 +
elseif btn.currselector == 5 then
 +
if not application:isPlayerMode() then application:exit()
 +
else print("EXIT")
 +
end
 +
end
 
end
 
end
 
-- add listeners
 
-- add listeners
 
for k, v in ipairs(btns) do
 
for k, v in ipairs(btns) do
 
v:addEventListener("clicked", clicked, "mouse", v)
 
v:addEventListener("clicked", clicked, "mouse", v)
v:addEventListener("hovered", function(e) selector = e.currselector end) -- update button id selector
+
v:addEventListener("hovered", function(e) selector = e.currselector end)
 
v.btns = btns -- list of navigatable buttons
 
v.btns = btns -- list of navigatable buttons
 
end
 
end
  
 
-- keyboard handler
 
-- keyboard handler
 +
function updateButton()
 +
for k, v in ipairs(btns) do
 +
v.currselector = selector
 +
v:updateVisualState()
 +
if k == selector then v:selectionSfx() end
 +
end
 +
end
 
stage:addEventListener(Event.KEY_DOWN, function(e)
 
stage:addEventListener(Event.KEY_DOWN, function(e)
 
if e.keyCode == KeyCode.UP or e.keyCode == KeyCode.LEFT then
 
if e.keyCode == KeyCode.UP or e.keyCode == KeyCode.LEFT then
Line 515: Line 639:
 
end
 
end
 
end)
 
end)
function updateButton()
 
for k, v in ipairs(btns) do
 
v.currselector = selector
 
v:updateVisualState()
 
if k == selector then v:selectionSfx() end
 
end
 
end
 
  
 
-- let's go!
 
-- let's go!

Latest revision as of 02:31, 9 November 2024

This ButtonMonster class allows you to build any button, from the simplest (text) to the most complicated (tooltip, keyboard navigation). It is optimised for menus and in games.

Media:buttonMonster.lua (tip: right click and save link as)

ButtonMonster Class

--[[
-- ButtonMonster
-- mokalux, cc0
-- Pixel, Image, 9patch, Text, Tooltip,
-- Up, Down, Disabled, Hover,
-- Sfx, Touch, Mouse and Keyboard navigation!
-- Functions
v 0.2.2: 2024-11-08 selectionSfx(): added channel checks before setting volume
v 0.2.1: 2024-04-22 added function callback (functions with no parameters for now!)
v 0.2.0: 2023-12-01 terminator, should be fine in games too now
v 0.1.0: 2021-06-01 total recall, this class has become a Monster! best used in menus but who knows?
v 0.0.1: 2020-03-28 init (based on the initial generic Gideros Button class)
]]

-- Class
ButtonMonster = Core.class(Sprite)

function ButtonMonster:init(xparams, xselector, xttlayer)
	-- user params
	self.params = xparams or {}
	-- add keyboard navigation?
	self.selector = xselector or nil -- button id selector
	self.btns = nil -- assign this value directly from your code (you assign it a list of buttons)
	-- add a layer for the tooltip?
	self.tooltiplayer = xttlayer or nil
	-- button params
	self.params.autoscale = xparams.autoscale or (xparams.autoscale == nil) -- bool (default = true)
	self.params.btnscalexup = xparams.btnscalexup or 1 -- number
	self.params.btnscaleyup = xparams.btnscaleyup or self.params.btnscalexup -- number
	self.params.btnscalexdown = xparams.btnscalexdown or self.params.btnscalexup -- number
	self.params.btnscaleydown = xparams.btnscaleydown or self.params.btnscaleyup -- number
	self.params.btnalphaup = xparams.btnalphaup or 1 -- number
	self.params.btnalphadown = xparams.btnalphadown or self.params.btnalphaup -- number
	-- pixel?
	self.params.pixelcolorup = xparams.pixelcolorup or 0xffffff -- color
	self.params.pixelcolordown = xparams.pixelcolordown or self.params.pixelcolorup -- color
	self.params.pixelcolordisabled = xparams.pixelcolordisabled or 0x555555 -- color
	self.params.pixelimgup = xparams.pixelimgup or nil -- img Up Texture
	self.params.pixelimgdown = xparams.pixelimgdown or self.params.pixelimgup -- img Down Texture
	self.params.pixelimgdisabled = xparams.pixelimgdisabled or self.params.pixelimgup -- img Disabled Texture
	self.params.pixelalphaup = xparams.pixelalphaup or 1 -- number
	self.params.pixelalphadown = xparams.pixelalphadown or self.params.pixelalphaup -- number
	self.params.pixelscalexup = xparams.pixelscalexup or 1 -- number
	self.params.pixelscaleyup = xparams.pixelscaleyup or self.params.pixelscalexup -- number
	self.params.pixelscalexdown = xparams.pixelscalexdown or self.params.pixelscalexup -- number
	self.params.pixelscaleydown = xparams.pixelscaleydown or self.params.pixelscaleyup -- number
	self.params.pixelwidth = xparams.pixelwidth or 24 -- 24, number (autoscale = x padding else width)
	self.params.pixelheight = xparams.pixelheight or self.params.pixelwidth -- number (autoscale = y padding else height)
	self.params.ninepatch = xparams.ninepatch or 16 -- 0, 8, number
	-- text?
	self.params.text = xparams.text or nil -- string
	self.params.ttf = xparams.ttf or nil -- ttf font
	self.params.textcolorup = xparams.textcolorup or 0x0 -- color
	self.params.textcolordown = xparams.textcolordown or self.params.textcolorup -- color
	self.params.textcolordisabled = xparams.textcolordisabled or 0x777777 -- color
	self.params.textalphaup = xparams.textalphaup or 1 -- number
	self.params.textalphadown = xparams.textalphaup or self.params.textalphaup -- number
	self.params.textscalexup = xparams.textscalexup or 1 -- number
	self.params.textscaleyup = xparams.textscaleyup or self.params.textscalexup -- number
	self.params.textscalexdown = xparams.textscalexdown or self.params.textscalexup -- number
	self.params.textscaleydown = xparams.textscaleydown or self.params.textscaleyup -- number
	-- tool tip?
	self.params.tooltiptext = xparams.tooltiptext or nil -- string
	self.params.tooltipttf = xparams.tooltipttf or nil -- ttf font
	self.params.tooltiptextcolor = xparams.tooltiptextcolor or 0x0 -- color
	self.params.tooltiptextscale = xparams.tooltiptextscale or 1 -- number
	self.params.tooltipoffsetx = xparams.tooltipoffsetx or 0 -- number
	self.params.tooltipoffsety = xparams.tooltipoffsety or 0 -- self.params.tooltipoffsetx -- number
	-- audio?
	self.params.sound = xparams.sound or nil -- sound fx
	self.params.volume = xparams.volume or 0.5 -- sound volume
	-- EXTRAS
	self.params.fun = xparams.fun or nil -- function (please check function name if not working!)
	-- set warnings, errors
	if self.params.sound and (type(self.params.sound) ~= "table" or self.params.sound.sound == nil) then
		print("*** ERROR ***", "ON BUTTON: "..self.selector or "?",
			"YOUR SOUND MUST BE A TABLE, SOMETHING LIKE: { sound=Sound.new(\"path/to/your/sound.wav\"), time=0, delay=0.2 }")
		self.params.sound = nil
	end
	if self.params.fun and type(self.params.fun) ~= "function" then
		print("*** ERROR ***", "ON BUTTON: "..self.selector or "?", "YOU ARE NOT PASSING A FUNCTION")
		if self.params.text then self.params.text = self.params.text.." (error)"
		else self.params.text = "error"
		end
		self.params.ttf = nil self.params.textscalexup = 3 self.params.textscaleyup = 3
		self.params.textcolorup = 0xff0000
	end
	-- let's go!
	self:setButton()
	-- update visual state
	self.focus = false
	self.hover = false
	self.disabled = false
	self:updateVisualState()
	-- mouse event listeners
	self:addEventListener(Event.MOUSE_DOWN, self.onMouseDown, self)
	self:addEventListener(Event.MOUSE_MOVE, self.onMouseMove, self)
	self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
	self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
	-- touch event listeners
	self:addEventListener(Event.TOUCHES_BEGIN, self.onTouchesBegin, self)
	self:addEventListener(Event.TOUCHES_MOVE, self.onTouchesMove, self)
	self:addEventListener(Event.TOUCHES_END, self.onTouchesEnd, self)
	self:addEventListener(Event.TOUCHES_CANCEL, self.onTouchesCancel, self)
end

-- FUNCTIONS
function ButtonMonster:setButton()
	-- text dimensions
	local textwidth, textheight
	if self.params.text then
		if self.tf then self:removeChild(self.tf) self.tf = nil end
		self.tf = TextField.new(self.params.ttf, self.params.text, self.params.text)
		self.tf:setAnchorPoint(0.5, 0.5)
		self.tf:setScale(self.params.textscalexup, self.params.textscaleyup)
		self.tf:setTextColor(self.params.textcolorup)
		self.tf:setAlpha(self.params.textalphaup)
		textwidth, textheight = self.tf:getWidth(), self.tf:getHeight()
	end
	-- first add pixel
	if self.pixel then self:removeChild(self.pixel) self.pixel = nil end
	if self.params.autoscale and self.params.text then
		self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
			textwidth+self.params.pixelwidth, textheight+self.params.pixelheight)
	else
		self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
			self.params.pixelwidth, self.params.pixelheight)
	end
	self.pixel:setScale(self.params.pixelscalexup, self.params.pixelscaleyup)
	self.pixel:setAnchorPoint(0.5, 0.5)
	self.pixel:setNinePatch(self.params.ninepatch)
	if self.params.pixelimgup then self.pixel:setTexture(self.params.pixelimgup)
	elseif self.params.pixelimgdown then self.pixel:setTexture(self.params.pixelimgdown)
	elseif self.params.pixelimgdisabled then self.pixel:setTexture(self.params.pixelimgdisabled)
	end
	self:addChild(self.pixel)
	-- then add text?
	if self.params.text then self:addChild(self.tf) end
	-- finally add tooltip?
	if self.params.tooltiptext then
		if self.ttiptf then
			if self.tooltiplayer then self.tooltiplayer:removeChild(self.ttiptf)
			else self:removeChild(self.ttiptf)
			end self.ttiptf = nil
		end
		self.ttiptf = TextField.new(self.params.tooltipttf, self.params.tooltiptext, self.params.tooltiptext)
		self.ttiptf:setScale(self.params.tooltiptextscale)
		self.ttiptf:setTextColor(self.params.tooltiptextcolor)
		self.ttiptf:setVisible(false)
		if self.tooltiplayer then self.tooltiplayer:addChild(self.ttiptf)
		else self:addChild(self.ttiptf)
		end
	end
end

function ButtonMonster:updateVisualState()
	local function visualState(btn, btnscalex, btnscaley, btnalpha, textcolor, textscalex, textscaley,
			pixeltex, pixelcolor, pixelalpha, pixelscalex, pixelscaley)
		btn:setScale(btnscalex, btnscaley)
		btn:setAlpha(btnalpha)
		if btn.params.text then
			btn.tf:setTextColor(textcolor)
			btn.tf:setScale(textscalex, textscaley)
		end
		if pixeltex then btn.pixel:setTexture(pixeltex) end
		btn.pixel:setColor(pixelcolor, pixelalpha)
		btn.pixel:setScale(pixelscalex, pixelscaley)
	end
	if self.btns then
--		print("KEYBOARD NAVIGATION")
		for k, v in ipairs(self.btns) do
			if v.disabled then -- disabledState
				visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
					v.params.textcolordisabled, v.params.textscalexdown, v.params.textscaleydown,
					v.params.pixelimgdisabled, v.params.pixelcolordisabled,
					v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
--				if v.ttiptf and not v.disabled then -- OPTION 1: hides tooltip when button is Disabled
				if v.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
					v.ttiptf:setText("("..v.params.tooltiptext..")") -- extra!
					if k == v.currselector then v.ttiptf:setVisible(true)
					else v.ttiptf:setVisible(false)
					end
				end
			elseif k == v.currselector then -- downState
				visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
					v.params.textcolordown, v.params.textscalexdown, v.params.textscaleydown,
					v.params.pixelimgdown, v.params.pixelcolordown,
					v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
				if v.ttiptf then
					v.ttiptf:setText(v.params.tooltiptext)
					if v.tooltiplayer then -- reset tooltip text position
						v.ttiptf:setPosition(
							v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety)
					else
						v.ttiptf:setPosition(v:globalToLocal(
							v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety))
					end
					v.ttiptf:setVisible(true)
				end
			else -- upState
				visualState(v, v.params.btnscalexup, v.params.btnscaleyup, v.params.btnalphaup,
					v.params.textcolorup, v.params.textscalexup, v.params.textscaleyup,
					v.params.pixelimgup, v.params.pixelcolorup,
					v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
				if v.ttiptf then v.ttiptf:setVisible(false) end
			end
		end
	else
--		print("TOUCH, MOUSE NAVIGATION")
		if self.disabled then -- disabledState
			visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
				self.params.textcolordisabled, self.params.textscalexdown, self.params.textscaleydown,
				self.params.pixelimgdisabled, self.params.pixelcolordisabled,
				self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
--			if self.ttiptf and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
			if self.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
				self.ttiptf:setText("("..self.params.tooltiptext..")") -- extra!
				if self.focus then self.ttiptf:setVisible(true)
				else self.ttiptf:setVisible(false)
				end
			end
		elseif self.focus or self.hover then -- downState
			visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
				self.params.textcolordown, self.params.textscalexdown, self.params.textscaleydown,
				self.params.pixelimgdown, self.params.pixelcolordown,
				self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
			if self.ttiptf then
				self.ttiptf:setText(self.params.tooltiptext)
				self.ttiptf:setVisible(true)
			end
		else -- upState
			visualState(self, self.params.btnscalexup, self.params.btnscaleyup, self.params.btnalphaup,
				self.params.textcolorup, self.params.textscalexup, self.params.textscaleyup,
				self.params.pixelimgup, self.params.pixelcolorup,
				self.params.pixelalphaup, self.params.pixelscalexup, self.params.pixelscaleyup)
			if self.ttiptf then self.ttiptf:setVisible(false) end
		end
	end
end

-- SOME UTILITY FUNCTIONS
function ButtonMonster:changeText(xtext)
	self.params.text = xtext
	self:setButton()
end
function ButtonMonster:getText()
	return self.params.text
end
function ButtonMonster:setDisabled(disabled)
	if self.disabled == disabled then return end
	self.disabled = disabled
	self:updateVisualState()
end
function ButtonMonster:getDisabled()
	return self.disabled
end

-- LISTENERS
-- MOUSE
function ButtonMonster:onMouseDown(ev) -- both mouse and touch
	if self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
		self.focus = true
		if self.btns then -- update keyboard button id selector
			for k, v in ipairs(self.btns) do v.currselector = self.selector end
		end
		self:updateVisualState()
--		self:selectionSfx() -- play sound fx, you choose!
		local e = Event.new("pressed")
		e.currselector = self.selector -- update button id selector
		e.disabled = self.disabled -- update button disabled
		self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
--		if self.params.fun then self.params.fun(self:getParent()) end -- YOU CAN ADD THIS HERE
		if self.ttiptf then -- set tooltip initial position
			if self.tooltiplayer then
				self.ttiptf:setPosition(
					self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
			else
				self.ttiptf:setPosition(self:globalToLocal(
					self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
			end
		end
		ev:stopPropagation()
	end
end
function ButtonMonster:onMouseMove(ev) -- both mouse and touch
	if self.focus then
		if self.ttiptf then -- tooltip follows position
			if self.tooltiplayer then
				self.ttiptf:setPosition(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
			else
				self.ttiptf:setPosition(self:globalToLocal(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
			end
		end
		if not self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
			self.focus = false
			self:updateVisualState()
		end
		ev:stopPropagation()
	elseif self.ttiptf then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptf:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptf:setPosition(self:globalToLocal(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
		end
	end
end
function ButtonMonster:onMouseUp(ev) -- both mouse and touch
	if self.focus then
		self.focus = false
		self:updateVisualState()
		local e = Event.new("clicked")
		e.currselector = self.selector -- update button id selector
		e.disabled = self.disabled -- update button disabled
		self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
		if self.params.fun then self.params.fun(self:getParent()) end -- OR EVEN HERE?
		ev:stopPropagation()
	end
end
function ButtonMonster:onMouseHover(ev) -- mouse only
	if self:hitTestPoint(ev.x, ev.y, true) then -- onenter
		self.focus = true
		self.hover = true
		if self.ttiptf then -- tooltip follows mouse position
			if self.tooltiplayer then
				self.ttiptf:setPosition(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
			else
				self.ttiptf:setPosition(self:globalToLocal(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
			end
		end
		-- execute onenter code only once
		self.onenter = not self.onenter
		if not self.onenter then self.moving = true end
		if not self.moving then
			if self.btns then -- update keyboard button id selector
				for k, v in ipairs(self.btns) do v.currselector = self.selector end
			end
			local e = Event.new("hovered") -- dispatch "hovered" event
			e.currselector = self.selector -- update button id selector
			e.disabled = self.disabled -- update button disabled
			self:dispatchEvent(e)
			self:selectionSfx() -- play sound fx? you choose!
			-- trick to remove residuals when fast moving mouse
			local timer = Timer.new(100*1, 1) -- number of repetition, the higher the safer, (delay,repeatCount)
			timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
			timer:start()
--			if self.params.fun then self.params.fun(self:getParent()) end -- HERE COULD ALSO BE USEFUL?
		else
			self.onexit = true
		end
		ev:stopPropagation()
	else -- onexit
		self.focus = false
		self.hover = false
		self.onenter = false
		self.moving = false
		if self.onexit then -- execute onexit code only once
			self.onexit = false
			self:updateVisualState()
		end
	end
end

-- TOUCHES
-- if button is on focus, stop propagation of touch events
function ButtonMonster:onTouchesBegin(ev) -- touch only
	if self.focus then
		ev:stopPropagation()
	end
end
-- if button is on focus, stop propagation of touch events
function ButtonMonster:onTouchesMove(ev) -- touch only
	if self.focus then
		ev:stopPropagation()
	end
end
-- if button is on focus, stop propagation of touch events
function ButtonMonster:onTouchesEnd(ev) -- touch only
	if self.focus then
		ev:stopPropagation()
	elseif self.ttiptf then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptf:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptf:setPosition(self:globalToLocal(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
		end
	end
end
-- if touches are cancelled, reset the state of the button
function ButtonMonster:onTouchesCancel(ev) -- app interrupted (phone call, ...), touch only
	if self.focus then
		self.focus = false
		if self.btns then -- update keyboard button id selector
			for k, v in ipairs(self.btns) do v.currselector = self.selector end
		end
		self:updateVisualState()
	elseif self.ttiptf then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptf:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptf:setPosition(self:globalToLocal(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
		end
	end
end

-- AUDIO
function ButtonMonster:selectionSfx()
	if self.params.sound then
		local snd = self.params.sound
		local curr = os.timer()
		local prev = snd.time
		if curr - prev > snd.delay then
			local channel = snd.sound:play()
			if channel then
				channel:setVolume(self.params.volume)
			end
			snd.time = curr
		end
	end
end
function ButtonMonster:setVolume(xvolume)
	self.params.volume = xvolume
end
function ButtonMonster:getVolume()
	return self.params.volume
end

ButtonMonster Demos

Demo 1

-- ButtonMonster DEMO 1
-- bg
application:setBackgroundColor(0x6c6c6c)
-- font
local myttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 20)
-- textures
local btnuptex = Texture.new("gfx/ui/btn_01_up.png")
local btndowntex = Texture.new("gfx/ui/btn_01_down.png")
local btndisabledtex = Texture.new("gfx/ui/btn_01_disabled.png")
-- buttons sound
local btnsound = {sound=Sound.new("audio/Braam - Retro Pulse.wav"), time=0, delay=0.5} -- delay=0.5, 0.05
local volume = 0.3
-- buttons
local btn01 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscaleyup=0.7, pixelscaleydown=1,
	text="button 1", ttf=myttf,
	sound=btnsound, volume=volume,
}, 1)
local btn02 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscaleyup=0.7, pixelscaleydown=1,
	text="button 2", ttf=myttf,
	sound=btnsound, volume=volume,
}, 2)
local btn03 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscaleyup=0.7, pixelscaleydown=1,
	text="button 3", ttf=myttf,
	sound=btnsound, volume=volume,
}, 3)
local btn04 = ButtonMonster.new({
	autoscale=false,
	pixelwidth=8*6, pixelheight=8*30,
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscalexup=0.7, pixelscalexdown=1,
	pixelcolordown=0x00ff00,
	text="b\nt\nn\n \n4", ttf=myttf,
	sound=btnsound, volume=volume,
}, 4)
local btnexit = ButtonMonster.new({
	pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
	text="EXIT", ttf=myttf,
})
-- position
btn01:setPosition(16*5, 16*3)
btn02:setPosition(16*5, 16*8)
btn03:setPosition(16*5, 16*12.5)
btn04:setPosition(16*12, 16*8)
btnexit:setPosition(16*24, 16*16)
-- order
stage:addChild(btn01)
stage:addChild(btn02)
stage:addChild(btn03)
stage:addChild(btn04)
stage:addChild(btnexit)

-- add listeners
function clicked(btn)
	print(btn.currselector, btn.disabled)
	if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled())
	elseif btn.currselector == 5 then
		if not application:isPlayerMode() then application:exit()
		else print("EXIT")
		end
	end
end
btn01:addEventListener("clicked", clicked)
btn02:addEventListener("clicked", clicked)
btn03:addEventListener("clicked", clicked)
btn04:addEventListener("clicked", clicked)
btnexit:addEventListener("clicked", clicked)

Demo 2

Keyboard navigation (arrow keys + ENTER).

-- ButtonMonster DEMO 2: keyboard navigation (arrow keys + ENTER)
application:setBackgroundColor(0x6c6c6c)
-- a gradient bg
local gradient = Pixel.new(0xffffff, 1, application:getContentWidth(), application:getContentHeight())
gradient:setColor(0x0, 1, 0xaa5500, 1, 15*16)
gradient:setAnchorPoint(0.5, 0.5)

-- font
local myttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 20)
local myttipttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 18)
-- textures
local btnuptex = Texture.new("gfx/ui/btn_01_up.png")
local btndowntex = Texture.new("gfx/ui/btn_01_down.png")
local btndisabledtex = Texture.new("gfx/ui/btn_01_disabled.png")
-- buttons tooltip layer
local tooltiplayer = Sprite.new()
-- buttons sound
local btnsound = {sound=Sound.new("audio/Braam - Retro Pulse.wav"), time=0, delay=0.5} -- delay=0.5, 0.05
local volume = 0.3
-- initial button selected
local selector = 1
-- buttons
local btn01 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 1", ttf=myttf,
	tooltiptext="btn1", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
	sound=btnsound, volume=volume,
}, 1, tooltiplayer)
local btn02 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 2", ttf=myttf,
	tooltiptext="click me!", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
	sound=btnsound, volume=volume,
}, 2, tooltiplayer)
local btn03 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 3", ttf=myttf,
	tooltiptext="btn3", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
	sound=btnsound, volume=volume,
}, 3, tooltiplayer)
local btn04 = ButtonMonster.new({
	autoscale=false,
	pixelwidth=8*5, pixelheight=8*24,
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	ninepatch=32,
	pixelcolordown=0x00ff00,
	text="b\nt\nn\n \n4", ttf=myttf,
	tooltiptext="btn4", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-4, tooltipoffsety=8*12,
	sound=btnsound, volume=volume,
}, 4, tooltiplayer)
local btnexit = ButtonMonster.new({
	pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
	text="EXIT", ttf=myttf,
}, 5, tooltiplayer)
-- keyboard navigation
local btns = {}
btns[#btns + 1] = btn01
btns[#btns + 1] = btn02
btns[#btns + 1] = btn03
btns[#btns + 1] = btn04
btns[#btns + 1] = btnexit
-- position
gradient:setPosition(application:getContentWidth()/2, application:getContentHeight()/2)
btn01:setPosition(16*5, 16*3)
btn02:setPosition(16*5, 16*8)
btn03:setPosition(16*5, 16*12.5)
btn04:setPosition(16*12, 16*8)
btnexit:setPosition(16*24, 16*16)
-- order
stage:addChild(gradient)
for k, v in ipairs(btns) do
	stage:addChild(v)
end
stage:addChild(tooltiplayer)

-- shared listener functions
function clicked(input, btn)
	selector = btn.currselector
	print(input, btn.currselector, btn.disabled)
	if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled())
	elseif btn.currselector == 5 then
		if not application:isPlayerMode() then application:exit()
		else print("EXIT")
		end
	end
end
-- add listeners
for k, v in ipairs(btns) do
	v:addEventListener("clicked", clicked, "mouse", v)
	v:addEventListener("hovered", function(e) selector = e.currselector end)
	v.btns = btns -- list of navigatable buttons
end

-- keyboard handler
function updateButton()
	for k, v in ipairs(btns) do
		v.currselector = selector
		v:updateVisualState()
		if k == selector then v:selectionSfx() end
	end
end
stage:addEventListener(Event.KEY_DOWN, function(e)
	if e.keyCode == KeyCode.UP or e.keyCode == KeyCode.LEFT then
		selector -= 1 if selector < 1 then selector = #btns end updateButton()
	elseif e.keyCode == KeyCode.DOWN or e.keyCode == KeyCode.RIGHT then
		selector += 1 if selector > #btns then selector = 1 end updateButton()
	elseif e.keyCode == KeyCode.ENTER then
		clicked("keyboard", btns[selector])
	end
end)

-- let's go!
updateButton() -- highlight first button


UI Buttons