#342 added element focusing feature to graphics library

This commit is contained in:
Mikayla Fischler
2023-09-16 21:06:16 -04:00
parent 1cb240b1b0
commit 29e910ba3c
10 changed files with 540 additions and 222 deletions

View File

@@ -19,6 +19,7 @@ local element = {}
---@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
@@ -32,6 +33,7 @@ local element = {}
---|spinbox_args
---|switch_button_args
---|tabbar_args
---|number_field_args
---|alarm_indicator_light
---|core_map_args
---|data_indicator_args
@@ -69,6 +71,7 @@ local element = {}
function element.new(args, 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
@@ -78,6 +81,7 @@ function element.new(args, child_offset_x, child_offset_y)
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 = {}
}
@@ -89,7 +93,8 @@ function element.new(args, child_offset_x, child_offset_y)
content_window = nil, ---@type table|nil
fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1),
children = {}
children = {},
child_id_map = {}
}
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
@@ -104,6 +109,69 @@ function element.new(args, child_offset_x, child_offset_y)
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 --
-------------------------
@@ -214,85 +282,6 @@ function element.new(args, child_offset_x, child_offset_y)
return in_x and in_y
end
-- 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 a mouse event
---@param event mouse_interaction mouse interaction event
function protected.handle_mouse(event)
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
-- 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
-- enable the control
function protected.enable()
end
-- disable the control
function protected.disable()
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
-- start animations
function protected.start_anim()
end
-- stop animations
function protected.stop_anim()
end
-- get public interface
---@nodiscard
---@return graphics_element element, element_id id
@@ -306,6 +295,98 @@ function element.new(args, child_offset_x, child_offset_y)
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
-- request focus management to focus this element
function protected.req_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 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 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 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
-- 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
-- enable the control
function protected.enable() end
-- disable the control
function protected.disable() 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
-- start animations
function protected.start_anim() end
-- stop animations
function protected.stop_anim() end
-----------
-- SETUP --
-----------
@@ -324,7 +405,7 @@ function element.new(args, child_offset_x, child_offset_y)
self.id = args.id or "__ROOT__"
protected.prepare_template(0, 0, 1)
else
self.id = args.parent.__add_child(args.id, protected)
self.id, self.ordinal = args.parent.__add_child(args.id, protected)
end
----------------------
@@ -358,7 +439,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- delete all children
for k, v in pairs(protected.children) do
v.delete()
v.get().delete()
protected.children[k] = nil
end
@@ -380,47 +461,59 @@ function element.new(args, child_offset_x, child_offset_y)
self.next_y = child.frame.y + child.frame.h
local child_element = child.get()
local id = key ---@type string|integer|nil
if id == nil then
id = self.next_id
self.next_id = self.next_id + 1
end
protected.children[id] = child_element
table.insert(protected.children, child)
protected.child_id_map[id] = #protected.children
return id
end
-- remove a child element
---@param key element_id id
function public.__remove_child(key)
if protected.children[key] ~= nil then
protected.on_removed(key)
protected.children[key] = nil
---@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)
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
-- get a child element
---@nodiscard
---@param id element_id
---@return graphics_element
function public.get_child(id) return protected.children[id] end
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)
if protected.children[id] ~= nil then
protected.children[id].delete()
local index = protected.child_id_map[id]
if protected.children[index] ~= nil then
protected.children[index].get().delete()
protected.on_removed(id)
protected.children[id] = nil
protected.children[index] = nil
protected.child_id_map[id] = nil
end
end
@@ -429,16 +522,13 @@ function element.new(args, child_offset_x, child_offset_y)
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
if protected.children[id] == nil then
local index = protected.child_id_map[id]
if protected.children[index] == nil then
for _, child in pairs(protected.children) do
local elem = child.get_element_by_id(id)
local elem = child.get().get_element_by_id(id)
if elem ~= nil then return elem end
end
else
return protected.children[id]
end
return nil
else return protected.children[index].get() end
end
-- AUTO-PLACEMENT --
@@ -450,97 +540,113 @@ function element.new(args, child_offset_x, child_offset_y)
-- PROPERTIES --
-- get the foreground/background colors
-- get element id
---@nodiscard
---@return cpair fg_bg
function public.get_fg_bg()
return protected.fg_bg
end
---@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
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
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
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
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
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
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
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
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()
protected.enabled = true
protected.enable()
if not protected.enabled then
protected.enabled = true
protected.enable()
end
end
-- disable the element
function public.disable()
protected.enabled = false
protected.disable()
if protected.enabled then
protected.enabled = false
protected.disable()
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 not self.focused then
self.focused = true
protected.on_focused()
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() end
end
-- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s)
function public.recolor(...)
protected.recolor(...)
end
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
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)
end
function public.reposition(x, y) protected.window.reposition(x, y) end
-- FUNCTION CALLBACKS --
@@ -553,12 +659,12 @@ function element.new(args, child_offset_x, child_offset_y)
local ini_in = protected.in_window_bounds(x_ini, y_ini)
if ini_in then
if event.type == events.CLICK_TYPE.UP or event.type == events.CLICK_TYPE.DRAG 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.CLICK_TYPE.DOWN then
elseif event.type == events.MOUSE_CLICK.DOWN then
self.button_down[event.button] = event.initial
end
@@ -566,25 +672,39 @@ function element.new(args, child_offset_x, child_offset_y)
-- handle the mouse event then pass to children
protected.handle_mouse(event_T)
for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
for _, child in pairs(protected.children) do child.get().handle_mouse(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
elseif event.type == events.CLICK_TYPE.DOWN then
-- don't track this click
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
-- draw the element given new data
---@vararg any new data
function public.update(...)
protected.on_update(...)
end
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
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()
@@ -598,6 +718,9 @@ function element.new(args, child_offset_x, child_offset_y)
-- 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)
@@ -610,44 +733,39 @@ function element.new(args, child_offset_x, child_offset_y)
---@see graphics_element.content_redraw
function public.hide()
public.freeze_all() -- stop animations for efficiency/performance
public.unfocus_all()
protected.window.setVisible(false)
end
-- start/resume animation(s)
function public.animate()
protected.start_anim()
end
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.animate_all() end
for _, child in pairs(protected.children) do child.get().animate_all() end
end
end
-- freeze animation(s)
function public.freeze()
protected.stop_anim()
end
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.freeze_all() end
for _, child in pairs(protected.children) do child.get().freeze_all() end
end
-- re-draw the element
function public.redraw()
protected.window.redraw()
end
function public.redraw() protected.window.redraw() 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.redraw() end
for _, child in pairs(protected.children) do child.get().redraw() end
end
end