#410 pocket nav overhaul

This commit is contained in:
Mikayla Fischler 2024-04-13 14:47:20 -04:00
parent 2b4309afa7
commit 23b31e0049
11 changed files with 288 additions and 255 deletions

View File

@ -29,7 +29,6 @@ local element = {}
---|checkbox_args
---|hazard_button_args
---|multi_button_args
---|app_page_selector_args
---|push_button_args
---|radio_2d_args
---|radio_button_args
@ -54,6 +53,7 @@ local element = {}
---|state_indicator_args
---|tristate_indicator_light_args
---|vbar_args
---|app_multipane_args
---|colormap_args
---|displaybox_args
---|div_args

View File

@ -0,0 +1,110 @@
-- App Page Multi-Pane Display Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local events = require("graphics.events")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class app_multipane_args
---@field panes table panes to swap between
---@field nav_colors cpair on/off colors (a/b respectively) for page navigator
---@field scroll_nav boolean? true to allow scrolling to change the active pane
---@field drag_nav boolean? true to allow mouse dragging to change the active pane (on mouse up)
---@field callback function? function to call when scrolling or dragging changes the pane
---@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 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
-- new app multipane element
---@nodiscard
---@param args app_multipane_args
---@return graphics_element element, element_id id
local function multipane(args)
element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object
local e = element.new(args)
e.value = 1
local nav_x_start = math.floor((e.frame.w / 2) - (#args.panes / 2)) + 1
local nav_x_end = math.floor((e.frame.w / 2) - (#args.panes / 2)) + #args.panes
-- show the selected pane
function e.redraw()
for i = 1, #args.panes do args.panes[i].hide() end
args.panes[e.value].show()
for i = 1, #args.panes do
e.w_set_cur(nav_x_start + (i - 1), e.frame.h)
e.w_set_fgd(util.trinary(i == e.value, args.nav_colors.color_a, args.nav_colors.color_b))
e.w_write("\x07")
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
local initial = e.value
if e.enabled then
if event.current.y == e.frame.h and event.current.x >= nav_x_start and event.current.x <= nav_x_end then
local id = event.current.x - nav_x_start + 1
if event.type == MOUSE_CLICK.TAP then
e.set_value(id)
args.callback(e.value)
elseif event.type == MOUSE_CLICK.UP then
e.set_value(id)
args.callback(e.value)
end
end
end
if args.scroll_nav then
if event.type == events.MOUSE_CLICK.SCROLL_UP then
e.set_value(e.value + 1)
elseif event.type == events.MOUSE_CLICK.SCROLL_DOWN then
e.set_value(e.value - 1)
end
end
if args.drag_nav then
local x1, x2 = event.initial.x, event.current.x
if event.type == events.MOUSE_CLICK.UP and e.in_frame_bounds(x1, event.initial.y) and e.in_frame_bounds(x1, event.current.y) then
if x2 > x1 then
e.set_value(e.value - 1)
elseif x2 < x1 then
e.set_value(e.value + 1)
end
end
end
if e.value ~= initial and type(args.callback) == "function" then args.callback(e.value) end
end
-- select which pane is shown
---@param value integer pane to show
function e.set_value(value)
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
e.value = value
e.redraw()
end
end
-- initial draw
e.redraw()
return e.complete()
end
return multipane

View File

@ -1,75 +0,0 @@
-- App Page Selector Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class app_page_selector_args
---@field page_count integer number of pages (will become this element's width)
---@field active_color color on/off colors (a/b respectively)
---@field callback function function to call on touch
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new app page selector
---@param args app_page_selector_args
---@return graphics_element element, element_id id
local function app_page_selector(args)
element.assert(util.is_int(args.page_count), "page_count is a required field")
element.assert(util.is_int(args.active_color), "active_color is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
args.height = 1
args.width = args.page_count
-- create new graphics element base object
local e = element.new(args)
e.value = 1
-- draw dot selectors
function e.redraw()
for i = 1, args.page_count do
e.w_set_cur(i, 1)
e.w_set_fgd(util.trinary(i == e.value, args.active_color, e.fg_bg.fgd))
e.w_write("\x07")
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == MOUSE_CLICK.TAP then
e.set_value(event.current.x)
args.callback(e.value)
elseif event.type == MOUSE_CLICK.UP then
if e.in_frame_bounds(event.current.x, event.current.y) then
e.set_value(event.current.x)
args.callback(e.value)
end
end
end
end
-- set the value (does not call the callback)
---@param val integer new value
function e.set_value(val)
e.value = val
e.redraw()
end
-- initial draw
e.redraw()
return e.complete()
end
return app_page_selector

View File

@ -3,6 +3,7 @@
--
local psil = require("scada-common.psil")
local log = require("scada-common.log")
local types = require("scada-common.types")
@ -24,97 +25,154 @@ local LINK_STATE = {
iocontrol.LINK_STATE = LINK_STATE
---@enum POCKET_APP_ID
local APP_ID = {
ROOT = 1,
UNITS = 2,
ALARMS = 3,
DUMMY = 4,
NUM_APPS = 4
}
iocontrol.APP_ID = APP_ID
---@class pocket_ioctl
local io = {
nav_root = nil, ---@type nav_tree_node
ps = psil.create()
}
---@class nav_tree_node
---@field _p nav_tree_node|nil page's parent
---@class nav_tree_page
---@field _p nav_tree_page|nil page's parent
---@field _c table page's children
---@field pane_elem graphics_element|nil multipane for this branch
---@field pane_id integer this page's ID in it's contained pane
---@field switcher function|nil function to switch this page's active multipane
---@field nav_to function function to navigate to this page
---@field tasks table tasks to run on this page
---@field switcher function|nil function to switch between children
---@field tasks table tasks to run while viewing this page
-- allocate the page navigation tree system<br>
-- navigation is not ready until init_nav has been called
-- allocate the page navigation system
function iocontrol.alloc_nav()
local self = {
root = { _p = nil, _c = {}, pane_id = 0, pane_elem = nil, nav_to = function () end, tasks = {} }, ---@type nav_tree_node
cur_page = nil ---@type nav_tree_node
pane = nil, ---@type graphics_element
apps = {},
containers = {},
cur_app = APP_ID.ROOT
}
function self.root.switcher(pane_id)
if self.root._c[pane_id] then self.root._c[pane_id].nav_to() end
end
-- find the pane this element belongs to
---@param parent nav_tree_node
local function _find_pane(parent)
if parent == nil then
return nil
elseif parent.pane_elem then
return parent.pane_elem
else
return _find_pane(parent._p)
end
end
self.cur_page = self.root
---@class pocket_nav
io.nav = {}
-- create a new page entry in the page navigation tree
---@param parent nav_tree_node? a parent page or nil to use the root
---@param pane_id integer the pane number for this page in it's parent's multipane
---@param pane graphics_element? this page's multipane, if it has children
---@return nav_tree_node new_page this new page
function io.nav.new_page(parent, pane_id, pane)
local page = { _p = parent or self.root, _c = {}, pane_id = pane_id, pane_elem = pane, tasks = {} }
page._p._c[pane_id] = page
function page.nav_to()
local p_pane = _find_pane(page._p)
if p_pane then p_pane.set_value(page.pane_id) end
self.cur_page = page
-- set the root pane element to switch between apps with
---@param root_pane graphics_element
function io.nav.set_pane(root_pane)
self.pane = root_pane
end
if pane then
function page.switcher() if page._c[pane_id] then page._c[pane_id].nav_to() end end
-- register an app
---@param app_id POCKET_APP_ID app ID
---@param container graphics_element element that contains this app (usually a Div)
---@param pane graphics_element? multipane if this is a simple paned app, then nav_to must be a number
function io.nav.register_app(app_id, container, pane)
---@class pocket_app
local app = {
root = { _p = nil, _c = {}, nav_to = function () end, tasks = {} }, ---@type nav_tree_page
cur_page = nil, ---@type nav_tree_page
paned_pages = {}
}
-- if a pane was provided, this will switch between numbered pages
---@param idx integer page index
function app.switcher(idx)
if app.paned_pages[idx] then
app.paned_pages[idx].nav_to()
end
end
-- create a new page entry in the page navigation tree
---@param parent nav_tree_page? a parent page or nil to set this as the root
---@param nav_to function|integer function to navigate to this page or pane index
---@return nav_tree_page new_page this new page
function app.new_page(parent, nav_to)
---@type nav_tree_page
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
if parent == nil then
app.root = page
if app.cur_page == nil then app.cur_page = page end
end
if type(nav_to) == "number" then
app.paned_pages[nav_to] = page
function page.nav_to()
app.cur_page = page
if pane then pane.set_value(nav_to) end
end
else
function page.nav_to()
app.cur_page = page
nav_to()
end
end
-- switch between children
---@param id integer child ID
function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end
if parent ~= nil then
table.insert(page._p._c, page)
end
return page
end
-- get the currently active page
function io.nav.get_current_page() return self.cur_page end
function app.get_current_page() return app.cur_page end
-- attempt to navigate up the tree
function io.nav.nav_up()
local parent = self.cur_page._p
-- if a parent is defined and this element is not root
---@return boolean success true if successfully navigated up
function app.nav_up()
local parent = app.cur_page._p
if parent then parent.nav_to() end
return parent ~= nil
end
io.nav_root = self.root
end
self.apps[app_id] = app
self.containers[app_id] = container
-- complete initialization of navigation by providing the root muiltipane
---@param root_pane graphics_element navigation root multipane
---@param default_page integer? page to nagivate to if nav_up is called on a base node
function iocontrol.init_nav(root_pane, default_page)
io.nav_root.pane_elem = root_pane
return app
end
---@todo keep this?
-- if default_page ~= nil then
-- io.nav_root.nav_to = function() io.nav_root.switcher(default_page) end
-- end
-- get a list of the app containers (usually Div elements)
function io.nav.get_containers() return self.containers end
return io.nav_root
-- open a given app
---@param app_id POCKET_APP_ID
function io.nav.open_app(app_id)
if self.apps[app_id] then
self.cur_app = app_id
self.pane.set_value(app_id)
else
log.debug("tried to open unknown app")
end
end
-- get the currently active page
---@return nav_tree_page
function io.nav.get_current_page()
return self.apps[self.cur_app].get_current_page()
end
-- attempt to navigate up
function io.nav.nav_up()
local app = self.apps[self.cur_app] ---@type pocket_app
log.debug("attempting app nav up for app " .. self.cur_app)
if not app.nav_up() then
log.debug("internal app nav up failed, going to home screen")
io.nav.open_app(APP_ID.ROOT)
end
end
end
-- initialize facility-independent components of pocket iocontrol

View File

@ -29,7 +29,11 @@ local function create_pages(root)
------------------------
local alarm_test = Div{parent=root,x=1,y=1}
local alarm_tasks = { db.diag.tone_test.get_tone_states }
local alarm_app = db.nav.register_app(iocontrol.APP_ID.ALARMS, alarm_test)
local page = alarm_app.new_page(nil, function () end)
page.tasks = { db.diag.tone_test.get_tone_states }
local ttest = db.diag.tone_test
@ -107,8 +111,6 @@ local function create_pages(root)
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
return { Alarm = { e = alarm_test, tasks = alarm_tasks } }
end
return create_pages

View File

@ -1,5 +1,5 @@
--
-- Boiler Detail Page
-- Placeholder App
--
local iocontrol = require("pocket.iocontrol")
@ -9,20 +9,16 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local ALIGN = core.ALIGN
-- new boiler page view
-- create placeholder app page
---@param root graphics_element parent
local function new_view(root)
local function create_pages(root)
local db = iocontrol.get_db()
db.nav.new_page(nil, 4)
local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end)
return main
TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER}
end
return new_view
return create_pages

View File

@ -4,16 +4,16 @@
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local conn_waiting = require("pocket.ui.components.conn_waiting")
local boiler_page = require("pocket.ui.pages.boiler_page")
local home_page = require("pocket.ui.pages.home_page")
local reactor_page = require("pocket.ui.pages.reactor_page")
local turbine_page = require("pocket.ui.pages.turbine_page")
local unit_page = require("pocket.ui.pages.unit_page")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
@ -71,18 +71,21 @@ local function init(main)
local page_div = Div{parent=main_pane,x=4,y=1}
local sidebar_tabs = {
{ char = "#", color = cpair(colors.black,colors.green) },
{ char = "U", color = cpair(colors.black,colors.yellow) },
{ char = "R", color = cpair(colors.black,colors.cyan) },
{ char = "B", color = cpair(colors.black,colors.lightGray) },
{ char = "T", color = cpair(colors.black,colors.white) }
{ char = "#", color = cpair(colors.black,colors.green) }
}
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes={home_page(page_div),unit_page(page_div),reactor_page(page_div),boiler_page(page_div),turbine_page(page_div)}}
home_page(page_div)
unit_page(page_div)
local base = iocontrol.init_nav(page_pane)
diag_apps(page_div)
dummy_app(page_div)
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=base.switcher}
assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()}
db.nav.set_pane(page_pane)
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=db.nav.open_app}
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}

