mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
898 lines
30 KiB
Lua
898 lines
30 KiB
Lua
--
|
|
-- Generic Graphics Element
|
|
--
|
|
|
|
-- local log = require("scada-common.log")
|
|
local util = require("scada-common.util")
|
|
|
|
local core = require("graphics.core")
|
|
|
|
local events = core.events
|
|
|
|
local element = {}
|
|
|
|
---@class graphics_args_generic
|
|
---@field window? table
|
|
---@field parent? graphics_element
|
|
---@field id? string element id
|
|
---@field x? integer 1 if omitted
|
|
---@field y? integer next line if omitted
|
|
---@field width? integer parent width if omitted
|
|
---@field height? integer parent height if omitted
|
|
---@field gframe? graphics_frame frame instead of x/y/width/height
|
|
---@field fg_bg? cpair foreground/background colors
|
|
---@field hidden? boolean true to hide on initial draw
|
|
---@field can_focus? boolean true if this element can be focused, false by default
|
|
|
|
---@alias graphics_args graphics_args_generic
|
|
---|waiting_args
|
|
---|app_button_args
|
|
---|checkbox_args
|
|
---|hazard_button_args
|
|
---|multi_button_args
|
|
---|push_button_args
|
|
---|radio_2d_args
|
|
---|radio_button_args
|
|
---|sidebar_args
|
|
---|spinbox_args
|
|
---|switch_button_args
|
|
---|tabbar_args
|
|
---|number_field_args
|
|
---|text_field_args
|
|
---|alarm_indicator_light
|
|
---|core_map_args
|
|
---|data_indicator_args
|
|
---|hbar_args
|
|
---|icon_indicator_args
|
|
---|indicator_led_args
|
|
---|indicator_led_pair_args
|
|
---|indicator_led_rgb_args
|
|
---|indicator_light_args
|
|
---|power_indicator_args
|
|
---|rad_indicator_args
|
|
---|signal_bar_args
|
|
---|state_indicator_args
|
|
---|tristate_indicator_light_args
|
|
---|vbar_args
|
|
---|app_multipane_args
|
|
---|colormap_args
|
|
---|displaybox_args
|
|
---|div_args
|
|
---|listbox_args
|
|
---|multipane_args
|
|
---|pipenet_args
|
|
---|rectangle_args
|
|
---|textbox_args
|
|
---|tiling_args
|
|
|
|
---@class element_subscription
|
|
---@field ps psil ps used
|
|
---@field key string data key
|
|
---@field func function callback
|
|
|
|
-- more detailed assert message for element verification
|
|
---@param condition any assert condition
|
|
---@param msg string assert message
|
|
---@param callstack_offset? integer shift value to change targets of debug.getinfo()
|
|
function element.assert(condition, msg, callstack_offset)
|
|
callstack_offset = callstack_offset or 0
|
|
local caller = debug.getinfo(3 + callstack_offset)
|
|
assert(condition, util.c(caller.source, ":", caller.currentline, "{", debug.getinfo(2 + callstack_offset).name, "}: ", msg))
|
|
end
|
|
|
|
-- a base graphics element, should not be created on its own
|
|
---@nodiscard
|
|
---@param args graphics_args arguments
|
|
---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height
|
|
---@param child_offset_x? integer mouse event offset x
|
|
---@param child_offset_y? integer mouse event offset y
|
|
function element.new(args, constraint, child_offset_x, child_offset_y)
|
|
local self = {
|
|
id = nil, ---@type element_id|nil
|
|
is_root = args.parent == nil,
|
|
elem_type = debug.getinfo(2).name,
|
|
define_completed = false,
|
|
p_window = nil, ---@type table
|
|
position = events.new_coord_2d(1, 1),
|
|
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
|
|
offset_x = 0,
|
|
offset_y = 0,
|
|
next_y = 1, -- next child y coordinate
|
|
next_id = 0, -- next child ID
|
|
subscriptions = {},
|
|
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
|
|
focused = false,
|
|
mt = {}
|
|
}
|
|
|
|
---@class graphics_base
|
|
local protected = {
|
|
enabled = true,
|
|
value = nil, ---@type any
|
|
window = nil, ---@type table
|
|
content_window = nil, ---@type table|nil
|
|
mouse_window_shift = { x = 0, y = 0 },
|
|
fg_bg = core.cpair(colors.white, colors.black),
|
|
frame = core.gframe(1, 1, 1, 1),
|
|
children = {},
|
|
child_id_map = {}
|
|
}
|
|
|
|
-- element as string
|
|
function self.mt.__tostring()
|
|
return util.c("graphics.element{", self.elem_type, "} @ ", self)
|
|
end
|
|
|
|
---@class graphics_element
|
|
local public = {}
|
|
|
|
setmetatable(public, self.mt)
|
|
|
|
-----------------------
|
|
-- PRIVATE FUNCTIONS --
|
|
-----------------------
|
|
|
|
-- use tab to jump to the next focusable field
|
|
---@param reverse boolean
|
|
local function _tab_focusable(reverse)
|
|
local first_f = nil ---@type graphics_element|nil
|
|
local prev_f = nil ---@type graphics_element|nil
|
|
local cur_f = nil ---@type graphics_element|nil
|
|
local done = false
|
|
|
|
---@param elem graphics_element
|
|
local function handle_element(elem)
|
|
if elem.is_visible() and elem.is_focusable() and elem.is_enabled() then
|
|
if first_f == nil then first_f = elem end
|
|
|
|
if cur_f == nil then
|
|
if elem.is_focused() then
|
|
cur_f = elem
|
|
if (not done) and (reverse and prev_f ~= nil) then
|
|
cur_f.unfocus()
|
|
prev_f.focus()
|
|
done = true
|
|
end
|
|
end
|
|
else
|
|
if elem.is_focused() then
|
|
elem.unfocus()
|
|
elseif not (reverse or done) then
|
|
cur_f.unfocus()
|
|
elem.focus()
|
|
done = true
|
|
end
|
|
end
|
|
|
|
prev_f = elem
|
|
end
|
|
end
|
|
|
|
---@param children table
|
|
local function traverse(children)
|
|
for i = 1, #children do
|
|
local child = children[i] ---@type graphics_base
|
|
handle_element(child.get())
|
|
if child.get().is_visible() then traverse(child.children) end
|
|
end
|
|
end
|
|
|
|
traverse(protected.children)
|
|
|
|
-- if no element was focused, wrap focus
|
|
if first_f ~= nil and not done then
|
|
if reverse then
|
|
if cur_f ~= nil then cur_f.unfocus() end
|
|
if prev_f ~= nil then prev_f.focus() end
|
|
else
|
|
if cur_f ~= nil then cur_f.unfocus() end
|
|
first_f.focus()
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------
|
|
-- PROTECTED FUNCTIONS --
|
|
-------------------------
|
|
|
|
-- prepare the template
|
|
---@param offset_x integer x offset for mouse events
|
|
---@param offset_y integer y offset for mouse events
|
|
---@param next_y integer next line if no y was provided
|
|
function protected.prepare_template(offset_x, offset_y, next_y)
|
|
-- don't auto incrememnt y if inheriting height, that would cause an assertion
|
|
next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y)
|
|
|
|
-- record offsets in case there is a reposition
|
|
self.offset_x = offset_x
|
|
self.offset_y = offset_y
|
|
|
|
-- get frame coordinates/size
|
|
if args.gframe ~= nil then
|
|
protected.frame.x = args.gframe.x
|
|
protected.frame.y = args.gframe.y
|
|
protected.frame.w = args.gframe.w
|
|
protected.frame.h = args.gframe.h
|
|
else
|
|
local w, h = self.p_window.getSize()
|
|
protected.frame.x = args.x or 1
|
|
protected.frame.y = args.y or next_y
|
|
protected.frame.w = args.width or w
|
|
protected.frame.h = args.height or h
|
|
end
|
|
|
|
-- adjust window frame if applicable
|
|
local f = protected.frame
|
|
if args.parent ~= nil then
|
|
-- constrain to parent inner width/height
|
|
local w, h = self.p_window.getSize()
|
|
f.w = math.min(f.w, w - (f.x - 1))
|
|
f.h = math.min(f.h, h - (f.y - 1))
|
|
|
|
if type(constraint) == "function" then
|
|
-- constrain per provided constraint function (can only get smaller than available space)
|
|
w, h = constraint(f)
|
|
f.w = math.min(f.w, w)
|
|
f.h = math.min(f.h, h)
|
|
end
|
|
end
|
|
|
|
-- check frame
|
|
element.assert(f.x >= 1, "frame x not >= 1", 3)
|
|
element.assert(f.y >= 1, "frame y not >= 1", 3)
|
|
element.assert(f.w >= 1, "frame width not >= 1", 3)
|
|
element.assert(f.h >= 1, "frame height not >= 1", 3)
|
|
|
|
-- create window
|
|
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
|
|
|
|
-- init colors
|
|
if args.fg_bg ~= nil then
|
|
protected.fg_bg = core.cpair(args.fg_bg.fgd, args.fg_bg.bkg)
|
|
end
|
|
|
|
if args.parent ~= nil then
|
|
local p_fg_bg = args.parent.get_fg_bg()
|
|
|
|
if args.fg_bg == nil then
|
|
protected.fg_bg = core.cpair(p_fg_bg.fgd, p_fg_bg.bkg)
|
|
else
|
|
if protected.fg_bg.fgd == colors._INHERIT then protected.fg_bg = core.cpair(p_fg_bg.fgd, protected.fg_bg.bkg) end
|
|
if protected.fg_bg.bkg == colors._INHERIT then protected.fg_bg = core.cpair(protected.fg_bg.fgd, p_fg_bg.bkg) end
|
|
end
|
|
end
|
|
|
|
-- check colors
|
|
element.assert(protected.fg_bg.fgd ~= colors._INHERIT, "could not determine foreground color to inherit")
|
|
element.assert(protected.fg_bg.bkg ~= colors._INHERIT, "could not determine background color to inherit")
|
|
|
|
-- set colors
|
|
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
|
protected.window.setTextColor(protected.fg_bg.fgd)
|
|
protected.window.clear()
|
|
|
|
-- record position
|
|
self.position.x, self.position.y = protected.window.getPosition()
|
|
|
|
-- shift per parent child offset
|
|
self.position.x = self.position.x + offset_x
|
|
self.position.y = self.position.y + offset_y
|
|
|
|
-- calculate mouse event bounds
|
|
self.bounds.x1 = self.position.x
|
|
self.bounds.x2 = self.position.x + f.w - 1
|
|
self.bounds.y1 = self.position.y
|
|
self.bounds.y2 = self.position.y + f.h - 1
|
|
|
|
-- alias functions
|
|
|
|
-- window set cursor position
|
|
---@param x integer
|
|
---@param y integer
|
|
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
|
|
|
|
-- set background color
|
|
---@param c color
|
|
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
|
|
|
|
-- set foreground (text) color
|
|
---@param c color
|
|
function protected.w_set_fgd(c) protected.window.setTextColor(c) end
|
|
|
|
-- write text
|
|
---@param str string
|
|
function protected.w_write(str) protected.window.write(str) end
|
|
|
|
-- blit text
|
|
---@param str string
|
|
---@param fg string
|
|
---@param bg string
|
|
function protected.w_blit(str, fg, bg) protected.window.blit(str, fg, bg) end
|
|
end
|
|
|
|
-- check if a coordinate relative to the parent is within the bounds of this element
|
|
---@param x integer
|
|
---@param y integer
|
|
function protected.in_window_bounds(x, y)
|
|
local in_x = x >= self.bounds.x1 and x <= self.bounds.x2
|
|
local in_y = y >= self.bounds.y1 and y <= self.bounds.y2
|
|
return in_x and in_y
|
|
end
|
|
|
|
-- check if a coordinate relative to this window is within the bounds of this element
|
|
---@param x integer
|
|
---@param y integer
|
|
function protected.in_frame_bounds(x, y)
|
|
local in_x = x >= 1 and x <= protected.frame.w
|
|
local in_y = y >= 1 and y <= protected.frame.h
|
|
return in_x and in_y
|
|
end
|
|
|
|
-- get public interface
|
|
---@nodiscard
|
|
---@return graphics_element element, element_id id
|
|
function protected.get() return public, self.id end
|
|
|
|
-- report completion of element instantiation and get the public interface
|
|
---@nodiscard
|
|
---@return graphics_element element, element_id id
|
|
function protected.complete()
|
|
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
|
|
return public, self.id
|
|
end
|
|
|
|
-- protected version of public is_focused()
|
|
---@nodiscard
|
|
---@return boolean is_focused
|
|
function protected.is_focused() return self.focused end
|
|
|
|
-- defocus this element
|
|
function protected.defocus() public.unfocus_all() end
|
|
|
|
-- focus this element and take away focus from all other elements
|
|
function protected.take_focus() args.parent.__focus_child(public) end
|
|
|
|
-- action handlers --
|
|
|
|
-- luacheck: push ignore
|
|
---@diagnostic disable: unused-local, unused-vararg
|
|
|
|
-- handle a child element having been added
|
|
---@param id element_id element identifier
|
|
---@param child graphics_element child element
|
|
function protected.on_added(id, child) end
|
|
|
|
-- handle a child element having been removed
|
|
---@param id element_id element identifier
|
|
function protected.on_removed(id) end
|
|
|
|
-- handle enabled
|
|
function protected.on_enabled() end
|
|
|
|
-- handle disabled
|
|
function protected.on_disabled() end
|
|
|
|
-- handle this element having been focused
|
|
function protected.on_focused() end
|
|
|
|
-- handle this element having been unfocused
|
|
function protected.on_unfocused() end
|
|
|
|
-- handle this element having had a child focused
|
|
---@param child graphics_element
|
|
function protected.on_child_focused(child) end
|
|
|
|
-- handle this element having been shown
|
|
function protected.on_shown() end
|
|
|
|
-- handle this element having been hidden
|
|
function protected.on_hidden() end
|
|
|
|
-- handle a mouse event
|
|
---@param event mouse_interaction mouse interaction event
|
|
function protected.handle_mouse(event) end
|
|
|
|
-- handle a keyboard event
|
|
---@param event key_interaction key interaction event
|
|
function protected.handle_key(event) end
|
|
|
|
-- handle a paste event
|
|
---@param text string pasted text
|
|
function protected.handle_paste(text) end
|
|
|
|
-- handle data value changes
|
|
---@vararg any value(s)
|
|
function protected.on_update(...) end
|
|
|
|
-- callback on control press responses
|
|
---@param result any
|
|
function protected.response_callback(result) end
|
|
|
|
-- accessors and control --
|
|
|
|
-- get value
|
|
---@nodiscard
|
|
function protected.get_value() return protected.value end
|
|
|
|
-- set value
|
|
---@param value any value to set
|
|
function protected.set_value(value) end
|
|
|
|
-- set minimum input value
|
|
---@param min integer minimum allowed value
|
|
function protected.set_min(min) end
|
|
|
|
-- set maximum input value
|
|
---@param max integer maximum allowed value
|
|
function protected.set_max(max) end
|
|
|
|
-- custom recolor command, varies by element if implemented
|
|
---@vararg cpair|color color(s)
|
|
function protected.recolor(...) end
|
|
|
|
-- custom resize command, varies by element if implemented
|
|
---@vararg integer sizing
|
|
function protected.resize(...) end
|
|
|
|
-- luacheck: pop
|
|
---@diagnostic enable: unused-local, unused-vararg
|
|
|
|
-- re-draw this element
|
|
function protected.redraw() end
|
|
|
|
-- start animations
|
|
function protected.start_anim() end
|
|
|
|
-- stop animations
|
|
function protected.stop_anim() end
|
|
|
|
-----------
|
|
-- SETUP --
|
|
-----------
|
|
|
|
-- get the parent window
|
|
self.p_window = args.window
|
|
if self.p_window == nil and args.parent ~= nil then
|
|
self.p_window = args.parent.window()
|
|
end
|
|
|
|
-- check window
|
|
element.assert(self.p_window, "no parent window provided", 1)
|
|
|
|
-- prepare the template
|
|
if args.parent == nil then
|
|
self.id = args.id or "__ROOT__"
|
|
protected.prepare_template(0, 0, 1)
|
|
else
|
|
self.id = args.parent.__add_child(args.id, protected)
|
|
end
|
|
|
|
----------------------
|
|
-- PUBLIC FUNCTIONS --
|
|
----------------------
|
|
|
|
-- get the window object
|
|
---@nodiscard
|
|
function public.window() return protected.content_window or protected.window end
|
|
|
|
-- delete this element (hide and unsubscribe from PSIL)
|
|
function public.delete()
|
|
local fg_bg = protected.fg_bg
|
|
|
|
if args.parent ~= nil then
|
|
-- grab parent fg/bg so we can clear cleanly as a child element
|
|
fg_bg = args.parent.get_fg_bg()
|
|
end
|
|
|
|
-- clear, hide, and stop animations
|
|
protected.window.setBackgroundColor(fg_bg.bkg)
|
|
protected.window.setTextColor(fg_bg.fgd)
|
|
protected.window.clear()
|
|
public.hide()
|
|
|
|
-- unsubscribe from PSIL
|
|
for i = 1, #self.subscriptions do
|
|
local s = self.subscriptions[i] ---@type element_subscription
|
|
s.ps.unsubscribe(s.key, s.func)
|
|
end
|
|
|
|
-- delete all children
|
|
for k, v in pairs(protected.children) do
|
|
v.get().delete()
|
|
protected.children[k] = nil
|
|
end
|
|
|
|
if args.parent ~= nil then
|
|
-- remove self from parent
|
|
-- log.debug("removing " .. self.id .. " from parent")
|
|
args.parent.__remove_child(self.id)
|
|
else
|
|
-- log.debug("no parent for " .. self.id .. " on delete attempt")
|
|
end
|
|
end
|
|
|
|
-- ELEMENT TREE --
|
|
|
|
-- add a child element
|
|
---@nodiscard
|
|
---@param key string|nil id
|
|
---@param child graphics_base
|
|
---@return integer|string key
|
|
function public.__add_child(key, child)
|
|
child.prepare_template(child_offset_x or 0, child_offset_y or 0, self.next_y)
|
|
|
|
self.next_y = child.frame.y + child.frame.h
|
|
|
|
local id = key ---@type string|integer|nil
|
|
if id == nil then
|
|
id = self.next_id
|
|
self.next_id = self.next_id + 1
|
|
end
|
|
|
|
table.insert(protected.children, child)
|
|
|
|
protected.child_id_map[id] = #protected.children
|
|
|
|
return id
|
|
end
|
|
|
|
-- remove a child element
|
|
---@param id element_id id
|
|
function public.__remove_child(id)
|
|
local index = protected.child_id_map[id]
|
|
if protected.children[index] ~= nil then
|
|
protected.on_removed(id)
|
|
protected.children[index] = nil
|
|
protected.child_id_map[id] = nil
|
|
end
|
|
end
|
|
|
|
-- actions to take upon a child element becoming ready (initial draw/construction completed)
|
|
---@param key element_id id
|
|
---@param child graphics_element
|
|
function public.__child_ready(key, child) protected.on_added(key, child) end
|
|
|
|
-- focus solely on this child
|
|
---@param child graphics_element
|
|
function public.__focus_child(child)
|
|
if self.is_root then
|
|
public.unfocus_all()
|
|
child.focus()
|
|
else args.parent.__focus_child(child) end
|
|
end
|
|
|
|
-- a child was focused, used to make sure it is actually visible to the user in the content frame
|
|
---@param child graphics_element
|
|
function public.__child_focused(child)
|
|
protected.on_child_focused(child)
|
|
if not self.is_root then args.parent.__child_focused(public) end
|
|
end
|
|
|
|
-- get a child element
|
|
---@nodiscard
|
|
---@param id element_id
|
|
---@return graphics_element
|
|
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
|
|
|
|
-- remove a child element
|
|
---@param id element_id
|
|
function public.remove(id)
|
|
local index = protected.child_id_map[id]
|
|
if protected.children[index] ~= nil then
|
|
protected.children[index].get().delete()
|
|
protected.on_removed(id)
|
|
protected.children[index] = nil
|
|
protected.child_id_map[id] = nil
|
|
end
|
|
end
|
|
|
|
-- remove all child elements and reset next y
|
|
function public.remove_all()
|
|
for i = 1, #protected.children do
|
|
local child = protected.children[i].get() ---@type graphics_element
|
|
child.delete()
|
|
protected.on_removed(child.get_id())
|
|
end
|
|
|
|
self.next_y = 1
|
|
protected.children = {}
|
|
protected.child_id_map = {}
|
|
end
|
|
|
|
-- attempt to get a child element by ID (does not include this element itself)
|
|
---@nodiscard
|
|
---@param id element_id
|
|
---@return graphics_element|nil element
|
|
function public.get_element_by_id(id)
|
|
local index = protected.child_id_map[id]
|
|
if protected.children[index] == nil then
|
|
for _, child in pairs(protected.children) do
|
|
local elem = child.get().get_element_by_id(id)
|
|
if elem ~= nil then return elem end
|
|
end
|
|
else return protected.children[index].get() end
|
|
end
|
|
|
|
-- AUTO-PLACEMENT --
|
|
|
|
-- skip a line for automatically placed elements
|
|
function public.line_break()
|
|
self.next_y = self.next_y + 1
|
|
end
|
|
|
|
-- PROPERTIES --
|
|
|
|
-- get element id
|
|
---@nodiscard
|
|
---@return element_id
|
|
function public.get_id() return self.id end
|
|
|
|
-- get element x
|
|
---@nodiscard
|
|
---@return integer x
|
|
function public.get_x() return protected.frame.x end
|
|
|
|
-- get element y
|
|
---@nodiscard
|
|
---@return integer y
|
|
function public.get_y() return protected.frame.y end
|
|
|
|
-- get element width
|
|
---@nodiscard
|
|
---@return integer width
|
|
function public.get_width() return protected.frame.w end
|
|
|
|
-- get element height
|
|
---@nodiscard
|
|
---@return integer height
|
|
function public.get_height() return protected.frame.h end
|
|
|
|
-- get the foreground/background colors
|
|
---@nodiscard
|
|
---@return cpair fg_bg
|
|
function public.get_fg_bg() return protected.fg_bg end
|
|
|
|
-- get the element value
|
|
---@nodiscard
|
|
---@return any value
|
|
function public.get_value() return protected.get_value() end
|
|
|
|
-- set the element value
|
|
---@param value any new value
|
|
function public.set_value(value) protected.set_value(value) end
|
|
|
|
-- set minimum input value
|
|
---@param min integer minimum allowed value
|
|
function public.set_min(min) protected.set_min(min) end
|
|
|
|
-- set maximum input value
|
|
---@param max integer maximum allowed value
|
|
function public.set_max(max) protected.set_max(max) end
|
|
|
|
-- check if this element is enabled
|
|
function public.is_enabled() return protected.enabled end
|
|
|
|
-- enable the element
|
|
function public.enable()
|
|
if not protected.enabled then
|
|
protected.enabled = true
|
|
protected.on_enabled()
|
|
end
|
|
end
|
|
|
|
-- disable the element
|
|
function public.disable()
|
|
if protected.enabled then
|
|
protected.enabled = false
|
|
protected.on_disabled()
|
|
public.unfocus_all()
|
|
end
|
|
end
|
|
|
|
-- can this element be focused
|
|
function public.is_focusable() return args.can_focus end
|
|
|
|
-- is this element focused
|
|
function public.is_focused() return self.focused end
|
|
|
|
-- focus the element
|
|
function public.focus()
|
|
if args.can_focus and protected.enabled and not self.focused then
|
|
self.focused = true
|
|
protected.on_focused()
|
|
if not self.is_root then args.parent.__child_focused(public) end
|
|
end
|
|
end
|
|
|
|
-- unfocus this element
|
|
function public.unfocus()
|
|
if args.can_focus and self.focused then
|
|
self.focused = false
|
|
protected.on_unfocused()
|
|
end
|
|
end
|
|
|
|
-- unfocus this element and all its children
|
|
function public.unfocus_all()
|
|
public.unfocus()
|
|
for _, child in pairs(protected.children) do child.get().unfocus_all() end
|
|
end
|
|
|
|
-- custom recolor command, varies by element if implemented
|
|
---@vararg cpair|color color(s)
|
|
function public.recolor(...) protected.recolor(...) end
|
|
|
|
-- resize attributes of the element value if supported
|
|
---@vararg number dimensions (element specific)
|
|
function public.resize(...) protected.resize(...) end
|
|
|
|
-- reposition the element window<br>
|
|
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
|
|
---@param x integer x position relative to parent frame
|
|
---@param y integer y position relative to parent frame
|
|
function public.reposition(x, y)
|
|
protected.window.reposition(x, y)
|
|
|
|
-- record position
|
|
self.position.x, self.position.y = protected.window.getPosition()
|
|
|
|
-- shift per parent child offset
|
|
self.position.x = self.position.x + self.offset_x
|
|
self.position.y = self.position.y + self.offset_y
|
|
|
|
-- calculate mouse event bounds
|
|
self.bounds.x1 = self.position.x
|
|
self.bounds.x2 = self.position.x + protected.frame.w - 1
|
|
self.bounds.y1 = self.position.y
|
|
self.bounds.y2 = self.position.y + protected.frame.h - 1
|
|
end
|
|
|
|
-- FUNCTION CALLBACKS --
|
|
|
|
-- handle a monitor touch or mouse click if this element is visible
|
|
---@param event mouse_interaction mouse interaction event
|
|
function public.handle_mouse(event)
|
|
if protected.window.isVisible() then
|
|
local x_ini, y_ini = event.initial.x, event.initial.y
|
|
|
|
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
|
|
|
if ini_in then
|
|
if event.type == events.MOUSE_CLICK.UP or event.type == events.MOUSE_CLICK.DRAG then
|
|
-- make sure we don't handle mouse events that started before this element was made visible
|
|
if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then
|
|
return
|
|
end
|
|
elseif event.type == events.MOUSE_CLICK.DOWN then
|
|
self.button_down[event.button] = event.initial
|
|
end
|
|
|
|
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
|
|
protected.handle_mouse(event_T)
|
|
|
|
-- shift child event if the content window has moved then pass to children
|
|
local c_event_T = events.mouse_transposed(event_T, protected.mouse_window_shift.x + 1, protected.mouse_window_shift.y + 1)
|
|
for _, child in pairs(protected.children) do child.get().handle_mouse(c_event_T) end
|
|
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
|
|
-- clicked out, unfocus this element and children
|
|
public.unfocus_all()
|
|
end
|
|
else
|
|
-- don't track clicks while hidden
|
|
self.button_down[event.button] = events.new_coord_2d(-1, -1)
|
|
end
|
|
end
|
|
|
|
-- handle a keyboard click if this element is visible and focused
|
|
---@param event key_interaction keyboard interaction event
|
|
function public.handle_key(event)
|
|
if protected.window.isVisible() then
|
|
if self.is_root and (event.type == events.KEY_CLICK.DOWN) and (event.key == keys.tab) then
|
|
-- try to jump to the next/previous focusable field
|
|
_tab_focusable(event.shift)
|
|
else
|
|
-- handle the key event then pass to children
|
|
if self.focused then protected.handle_key(event) end
|
|
for _, child in pairs(protected.children) do child.get().handle_key(event) end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- handle text paste
|
|
---@param text string pasted text
|
|
function public.handle_paste(text)
|
|
if protected.window.isVisible() then
|
|
-- handle the paste event then pass to children
|
|
if self.focused then protected.handle_paste(text) end
|
|
for _, child in pairs(protected.children) do child.get().handle_paste(text) end
|
|
end
|
|
end
|
|
|
|
-- draw the element given new data
|
|
---@vararg any new data
|
|
function public.update(...) protected.on_update(...) end
|
|
|
|
-- on a control request response
|
|
---@param result any
|
|
function public.on_response(result) protected.response_callback(result) end
|
|
|
|
-- register a callback with a PSIL, allowing for automatic unregister on delete<br>
|
|
-- do not use graphics elements directly with PSIL subscribe()
|
|
---@param ps psil PSIL to subscribe to
|
|
---@param key string key to subscribe to
|
|
---@param func function function to link
|
|
function public.register(ps, key, func)
|
|
table.insert(self.subscriptions, { ps = ps, key = key, func = func })
|
|
ps.subscribe(key, func)
|
|
end
|
|
|
|
-- VISIBILITY & ANIMATIONS --
|
|
|
|
-- check if this element is visible
|
|
function public.is_visible() return protected.window.isVisible() end
|
|
|
|
-- show the element and enables animations by default
|
|
---@param animate? boolean true (default) to automatically resume animations
|
|
function public.show(animate)
|
|
protected.window.setVisible(true)
|
|
if animate ~= false then public.animate_all() end
|
|
end
|
|
|
|
-- hide the element and disables animations<br>
|
|
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
|
|
---@see graphics_element.redraw
|
|
---@see graphics_element.content_redraw
|
|
---@param clear? boolean true to visibly hide this element (redraws the parent)
|
|
function public.hide(clear)
|
|
public.freeze_all() -- stop animations for efficiency/performance
|
|
public.unfocus_all()
|
|
protected.window.setVisible(false)
|
|
if clear and args.parent then args.parent.redraw() end
|
|
end
|
|
|
|
-- start/resume animation(s)
|
|
function public.animate() protected.start_anim() end
|
|
|
|
-- start/resume animation(s) for this element and all its children<br>
|
|
-- only animates if a window is visible
|
|
function public.animate_all()
|
|
if protected.window.isVisible() then
|
|
public.animate()
|
|
for _, child in pairs(protected.children) do child.get().animate_all() end
|
|
end
|
|
end
|
|
|
|
-- freeze animation(s)
|
|
function public.freeze() protected.stop_anim() end
|
|
|
|
-- freeze animation(s) for this element and all its children
|
|
function public.freeze_all()
|
|
public.freeze()
|
|
for _, child in pairs(protected.children) do child.get().freeze_all() end
|
|
end
|
|
|
|
-- re-draw this element and all its children
|
|
function public.redraw()
|
|
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
|
|
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
|
protected.window.setTextColor(protected.fg_bg.fgd)
|
|
protected.window.clear()
|
|
protected.window.setBackgroundColor(bg)
|
|
protected.window.setTextColor(fg)
|
|
protected.redraw()
|
|
for _, child in pairs(protected.children) do child.get().redraw() end
|
|
end
|
|
|
|
-- if a content window is set, clears it then re-draws all children
|
|
function public.content_redraw()
|
|
if protected.content_window ~= nil then
|
|
protected.content_window.clear()
|
|
for _, child in pairs(protected.children) do child.get().redraw() end
|
|
end
|
|
end
|
|
|
|
return protected
|
|
end
|
|
|
|
return element
|