Files
2014-09-30 19:16:21 -06:00

493 lines
20 KiB
Lua

-- This is an actor class that implements functionality for having a numpad
-- that is used to enter a number.
-- The parameters for customizing its appearance will be discussed here. For
-- an example, see Docs/ThemerDocs/Examples/Example_Actors/NumPadEntry.lua
-- This actor handles input, so it has to be used a bit differently from
-- normal actors. Instead of just putting it inside an ActorFrame like
-- normal, you must follow these steps:
-- 1. Call the function "new_numpad_entry" and store the result in a local
-- variable. This creates the NumPadEntry that will handle the logic and
-- input of entering a number. Ex:
-- local entry_pad= new_numpad_entry(params)
-- 2. Call the "create_actors" function of the NumPadEntry you created and
-- put the actors it returns inside your ActorFrame. Ex:
-- Def.ActorFrame{ entry_pad:create_actors(params) }
-- 3. Set up an input callback for recieving input and pass it to the
-- NumPadEntry you created. Check the return value of the handle_input
-- function to see when the player has finished entering the number. Ex:
-- local function input(event)
-- if event.type ~= "InputEventType_Release" then
-- local entry_done= entry_pad:handle_input(event.button)
-- end
-- end
-- 4. If handle_input returns true, the player has finished entering the
-- number and you can do something with the value. Each NumPadEntry sets
-- its "done" field to true when the player hits the done button. Ex:
-- -- (continued from above)
-- if entry_done then
-- Trace("Player entered value: " .. entry_pad.value)
-- end
-- Params explanation:
--
-- "params" is a table containing the parameters used to customize
-- NumPadEntry.
--
-- Almost all parameters are optional except for the Name.
--
-- Some parameters are interdependent, so if you include one, you should
-- include the ones it is interdependent with to make sure they make sense
-- together.
--
-- The parameters can be passed to either new_numpad_entry() or to
-- create_actors().
--
-- Parameters passed to create_actors() are combined with the parameters
-- passed to new_numpad_entry().
--
-- If the same parameter is passed to both, the one passed to create_actors()
-- overrides the one passed to new_numpad_entry().
--
-- NumPadEntry is like an ActorFrame, you can put actors in params and they
-- will be inside it, drawn before (under) everything else.
--
-- You can also include custom commands for the NumPadEntry actor and they
-- will be part of the actor returned by create_actors().
--
-- Some parameters are custom actors that fill a role. They must support
-- certain commands that will be used to carry out their role.
-- Parameter listing:
-- {
-- Name= "foo", -- A unique string you will recognize later.
--
-- -- button_positions, rows, and columns are used to positioning the
-- -- buttons and moving the cursor. Each set of "rows" number of entries
-- -- in button_positions will be considered one row, and the cursor will
-- -- be wrapped to the beginning/end of the row or moved to a new row or
-- -- column as appropriate when input is handled.
--
-- -- Optional. A table of positions for the buttons. Each position is a
-- -- table containing the x, y, and optional z of that button.
-- button_positions= {{x, y, z}, {x, y, z}, ...},
-- rows= 4, -- Optional. The number of rows on the pad.
-- columns= 4, -- Optional. The number of columns on the pad.
--
-- -- Optional. The button that the cursor starts on. The default is the
-- -- button in the middle of the pad.
-- cursor_pos= 5,
--
-- done_text= "&start;", -- Optional. The text used for the "Done" button.
-- back_text= "←", -- Optional. The text used for the
-- -- "Backspace" button.
--
-- -- Optional. The values of the buttons. One of them should have
-- -- done_text as a value and one should have back_text as a value. The
-- -- default is for a standard 12 button numpad, numbers 0-9, done, and
-- -- backspace.
-- button_values= {7, 8, 9, 4, 5, 6, 1, 2, 3, 0, done_text, back_text},
--
-- -- button_positions, button_values, rows, columns, and cursor_pos are
-- -- all interdependent. If you pass one, you should pass them all.
--
-- -- Optional. The amount to multiply by when adding a digit.
-- digit_scale= 10,
-- -- Optional. If the player tries to set the value above this amount,
-- -- InvalidValueCommand will be played on the actor. Default allows any
-- -- value.
-- max_value= 300,
-- -- Optional. When the current value is above this, the cursor will be
-- -- moved to the done button. Default is to never automatically move the
-- -- cursor.
-- auto_done_value= 100,
--
-- -- Optional. The various default actors will use this font if a
-- -- specific for the actor is not passed in and they are not replaced by
-- -- custom actors. Default is "Common Normal".
-- Font= "Common Normal",
--
-- -- Commands: These are some commands that are recommended, but not
-- -- required.
--
-- -- Optional. You can pass an InitCommand to set the position and stuff.
-- InitCommand= function() end,
--
-- -- Optional. InvalidValueCommand will be executed when the player tries
-- -- to enter an invalid value. params is a table containing the value.
-- -- The default plays the common invalid sound.
-- InvalidValueCommand= function(self, params) Trace(params[1]) end,
--
-- -- Optional. EntryDoneCommand will be executed when the player presses
-- -- the done button. params is a table containing the value. The
-- -- default does nothing.
-- EntryDoneCommand= function(self, params) Trace(params[1]) end,
--
-- -- Actors: These are the optional custom actors you can provide to
-- -- change the appearance of each part of the numpad.
-- -- Each of them has a default that can be customized in minor ways, or
-- -- you can provide a full actor to replace the default.
--
-- -- Optional. The color the default cursor will use. Unused if you pass
-- -- in a custom cursor.
-- cursor_color= Color.black,
-- -- Optional. The actor used for the cursor. Should have the Move and
-- -- Fit commands. Default is a simple quad that moves and changes its
-- -- width.
-- -- When a cursor movement occurs, MoveCommand will be executed, followed
-- -- by FitCommand. Both are executed by playcommand so they can be
-- -- passed parameters. Default Move and Fit commands do nothing.
-- cursor= Def.Quad{
-- -- param is the position to move to. Be sure to avoid a tween
-- -- overflow.
-- MoveCommand= function(self, param) end,
-- -- param is the actor for the button the cursor is moving to.
-- FitCommand= function(self, param) end,
-- },
-- -- Required. Where the cursor should be placed in the ActorFrame of the
-- -- numpad. "first" means the cursor will be placed first, and thus
-- -- under all the buttons. "last" means the cursor will be placed last,
-- -- and thus above all the buttons. nil means no cursor.
-- cursor_draw= "first",
--
-- -- Optional. The font used by the default button actor. Unused if you
-- -- pass in a custom button actor.
-- button_font= "Common Normal",
-- -- Optional. The color used by the default button actor. Unused if you
-- -- pass in a custom button actor. Default is White.
-- button_color= Color.White,
-- -- Optional. A template for the actor that will be used for each button.
-- -- This template will be duplicated for each button.
-- -- Do not provide a name in the button template, each button will be
-- -- named uniquely by the numpad like this: Name= "num"..index
-- -- Do not attempt to do any positioning in the InitCommand for your
-- -- button actor, positioning will be handlded by the numpad, using the
-- -- positions provided in button_positions.
-- -- GainFocus, LoseFocus, and Press are optional commands for handling
-- -- their respective events.
-- button= Def.BitmapText{
-- InitCommand= cmd(diffuse, Color.White),
-- -- SetCommand is executed after InitCommand to set the value for the
-- -- button. param is a table containing a value from button_values.
-- SetCommand= function(self, param)
-- self:settext(param[1])
-- end,
-- -- GainFocusCommand is executed when the cursor moves onto the button.
-- GainFocusCommand= cmd(diffuse, Color.Red),
-- -- LoseFocusCommand is executed when the cursor moves off the button.
-- LoseFocusCommand= cmd(diffuse, Color.White),
-- -- PressCommand is executed when the button is pressed.
-- PressCommand= cmd(stoptweening; linear,.1; zoom,2; linear.1; zoom,1)
-- },
--
-- -- Optional. The position to place the default value text at. Position
-- -- is a table of x, y, and optional z. Unused if you pass in a custom
-- -- value actor.
-- value_pos= {0, -48},
-- -- Optional. The font to use for the default value actor.
-- value_font= "Common Normal",
-- -- Optional. The color for the default value text. Unused if you pass
-- -- in a custom value actor. Default is White.
-- value_color= Color.White,
-- -- Optional. The actor used for displaying the value the player has
-- -- entered so far. Default is a simple BitmapText.
-- value= Def.BitmapText{
-- Font= "Common Normal",
-- -- InitCommand should set the position you want if you pass a custom
-- -- actor.
-- InitCommand= cmd(xy, 0, -48),
-- -- SetCommand will be executed when a button is pressed and the value
-- -- is changed. param is a table containing the new value.
-- SetCommand= function(self, param) self:settext(param[1]) end
-- },
--
-- -- Optional. The position to place the default prompt actor at.
-- -- Position is a table of x, y, and optional z.
-- prompt_pos= {0, -72},
-- -- Optional. The font to use for the default prompt actor.
-- prompt_font= "Common Normal",
-- -- Optional. The color used by the default prompt actor.
-- prompt_color= Color.White,
-- -- Optional. The text used by the default prompt actor. Default is "".
-- prompt_text= "",
-- -- Optional. The actor to use for displaying the prompt. Any actor you
-- -- want. It should support a Set command if the NumPadEntry is on a
-- -- screen where it will be reused for different numbers.
-- prompt= Def.BitmapText{
-- Font= "Common Normal",
-- -- InitCommand should set the position you want if you pass a custom
-- -- actor.
-- InitCommand= cmd(xy, 0, -48),
-- -- SetCommand will be executed when a button is pressed and the value
-- -- is changed. param is a table containing the new value.
-- SetCommand= function(self, param) self:settext(param[1]) end
-- },
-- }
local function noop() end
local function add_default_commands_to_actor(default_set, actor)
for i, command_name in ipairs(default_set) do
if not actor[command_name] then actor[command_name]= noop end
end
end
local function pos_to_cr(pos, columns)
return {((pos-1) % columns)+1, math.ceil(pos / columns)}
end
local function cr_to_pos(cr, columns)
return ((cr[2] - 1) * columns) + (cr[1])
end
local numpad_entry_mt= {
__index= {
init= function(self, params)
self.init_params= params
return self
end,
create_actors= function(self, params)
params= params or {}
if self.init_params then
for name, param in pairs(self.init_params) do
if not params[name] then params[name]= param end
end
end
if not params.Name then
error("NumPadEntry(" .. self.name .. "): Every actor should have a Name.")
end
self.name= params.Name
self.button_poses= params.button_positions or
{{-24, -24}, {0, -24}, {24, -24},
{-24, 0}, {0, 0}, {24, 0},
{-24, 24}, {0, 24}, {24, 24},
{-24, 48}, {0, 48}, {24, 48}}
self.rows= params.rows or 4
self.columns= params.columns or 3
if #self.button_poses ~= self.rows * self.columns then
error("NumpadEntry(" .. self.name .. "): Number of buttons does not match rows * columns.")
end
local default_start= cr_to_pos(
{math.ceil(self.columns/2), math.ceil(self.rows/2)}, self.columns)
self.cursor_start= params.cursor_start or default_start
self.cursor_pos= self.cursor_start
self.done_text= params.done_text or "&start;"
self.back_text= params.back_text or "←"
self.button_values= params.button_values or
{7, 8, 9, 4, 5, 6, 1, 2, 3, 0, self.done_text, self.back_text}
if #self.button_values ~= #self.button_poses then
error("NumpadEntry(" .. self.name .. "): Number of button values does not match number of button positions.")
end
self.value= 0
self.digit_scale= params.digit_scale or 10
self.max_value= params.max_value
self.auto_done_value= params.auto_done_value
if MonthOfYear() == 3 and DayOfMonth() == 1 and PREFSMAN:GetPreference("EasterEggs") then
for i= 1, #self.button_values do
local a= math.random(1, #self.button_values)
local b= math.random(1, #self.button_values)
self.button_values[a], self.button_values[b]= self.button_values[b], self.button_values[a]
end
end
self.done_pos= (self.rows * self.columns) - 1
for i, val in ipairs(self.button_values) do
if val == self.done_text then
self.done_pos= i
break
end
end
local args= {
Name= self.name, InitCommand= function(subself)
(params.InitCommand or noop)(subself)
self:update_cursor()
self.container= subself
end
}
for i, actor in ipairs(params) do
args[#args+1]= actor
end
for name, command in pairs(params) do
if type(name) == "string" and name:find("Command") and not args[name] then
args[name]= command
end
end
if not args.InvalidValueCommand then
args.InvalidValueCommand= function(subself)
SOUND:PlayOnce(THEME:GetPathS("Common", "Invalid"))
end
end
add_default_commands_to_actor({"EntryDoneCommand"}, args)
local default_cursor= Def.Quad{
Name= "cursor", InitCommand= cmd(setsize, 16, 24; diffuse, params.cursor_color or Color.Black),
MoveCommand= function(subself, param)
subself:stoptweening()
subself:linear(.1)
subself:xy(param[1], param[2])
if param[3] then subself:z(param[3]) end
end,
FitCommand= function(subself, param)
subself:SetWidth(param:GetWidth())
end
}
local cursor_template= params.cursor or default_cursor
local cursor_init= cursor_template.InitCommand or noop
cursor_template.InitCommand= function(subself)
self.cursor= subself
cursor_init(subself)
end
add_default_commands_to_actor({"FitCommand", "MoveCommand"}, cursor_template)
if params.cursor_draw == "first" then
args[#args+1]= cursor_template
end
self.button_actors= {}
local default_bat_commands= {
"GainFocusCommand", "LoseFocusCommand", "PressCommand"}
local default_bat= Def.BitmapText{
Font= params.button_font or params.Font or "Common Normal",
InitCommand= cmd(diffuse, params.button_color or Color.White),
SetCommand= function(subself, param)
subself:settext(param[1])
end
}
local ba_template= params.button or default_bat
add_default_commands_to_actor(default_bat_commands, ba_template)
local bainit= ba_template.InitCommand or noop
for i, pos in ipairs(self.button_poses) do
local actor= DeepCopy(ba_template)
actor.InitCommand= function(subself)
self.button_actors[i]= subself
subself:xy(pos[1], pos[2])
if pos[3] then subself:z(pos[3]) end
bainit(subself)
subself:playcommand("Set", {self.button_values[i]})
end
actor.Name= "num"..i
args[#args+1]= actor
end
local vat_pos= params.value_pos or {0, -48}
local default_vat= Def.BitmapText{
Name= "value",
Font= params.value_font or params.Font or "Common Normal",
InitCommand= function(subself)
subself:xy(vat_pos[1], vat_pos[2])
if vat_pos[3] then subself:z(vat_pos[3]) end
subself:diffuse(params.value_color or Color.White)
end,
SetCommand= function(subself, param) subself:settext(param[1]) end}
local va_template= params.value or default_vat
add_default_commands_to_actor({"SetCommand"}, va_template)
local vainit= va_template.InitCommand or noop
va_template.InitCommand= function(subself)
self.value_actor= subself
vainit(subself)
subself:playcommand("Set", {self.value})
end
args[#args+1]= va_template
local prompt_pos= params.prompt_pos or {0, -72}
local default_prompt= Def.BitmapText{
Name= "prompt",
Font= params.prompt_font or params.Font or "Common Normal",
InitCommand= cmd(xy, prompt_pos[1], prompt_pos[2];
diffuse, params.prompt_color or Color.White),
Text= params.prompt_text or "",
SetCommand= function(subself, param) subself:settext(param[1]) end}
local prompt_template= params.prompt or default_prompt
local prompt_init= prompt_template.InitCommand or noop
add_default_commands_to_actor({"SetCommand"}, prompt_template)
prompt_template.InitCommand= function(subself)
self.prompt_actor= subself
prompt_init(subself)
end
args[#args+1]= prompt_template
if params.cursor_draw == "last" then
args[#args+1]= cursor_template
end
return Def.ActorFrame(args)
end,
update_cursor= function(self, new_pos)
if new_pos then
self.button_actors[self.cursor_pos]:playcommand("LoseFocus")
self.cursor_pos= new_pos
end
self.button_actors[self.cursor_pos]:playcommand("GainFocus")
if not self.cursor then return end
self.cursor:playcommand("Move", self.button_poses[self.cursor_pos])
self.cursor:playcommand("Fit", self.button_actors[self.cursor_pos])
end,
handle_input= function(self, button)
if button == "Start" then
self.button_actors[self.cursor_pos]:playcommand("Press")
local num= self.button_values[self.cursor_pos]
local as_num= tonumber(num)
if as_num then
local new_value= (self.value * self.digit_scale) + as_num
if self.max_value and new_value > self.max_value then
self.container:playcommand("InvalidValue", {new_value})
else
self.value= new_value
if self.auto_done_value and new_value > self.auto_done_value then
self:update_cursor(self.done_pos)
end
self.value_actor:playcommand("Set", {self.value})
end
else
if num == self.done_text then
self.container:playcommand("EntryDone", {self.value})
self.done= true
return true
elseif num == self.back_text then
self.value= math.floor(self.value / self.digit_scale)
self.value_actor:playcommand("Set", {self.value})
end
end
else
local cr_pos= pos_to_cr(self.cursor_pos, self.columns)
local button_motions= {
Left= {-1, 0}, Right= {1, 0}, Up= {0, -1}, Down= {0, 1}}
button_motions.MenuLeft= button_motions.Left
button_motions.MenuRight= button_motions.Right
button_motions.MenuUp= button_motions.Up
button_motions.MenuDown= button_motions.Down
local motion= button_motions[button] -- Come on, do the loca-motion!
if motion then
cr_pos[1]= cr_pos[1] + motion[1]
cr_pos[2]= cr_pos[2] + motion[2]
if cr_pos[1] < 1 then
cr_pos[1]= self.columns
if button == "MenuLeft" then
cr_pos[2]= cr_pos[2] - 1
end
end
if cr_pos[1] > self.columns then
cr_pos[1]= 1
if button == "MenuRight" then
cr_pos[2]= cr_pos[2] + 1
end
end
if cr_pos[2] < 1 then cr_pos[2]= self.rows end
if cr_pos[2] > self.rows then cr_pos[2]= 1 end
self:update_cursor(cr_to_pos(cr_pos, self.columns))
end
end
end
}}
function new_numpad_entry(params)
return setmetatable({}, numpad_entry_mt):init(params)
end
--[[
Copyright © 2014 Eric Reese / Kyzentun
All rights reserved.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]