2023-09-17 01:06:16 +00:00
-- Numeric Value Entry Graphics Element
2023-12-30 19:41:03 +00:00
local util = require ( " scada-common.util " )
2023-09-17 01:06:16 +00:00
local core = require ( " graphics.core " )
local element = require ( " graphics.element " )
local KEY_CLICK = core.events . KEY_CLICK
2023-09-23 16:49:31 +00:00
local MOUSE_CLICK = core.events . MOUSE_CLICK
2023-09-17 01:06:16 +00:00
---@class number_field_args
---@field default? number default value, defaults to 0
2023-12-30 19:41:03 +00:00
---@field min? number minimum, enforced on unfocus
---@field max? number maximum, enforced on unfocus
---@field max_chars? integer maximum number of characters, defaults to width
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
2023-09-17 01:06:16 +00:00
---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers
2023-09-17 04:12:14 +00:00
---@field dis_fg_bg? cpair foreground/background colors when disabled
2023-09-17 01:06:16 +00:00
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
2023-09-17 04:12:14 +00:00
-- new numeric entry field
2023-09-17 01:06:16 +00:00
---@param args number_field_args
---@return graphics_element element, element_id id
local function number_field ( args )
2023-12-30 19:41:03 +00:00
element.assert ( args.max_int_digits == nil or ( util.is_int ( args.max_int_digits ) and args.max_int_digits > 0 ) , " max_int_digits must be an integer greater than zero if supplied " )
element.assert ( args.max_frac_digits == nil or ( util.is_int ( args.max_frac_digits ) and args.max_frac_digits > 0 ) , " max_frac_digits must be an integer greater than zero if supplied " )
2023-09-17 01:06:16 +00:00
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new ( args )
local has_decimal = false
2023-12-30 19:41:03 +00:00
args.max_chars = args.max_chars or e.frame . w
2023-09-17 01:06:16 +00:00
-- set initial value
2023-09-23 16:49:31 +00:00
e.value = " " .. ( args.default or 0 )
-- make an interactive field manager
2023-12-30 19:41:03 +00:00
local ifield = core.new_ifield ( e , args.max_chars , args.fg_bg , args.dis_fg_bg )
2023-09-23 16:49:31 +00:00
2023-09-17 01:06:16 +00:00
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e . handle_mouse ( event )
-- only handle if on an increment or decrement arrow
2024-02-25 20:53:14 +00:00
if e.enabled and e.in_frame_bounds ( event.current . x , event.current . y ) then
2023-09-23 16:49:31 +00:00
if core.events . was_clicked ( event.type ) then
2023-10-04 02:52:13 +00:00
e.take_focus ( )
2023-09-23 16:49:31 +00:00
2024-02-25 20:53:14 +00:00
if event.type == MOUSE_CLICK.UP then
2023-09-23 16:49:31 +00:00
ifield.move_cursor ( event.current . x )
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all ( )
end
2023-09-17 01:06:16 +00:00
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e . handle_key ( event )
2023-12-30 19:41:03 +00:00
if event.type == KEY_CLICK.CHAR and string.len ( e.value ) < args.max_chars then
2023-09-17 01:06:16 +00:00
if tonumber ( event.name ) then
2023-09-23 16:49:31 +00:00
if e.value == 0 then e.value = " " end
ifield.try_insert_char ( event.name )
2023-09-17 01:06:16 +00:00
end
2023-09-23 20:50:54 +00:00
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
2023-09-17 01:06:16 +00:00
if ( event.key == keys.backspace or event.key == keys.delete ) and ( string.len ( e.value ) > 0 ) then
2023-09-23 16:49:31 +00:00
ifield.backspace ( )
2023-09-17 01:06:16 +00:00
has_decimal = string.find ( e.value , " %. " ) ~= nil
elseif ( event.key == keys.period or event.key == keys.numPadDecimal ) and ( not has_decimal ) and args.allow_decimal then
has_decimal = true
2023-09-23 16:49:31 +00:00
ifield.try_insert_char ( " . " )
2023-09-17 01:06:16 +00:00
elseif ( event.key == keys.minus or event.key == keys.numPadSubtract ) and ( string.len ( e.value ) == 0 ) and args.allow_negative then
2023-09-23 16:49:31 +00:00
ifield.set_value ( " - " )
elseif event.key == keys.left then
ifield.nav_left ( )
elseif event.key == keys.right then
ifield.nav_right ( )
elseif event.key == keys.a and event.ctrl then
ifield.select_all ( )
2023-09-23 19:30:53 +00:00
elseif event.key == keys.home or event.key == keys.up then
ifield.nav_start ( )
elseif event.key == keys [ " end " ] or event.key == keys.down then
ifield.nav_end ( )
2023-09-17 01:06:16 +00:00
end
end
end
2023-09-23 16:49:31 +00:00
-- set the value (must be a number)
2023-09-17 01:06:16 +00:00
---@param val number number to show
2023-09-20 03:51:58 +00:00
function e . set_value ( val )
2023-09-29 23:34:10 +00:00
if tonumber ( val ) then ifield.set_value ( " " .. tonumber ( val ) ) end
2023-09-20 03:51:58 +00:00
end
2023-09-17 01:06:16 +00:00
-- set minimum input value
---@param min integer minimum allowed value
2023-10-01 04:18:57 +00:00
function e . set_min ( min )
args.min = min
e.on_unfocused ( )
end
2023-09-17 01:06:16 +00:00
-- set maximum input value
---@param max integer maximum allowed value
2023-10-01 04:18:57 +00:00
function e . set_max ( max )
args.max = max
e.on_unfocused ( )
end
2023-09-23 16:49:31 +00:00
-- replace text with pasted text if its a number
---@param text string string pasted
function e . handle_paste ( text )
if tonumber ( text ) then
ifield.set_value ( " " .. tonumber ( text ) )
else
ifield.set_value ( " 0 " )
end
2023-09-20 03:51:58 +00:00
end
2023-09-17 01:06:16 +00:00
2023-09-17 04:12:14 +00:00
-- handle unfocused
2023-09-17 01:06:16 +00:00
function e . on_unfocused ( )
local val = tonumber ( e.value )
local max = tonumber ( args.max )
local min = tonumber ( args.min )
if type ( val ) == " number " then
2023-12-31 00:21:44 +00:00
if args.max_int_digits or args.max_frac_digits then
2023-12-30 19:41:03 +00:00
local str = e.value
local ceil = false
if string.find ( str , " - " ) then str = string.sub ( e.value , 2 ) end
local parts = util.strtok ( str , " . " )
if parts [ 1 ] and args.max_int_digits then
if string.len ( parts [ 1 ] ) > args.max_int_digits then
parts [ 1 ] = string.rep ( " 9 " , args.max_int_digits )
ceil = true
end
end
if args.allow_decimal and args.max_frac_digits then
if ceil then
parts [ 2 ] = string.rep ( " 9 " , args.max_frac_digits )
elseif parts [ 2 ] and ( string.len ( parts [ 2 ] ) > args.max_frac_digits ) then
-- add a half of the highest precision fractional value in order to round using floor
local scaled = math.fmod ( val , 1 ) * ( 10 ^ ( args.max_frac_digits ) )
local value = math.floor ( scaled + 0.5 )
local unscaled = value * ( 10 ^ ( - args.max_frac_digits ) )
parts [ 2 ] = string.sub ( tostring ( unscaled ) , 3 ) -- remove starting "0."
end
end
if parts [ 2 ] then parts [ 2 ] = " . " .. parts [ 2 ] else parts [ 2 ] = " " end
val = tonumber ( ( parts [ 1 ] or " " ) .. parts [ 2 ] )
end
2023-09-17 01:06:16 +00:00
if type ( args.max ) == " number " and val > max then
e.value = " " .. max
2023-10-01 04:18:57 +00:00
ifield.nav_start ( )
2023-09-17 01:06:16 +00:00
elseif type ( args.min ) == " number " and val < min then
e.value = " " .. min
2023-10-01 04:18:57 +00:00
ifield.nav_start ( )
2023-10-20 03:20:04 +00:00
else
e.value = " " .. val
ifield.nav_end ( )
2023-09-17 01:06:16 +00:00
end
else
e.value = " "
end
2023-09-23 16:49:31 +00:00
ifield.show ( )
2023-09-17 01:06:16 +00:00
end
2023-09-29 23:34:10 +00:00
-- handle focus (not unfocus), enable, and redraw with show()
e.on_focused = ifield.show
2023-09-23 18:31:37 +00:00
e.on_enabled = ifield.show
e.on_disabled = ifield.show
2023-09-29 23:34:10 +00:00
e.redraw = ifield.show
2023-09-17 01:06:16 +00:00
-- initial draw
2023-09-29 23:34:10 +00:00
e.redraw ( )
2023-09-17 01:06:16 +00:00
return e.complete ( )
end
return number_field