View File

@ -4,19 +4,18 @@
local iocontrol = require("pocket.iocontrol")
local diag_apps = require("pocket.ui.apps.diag_apps")
local core = require("graphics.core")
local AppMultiPane = require("graphics.elements.appmultipane")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local AppPageSel = require("graphics.elements.controls.app_page_selector")
local App = require("graphics.elements.controls.app")
local cpair = core.cpair
local APP_ID = iocontrol.APP_ID
local ALIGN = core.ALIGN
-- new home page view
@ -24,42 +23,37 @@ local ALIGN = core.ALIGN
local function new_view(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
local main = Div{parent=root,x=1,y=1,height=19}
local apps = Div{parent=main,x=1,y=1,height=19}
local apps_1 = Div{parent=apps,x=1,y=1,height=15}
local apps_2 = Div{parent=apps,x=1,y=1,height=15}
local apps_1 = Div{parent=main,x=1,y=1,height=15}
local apps_2 = Div{parent=main,x=1,y=1,height=15}
local panes = { apps_1, apps_2 }
local app_pane = MultiPane{parent=apps,x=1,y=1,panes=panes,height=15}
local f_ref = {}
local app_pane = AppMultiPane{parent=main,x=1,y=1,height=18,panes=panes,active_color=colors.lightGray,nav_colors=cpair(colors.lightGray,colors.gray),scroll_nav=true,drag_nav=true,callback=function(v)f_ref.callback(v)end}
AppPageSel{parent=apps,x=11,y=18,page_count=2,active_color=colors.lightGray,callback=app_pane.set_value,fg_bg=cpair(colors.gray,colors.black)}
local app = db.nav.register_app(iocontrol.APP_ID.ROOT, main, app_pane)
f_ref.callback = app.switcher
local d_apps = diag_apps(main)
app.new_page(app.new_page(nil, 1), 2)
local page_panes = { apps, d_apps.Alarm.e }
local function open(id) db.nav.open_app(id) end
local page_pane = MultiPane{parent=main,x=1,y=1,panes=page_panes}
local active_fg_bg = cpair(colors.white,colors.gray)
local npage_home = db.nav.new_page(nil, 1, page_pane)
local npage_apps = db.nav.new_page(npage_home, 1)
local npage_alarm = db.nav.new_page(npage_apps, 2)
npage_alarm.tasks = d_apps.Alarm.tasks
App{parent=apps_1,x=3,y=2,text="\x17",title="PRC",callback=function()end,app_fg_bg=cpair(colors.black,colors.purple)}
App{parent=apps_1,x=10,y=2,text="\x15",title="CTL",callback=function()end,app_fg_bg=cpair(colors.black,colors.green)}
App{parent=apps_1,x=17,y=2,text="\x08",title="DEV",callback=function()end,app_fg_bg=cpair(colors.black,colors.lightGray)}
App{parent=apps_1,x=3,y=7,text="\x7f",title="Waste",callback=function()end,app_fg_bg=cpair(colors.black,colors.brown)}
App{parent=apps_1,x=10,y=7,text="\xb6",title="Guide",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
App{parent=apps_1,x=3,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=10,y=2,text="\x17",title="PRC",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=17,y=2,text="\x15",title="CTL",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=3,y=7,text="\x08",title="DEV",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=10,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=17,y=7,text="\xb6",title="Guide",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
TextBox{parent=apps_2,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
App{parent=apps_2,x=3,y=4,text="\x0f",title="Alarm",callback=npage_alarm.nav_to,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
App{parent=apps_2,x=10,y=4,text="\x1e",title="LoopT",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
App{parent=apps_2,x=17,y=4,text="@",title="Comps",callback=function()end,app_fg_bg=cpair(colors.black,colors.orange)}
App{parent=apps_2,x=3,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=10,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=17,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
return main
end

View File

@ -1,28 +0,0 @@
--
-- Reactor Detail Page
--
local iocontrol = require("pocket.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local ALIGN = core.ALIGN
-- new reactor page view
---@param root graphics_element parent
local function new_view(root)
local db = iocontrol.get_db()
db.nav.new_page(nil, 3)
local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main
end
return new_view

View File

@ -1,28 +0,0 @@
--
-- Turbine Detail Page
--
local iocontrol = require("pocket.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local ALIGN = core.ALIGN
-- new turbine page view
---@param root graphics_element parent
local function new_view(root)
local db = iocontrol.get_db()
db.nav.new_page(nil, 5)
local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main
end
return new_view

View File

@ -16,10 +16,11 @@ local ALIGN = core.ALIGN
local function new_view(root)
local db = iocontrol.get_db()
db.nav.new_page(nil, 2)
local main = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main)
app.new_page(nil, function () end)
TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main