2020-03-31 15:57:23 +00:00
|
|
|
local Array = require('opus.array')
|
2019-06-28 17:50:02 +00:00
|
|
|
local class = require('opus.class')
|
|
|
|
local Event = require('opus.event')
|
|
|
|
local Input = require('opus.input')
|
|
|
|
local Transition = require('opus.ui.transition')
|
|
|
|
local Util = require('opus.util')
|
2020-03-31 15:57:23 +00:00
|
|
|
local Canvas = require('opus.ui.canvas')
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2017-10-11 15:37:52 +00:00
|
|
|
local _rep = string.rep
|
|
|
|
local _sub = string.sub
|
|
|
|
local colors = _G.colors
|
2019-07-21 16:16:47 +00:00
|
|
|
local device = _G.device
|
2017-10-11 15:37:52 +00:00
|
|
|
local fs = _G.fs
|
|
|
|
local os = _G.os
|
|
|
|
local term = _G.term
|
2019-07-16 02:08:30 +00:00
|
|
|
local textutils = _G.textutils
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2017-10-09 04:26:19 +00:00
|
|
|
--[[
|
2018-01-24 22:39:38 +00:00
|
|
|
Using the shorthand window definition, elements are created from
|
|
|
|
the bottom up. Once reaching the top, setParent is called top down.
|
2017-10-09 04:26:19 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
On :init(), elements do not know the parent or can calculate sizing.
|
2017-10-09 04:26:19 +00:00
|
|
|
|
2019-11-18 21:32:10 +00:00
|
|
|
Calling order:
|
|
|
|
window:postInit()
|
|
|
|
at this point, the window has all default values set
|
|
|
|
window:setParent()
|
|
|
|
parent has been assigned
|
|
|
|
following are called:
|
|
|
|
window:layout()
|
|
|
|
sizing / positioning is performed
|
|
|
|
window:initChildren()
|
|
|
|
each child of window will get initialized
|
|
|
|
]]
|
2016-12-11 19:24:52 +00:00
|
|
|
|
|
|
|
--[[-- Top Level Manager --]]--
|
|
|
|
local Manager = class()
|
2017-10-07 04:27:41 +00:00
|
|
|
function Manager:init()
|
2019-01-02 15:33:47 +00:00
|
|
|
self.devices = { }
|
2019-03-26 04:31:25 +00:00
|
|
|
self.theme = { }
|
|
|
|
self.extChars = Util.getVersion() >= 1.76
|
2019-01-03 04:56:01 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
local function keyFunction(event, code, held)
|
|
|
|
local ie = Input:translate(event, code, held)
|
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
local currentPage = self:getActivePage()
|
|
|
|
if ie and currentPage then
|
|
|
|
local target = currentPage.focused or currentPage
|
2020-03-31 15:57:23 +00:00
|
|
|
target:emit({ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie })
|
2019-01-02 15:33:47 +00:00
|
|
|
currentPage:sync()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function resize(_, side)
|
|
|
|
local dev = self.devices[side or 'terminal']
|
|
|
|
if dev and dev.currentPage then
|
|
|
|
-- the parent doesn't have any children set...
|
|
|
|
-- that's why we have to resize both the parent and the current page
|
|
|
|
-- kinda makes sense
|
|
|
|
dev.currentPage.parent:resize()
|
|
|
|
|
|
|
|
dev.currentPage:resize()
|
|
|
|
dev.currentPage:draw()
|
|
|
|
dev.currentPage:sync()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local handlers = {
|
|
|
|
char = keyFunction,
|
|
|
|
key_up = keyFunction,
|
|
|
|
key = keyFunction,
|
2019-01-02 15:33:47 +00:00
|
|
|
term_resize = resize,
|
|
|
|
monitor_resize = resize,
|
2018-01-24 22:39:38 +00:00
|
|
|
|
|
|
|
mouse_scroll = function(_, direction, x, y)
|
2019-01-02 15:33:47 +00:00
|
|
|
local currentPage = self:getActivePage()
|
|
|
|
if currentPage then
|
|
|
|
local event = currentPage:pointToChild(x, y)
|
2018-01-24 22:39:38 +00:00
|
|
|
local directions = {
|
|
|
|
[ -1 ] = 'up',
|
|
|
|
[ 1 ] = 'down'
|
|
|
|
}
|
|
|
|
-- revisit - should send out scroll_up and scroll_down events
|
|
|
|
-- let the element convert them to up / down
|
2020-03-31 15:57:23 +00:00
|
|
|
event.element:emit({ type = 'key', key = directions[direction] })
|
2019-01-02 15:33:47 +00:00
|
|
|
currentPage:sync()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
monitor_touch = function(_, side, x, y)
|
2019-01-02 15:33:47 +00:00
|
|
|
local dev = self.devices[side]
|
|
|
|
if dev and dev.currentPage then
|
2019-01-09 13:50:42 +00:00
|
|
|
Input:translate('mouse_click', 1, x, y)
|
|
|
|
local ie = Input:translate('mouse_up', 1, x, y)
|
2020-03-31 15:57:23 +00:00
|
|
|
self:click(dev.currentPage, ie)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
mouse_click = function(_, button, x, y)
|
2019-03-29 13:56:56 +00:00
|
|
|
local ie = Input:translate('mouse_click', button, x, y)
|
2018-01-24 22:39:38 +00:00
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
local currentPage = self:getActivePage()
|
|
|
|
if currentPage then
|
|
|
|
if not currentPage.parent.device.side then
|
|
|
|
local event = currentPage:pointToChild(x, y)
|
2018-01-24 22:39:38 +00:00
|
|
|
if event.element.focus and not event.element.inactive then
|
2019-01-02 15:33:47 +00:00
|
|
|
currentPage:setFocus(event.element)
|
|
|
|
currentPage:sync()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2020-03-31 15:57:23 +00:00
|
|
|
self:click(currentPage, ie)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
mouse_up = function(_, button, x, y)
|
|
|
|
local ie = Input:translate('mouse_up', button, x, y)
|
2019-01-02 15:33:47 +00:00
|
|
|
local currentPage = self:getActivePage()
|
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if ie.code == 'control-shift-mouse_click' then -- hack
|
2019-01-02 15:33:47 +00:00
|
|
|
local event = currentPage:pointToChild(x, y)
|
2018-01-24 22:39:38 +00:00
|
|
|
_ENV.multishell.openTab({
|
|
|
|
path = 'sys/apps/Lua.lua',
|
|
|
|
args = { event.element },
|
|
|
|
focused = true })
|
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
elseif ie and currentPage then
|
2019-07-21 01:29:44 +00:00
|
|
|
if not currentPage.parent.device.side then
|
2020-03-31 15:57:23 +00:00
|
|
|
self:click(currentPage, ie)
|
2019-07-21 01:29:44 +00:00
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
mouse_drag = function(_, button, x, y)
|
|
|
|
local ie = Input:translate('mouse_drag', button, x, y)
|
2019-01-02 15:33:47 +00:00
|
|
|
local currentPage = self:getActivePage()
|
2019-05-04 10:11:22 +00:00
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
if ie and currentPage then
|
2020-03-31 15:57:23 +00:00
|
|
|
self:click(currentPage, ie)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end,
|
|
|
|
|
|
|
|
paste = function(_, text)
|
2019-03-29 13:56:56 +00:00
|
|
|
local ie = Input:translate('paste', text)
|
|
|
|
self:emitEvent({ type = 'paste', text = text, ie = ie })
|
2019-01-02 15:33:47 +00:00
|
|
|
self:getActivePage():sync()
|
2018-01-24 22:39:38 +00:00
|
|
|
end,
|
|
|
|
}
|
|
|
|
|
|
|
|
-- use 1 handler to single thread all events
|
|
|
|
Event.on({
|
2019-01-02 15:33:47 +00:00
|
|
|
'char', 'key_up', 'key', 'term_resize', 'monitor_resize',
|
2018-01-24 22:39:38 +00:00
|
|
|
'mouse_scroll', 'monitor_touch', 'mouse_click',
|
|
|
|
'mouse_up', 'mouse_drag', 'paste' },
|
|
|
|
function(event, ...)
|
|
|
|
handlers[event](event, ...)
|
|
|
|
end)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:configure(appName, ...)
|
2018-01-24 22:39:38 +00:00
|
|
|
local defaults = Util.loadTable('usr/config/' .. appName) or { }
|
|
|
|
if not defaults.device then
|
|
|
|
defaults.device = { }
|
|
|
|
end
|
|
|
|
|
2019-07-21 01:29:44 +00:00
|
|
|
-- starting a program: gpsServer --display=monitor_3148 --scale=.5 gps
|
|
|
|
local _, options = Util.parse(...)
|
2018-01-24 22:39:38 +00:00
|
|
|
local optionValues = {
|
2019-07-21 01:29:44 +00:00
|
|
|
name = options.display,
|
|
|
|
textScale = tonumber(options.scale),
|
2018-01-24 22:39:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Util.merge(defaults.device, optionValues)
|
|
|
|
|
|
|
|
if defaults.device.name then
|
|
|
|
local dev
|
|
|
|
|
|
|
|
if defaults.device.name == 'terminal' then
|
|
|
|
dev = term.current()
|
|
|
|
else
|
2019-07-21 16:16:47 +00:00
|
|
|
dev = device[defaults.device.name]
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
if not dev then
|
|
|
|
error('Invalid display device')
|
|
|
|
end
|
|
|
|
self:setDefaultDevice(self.Device({
|
|
|
|
device = dev,
|
|
|
|
textScale = defaults.device.textScale,
|
|
|
|
}))
|
|
|
|
end
|
|
|
|
|
|
|
|
if defaults.theme then
|
|
|
|
for k,v in pairs(defaults.theme) do
|
|
|
|
if self[k] and self[k].defaults then
|
|
|
|
Util.merge(self[k].defaults, v)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2016-12-15 14:45:27 +00:00
|
|
|
function Manager:disableEffects()
|
2018-01-24 22:39:38 +00:00
|
|
|
self.defaultDevice.effectsEnabled = false
|
2016-12-15 14:45:27 +00:00
|
|
|
end
|
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
function Manager:loadTheme(filename)
|
2018-01-24 22:39:38 +00:00
|
|
|
if fs.exists(filename) then
|
|
|
|
local theme, err = Util.loadTable(filename)
|
|
|
|
if not theme then
|
|
|
|
error(err)
|
|
|
|
end
|
2019-03-24 07:44:38 +00:00
|
|
|
Util.deepMerge(self.theme, theme)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-07-16 02:08:30 +00:00
|
|
|
function Manager:generateTheme(filename)
|
|
|
|
local t = { }
|
|
|
|
for k,v in pairs(self) do
|
|
|
|
if type(v) == 'table' then
|
|
|
|
if v._preload then
|
|
|
|
v._preload()
|
|
|
|
v = self[k]
|
|
|
|
end
|
|
|
|
if v.defaults and v.defaults.UIElement ~= 'Device' then
|
|
|
|
for p,d in pairs(v.defaults) do
|
|
|
|
if p:find('olor') then
|
|
|
|
if not t[k] then
|
|
|
|
t[k] = { }
|
|
|
|
end
|
|
|
|
for c, n in pairs(colors) do
|
|
|
|
if n == d then
|
|
|
|
t[k][p] = 'colors.' .. c
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
Util.writeFile(filename, textutils.serialize(t):gsub('(")', ''))
|
|
|
|
end
|
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
function Manager:emitEvent(event)
|
2019-01-02 15:33:47 +00:00
|
|
|
local currentPage = self:getActivePage()
|
|
|
|
if currentPage and currentPage.focused then
|
|
|
|
return currentPage.focused:emit(event)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
function Manager:click(target, ie)
|
|
|
|
local clickEvent
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
if ie.code == 'mouse_drag' then
|
|
|
|
clickEvent = {
|
|
|
|
element = self.lastClicked,
|
|
|
|
x = ie.x,
|
|
|
|
y = ie.y, -- this is not correct (should be relative to element)
|
|
|
|
dx = ie.dx,
|
|
|
|
dy = ie.dy,
|
|
|
|
}
|
|
|
|
else
|
|
|
|
clickEvent = target:pointToChild(ie.x, ie.y)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- hack for dropdown menus
|
|
|
|
if ie.code == 'mouse_click' and not clickEvent.element.focus then
|
|
|
|
self:emitEvent({ type = 'mouse_out' })
|
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
if ie.code == 'mouse_doubleclick' then
|
|
|
|
if self.lastClicked ~= clickEvent.element then
|
2019-01-02 15:33:47 +00:00
|
|
|
return
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-01-02 15:33:47 +00:00
|
|
|
else
|
2020-03-31 15:57:23 +00:00
|
|
|
self.lastClicked = clickEvent.element
|
2019-01-02 15:33:47 +00:00
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
clickEvent.button = ie.button
|
|
|
|
clickEvent.type = ie.code
|
|
|
|
clickEvent.key = ie.code
|
|
|
|
clickEvent.ie = { code = ie.code, x = clickEvent.x, y = clickEvent.y }
|
|
|
|
clickEvent.raw = ie
|
2018-01-24 22:39:38 +00:00
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
if clickEvent.element.focus then
|
|
|
|
target:setFocus(clickEvent.element)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2020-03-31 15:57:23 +00:00
|
|
|
clickEvent.element:emit(clickEvent)
|
2019-01-02 15:33:47 +00:00
|
|
|
|
|
|
|
target:sync()
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2017-10-11 15:37:52 +00:00
|
|
|
function Manager:setDefaultDevice(dev)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.defaultDevice = dev
|
|
|
|
self.term = dev
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:addPage(name, page)
|
2018-10-21 22:48:08 +00:00
|
|
|
if not self.pages then
|
|
|
|
self.pages = { }
|
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
self.pages[name] = page
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:setPages(pages)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.pages = pages
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:getPage(pageName)
|
2018-01-24 22:39:38 +00:00
|
|
|
local page = self.pages[pageName]
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if not page then
|
|
|
|
error('UI:getPage: Invalid page: ' .. tostring(pageName), 2)
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
return page
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
function Manager:getActivePage(page)
|
|
|
|
if page then
|
|
|
|
return page.parent.currentPage
|
|
|
|
end
|
|
|
|
return self.defaultDevice.currentPage
|
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:setActivePage(page)
|
|
|
|
page.parent.currentPage = page
|
|
|
|
end
|
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
function Manager:setPage(pageOrName, ...)
|
2018-01-24 22:39:38 +00:00
|
|
|
local page = pageOrName
|
|
|
|
|
|
|
|
if type(pageOrName) == 'string' then
|
|
|
|
page = self.pages[pageOrName] or error('Invalid page: ' .. pageOrName)
|
|
|
|
end
|
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
local currentPage = self:getActivePage(page)
|
|
|
|
if page == currentPage then
|
2018-01-24 22:39:38 +00:00
|
|
|
page:draw()
|
|
|
|
else
|
2019-01-02 15:33:47 +00:00
|
|
|
if currentPage then
|
|
|
|
if currentPage.focused then
|
|
|
|
currentPage.focused.focused = false
|
|
|
|
currentPage.focused:focus()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-01-02 15:33:47 +00:00
|
|
|
currentPage:disable()
|
|
|
|
page.previousPage = currentPage
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-01-02 15:33:47 +00:00
|
|
|
self:setActivePage(page)
|
2019-01-30 20:11:41 +00:00
|
|
|
--page:clear(page.backgroundColor)
|
2018-01-24 22:39:38 +00:00
|
|
|
page:enable(...)
|
|
|
|
page:draw()
|
2019-01-02 15:33:47 +00:00
|
|
|
if page.focused then
|
|
|
|
page.focused.focused = true
|
|
|
|
page.focused:focus()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-01-30 20:11:41 +00:00
|
|
|
page:sync()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:getCurrentPage()
|
2019-01-02 15:33:47 +00:00
|
|
|
return self.defaultDevice.currentPage
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:setPreviousPage()
|
2019-01-02 15:33:47 +00:00
|
|
|
if self.defaultDevice.currentPage.previousPage then
|
|
|
|
local previousPage = self.defaultDevice.currentPage.previousPage.previousPage
|
|
|
|
self:setPage(self.defaultDevice.currentPage.previousPage)
|
|
|
|
self.defaultDevice.currentPage.previousPage = previousPage
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Manager:getDefaults(element, args)
|
2018-01-24 22:39:38 +00:00
|
|
|
local defaults = Util.deepCopy(element.defaults)
|
|
|
|
if args then
|
|
|
|
Manager:mergeProperties(defaults, args)
|
|
|
|
end
|
|
|
|
return defaults
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2017-10-12 21:58:35 +00:00
|
|
|
function Manager:mergeProperties(obj, args)
|
2018-01-24 22:39:38 +00:00
|
|
|
if args then
|
|
|
|
for k,v in pairs(args) do
|
|
|
|
if k == 'accelerators' then
|
|
|
|
if obj.accelerators then
|
|
|
|
Util.merge(obj.accelerators, args.accelerators)
|
|
|
|
else
|
|
|
|
obj[k] = v
|
|
|
|
end
|
|
|
|
else
|
|
|
|
obj[k] = v
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2017-10-12 21:58:35 +00:00
|
|
|
function Manager:pullEvents(...)
|
2019-03-26 06:10:10 +00:00
|
|
|
local s, m = pcall(Event.pullEvents, ...)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.term:reset()
|
2019-03-26 06:10:10 +00:00
|
|
|
if not s and m then
|
|
|
|
error(m, -1)
|
|
|
|
end
|
2017-10-12 21:58:35 +00:00
|
|
|
end
|
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
Manager.colors = {
|
|
|
|
primary = colors.cyan,
|
|
|
|
secondary = colors.blue,
|
|
|
|
tertiary = colors.blue,
|
|
|
|
}
|
|
|
|
|
2019-12-07 19:04:58 +00:00
|
|
|
Manager.exitPullEvents = Event.exitPullEvents
|
|
|
|
Manager.quit = Event.exitPullEvents
|
|
|
|
Manager.start = Manager.pullEvents
|
2017-10-12 21:58:35 +00:00
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
local UI = Manager()
|
|
|
|
|
|
|
|
--[[-- Basic drawable area --]]--
|
2020-03-31 15:57:23 +00:00
|
|
|
UI.Window = class(Canvas)
|
2017-10-11 15:37:52 +00:00
|
|
|
UI.Window.uid = 1
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs = { }
|
2016-12-11 19:24:52 +00:00
|
|
|
UI.Window.defaults = {
|
2018-01-24 22:39:38 +00:00
|
|
|
UIElement = 'Window',
|
|
|
|
x = 1,
|
|
|
|
y = 1,
|
|
|
|
-- z = 0, -- eventually...
|
|
|
|
offx = 0,
|
|
|
|
offy = 0,
|
|
|
|
cursorX = 1,
|
|
|
|
cursorY = 1,
|
2016-12-11 19:24:52 +00:00
|
|
|
}
|
|
|
|
function UI.Window:init(args)
|
2018-01-24 22:39:38 +00:00
|
|
|
-- merge defaults for all subclasses
|
|
|
|
local defaults = args
|
|
|
|
local m = getmetatable(self) -- get the class for this instance
|
|
|
|
repeat
|
2020-03-31 15:57:23 +00:00
|
|
|
if m.disable then
|
|
|
|
defaults = UI:getDefaults(m, defaults)
|
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
m = m._base
|
|
|
|
until not m
|
|
|
|
UI:mergeProperties(self, defaults)
|
|
|
|
|
|
|
|
-- each element has a unique ID
|
|
|
|
self.uid = UI.Window.uid
|
|
|
|
UI.Window.uid = UI.Window.uid + 1
|
|
|
|
|
|
|
|
-- at this time, the object has all the properties set
|
|
|
|
|
|
|
|
-- postInit is a special constructor. the element does not need to implement
|
|
|
|
-- the method. But we need to guarantee that each subclass which has this
|
|
|
|
-- method is called.
|
|
|
|
m = self
|
|
|
|
local lpi
|
|
|
|
repeat
|
|
|
|
if m.postInit and m.postInit ~= lpi then
|
|
|
|
m.postInit(self)
|
|
|
|
lpi = m.postInit
|
|
|
|
end
|
|
|
|
m = m._base
|
|
|
|
until not m
|
2017-10-11 15:37:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:postInit()
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.parent then
|
|
|
|
-- this will cascade down the whole tree of elements starting at the
|
|
|
|
-- top level window (which has a device as a parent)
|
|
|
|
self:setParent()
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:initChildren()
|
2018-01-24 22:39:38 +00:00
|
|
|
local children = self.children
|
|
|
|
|
|
|
|
-- insert any UI elements created using the shorthand
|
|
|
|
-- window definition into the children array
|
|
|
|
for k,child in pairs(self) do
|
|
|
|
if k ~= 'parent' then -- reserved
|
|
|
|
if type(child) == 'table' and child.UIElement and not child.parent then
|
|
|
|
if not children then
|
|
|
|
children = { }
|
|
|
|
end
|
|
|
|
table.insert(children, child)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if children then
|
|
|
|
for _,child in pairs(children) do
|
|
|
|
if not child.parent then
|
|
|
|
child.parent = self
|
|
|
|
child:setParent()
|
|
|
|
if self.enabled then
|
|
|
|
child:enable()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self.children = children
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-02-06 04:03:57 +00:00
|
|
|
function UI.Window:layout()
|
2019-07-02 14:19:08 +00:00
|
|
|
local function calc(p, max)
|
|
|
|
p = tonumber(p:match('(%d+)%%'))
|
|
|
|
return p and math.floor(max * p / 100) or 1
|
|
|
|
end
|
|
|
|
|
|
|
|
if type(self.x) == 'string' then
|
2019-11-17 05:12:02 +00:00
|
|
|
self.x = calc(self.x, self.parent.width) + 1
|
|
|
|
-- +1 in order to allow both x and ex to use the same %
|
2019-07-02 14:19:08 +00:00
|
|
|
end
|
|
|
|
if type(self.ex) == 'string' then
|
|
|
|
self.ex = calc(self.ex, self.parent.width)
|
|
|
|
end
|
|
|
|
if type(self.y) == 'string' then
|
2019-11-17 05:12:02 +00:00
|
|
|
self.y = calc(self.y, self.parent.height) + 1
|
2019-07-02 14:19:08 +00:00
|
|
|
end
|
|
|
|
if type(self.ey) == 'string' then
|
|
|
|
self.ey = calc(self.ey, self.parent.height)
|
|
|
|
end
|
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.x < 0 then
|
|
|
|
self.x = self.parent.width + self.x + 1
|
|
|
|
end
|
|
|
|
if self.y < 0 then
|
|
|
|
self.y = self.parent.height + self.y + 1
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.ex then
|
|
|
|
local ex = self.ex
|
|
|
|
if self.ex <= 1 then
|
|
|
|
ex = self.parent.width + self.ex + 1
|
|
|
|
end
|
|
|
|
if self.width then
|
|
|
|
self.x = ex - self.width + 1
|
|
|
|
else
|
|
|
|
self.width = ex - self.x + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if self.ey then
|
|
|
|
local ey = self.ey
|
|
|
|
if self.ey <= 1 then
|
|
|
|
ey = self.parent.height + self.ey + 1
|
|
|
|
end
|
|
|
|
if self.height then
|
|
|
|
self.y = ey - self.height + 1
|
|
|
|
else
|
|
|
|
self.height = ey - self.y + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not self.width then
|
|
|
|
self.width = self.parent.width - self.x + 1
|
|
|
|
end
|
|
|
|
if not self.height then
|
|
|
|
self.height = self.parent.height - self.y + 1
|
|
|
|
end
|
2020-03-31 15:57:23 +00:00
|
|
|
|
|
|
|
self:reposition(self.x, self.y, self.width, self.height)
|
2017-09-30 02:30:01 +00:00
|
|
|
end
|
|
|
|
|
2019-02-09 00:21:20 +00:00
|
|
|
-- Called when the window's parent has be assigned
|
2017-09-30 02:30:01 +00:00
|
|
|
function UI.Window:setParent()
|
2018-01-24 22:39:38 +00:00
|
|
|
self.oh, self.ow = self.height, self.width
|
|
|
|
self.ox, self.oy = self.x, self.y
|
2017-09-30 02:30:01 +00:00
|
|
|
|
2019-02-06 04:03:57 +00:00
|
|
|
self:layout()
|
2018-01-24 22:39:38 +00:00
|
|
|
self:initChildren()
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2017-10-01 00:35:36 +00:00
|
|
|
function UI.Window:resize()
|
2018-01-24 22:39:38 +00:00
|
|
|
self.height, self.width = self.oh, self.ow
|
|
|
|
self.x, self.y = self.ox, self.oy
|
2017-10-01 00:35:36 +00:00
|
|
|
|
2019-02-06 04:03:57 +00:00
|
|
|
self:layout()
|
2017-10-01 00:35:36 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.children then
|
2020-03-31 15:57:23 +00:00
|
|
|
for child in self:eachChild() do
|
2018-01-24 22:39:38 +00:00
|
|
|
child:resize()
|
|
|
|
end
|
|
|
|
end
|
2017-10-01 00:35:36 +00:00
|
|
|
end
|
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
function UI.Window:reposition(x, y, w, h)
|
|
|
|
if not self.lines then
|
|
|
|
Canvas.init(self, {
|
|
|
|
x = x,
|
|
|
|
y = y,
|
|
|
|
width = w,
|
|
|
|
height = h,
|
|
|
|
isColor = self.parent.isColor,
|
|
|
|
})
|
|
|
|
else
|
|
|
|
self:move(x, y)
|
|
|
|
Canvas.resize(self, w, h)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:raise()
|
|
|
|
Array.removeByValue(self.parent.children, self)
|
|
|
|
table.insert(self.parent.children, self)
|
|
|
|
self:dirty(true)
|
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs.add = [[add(TABLE)
|
|
|
|
Add element(s) to a window. Example:
|
|
|
|
page:add({
|
|
|
|
text = UI.Text {
|
|
|
|
x=5,value='help'
|
|
|
|
}
|
|
|
|
})]]
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Window:add(children)
|
2018-01-24 22:39:38 +00:00
|
|
|
UI:mergeProperties(self, children)
|
|
|
|
self:initChildren()
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
function UI.Window:eachChild()
|
|
|
|
local c = self.children and Util.shallowCopy(self.children)
|
|
|
|
local i = 0
|
|
|
|
return function()
|
|
|
|
i = i + 1
|
|
|
|
return c and c[i]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:remove()
|
|
|
|
Array.removeByValue(self.parent.children, self)
|
|
|
|
self.parent:dirty(true)
|
|
|
|
end
|
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Window:getCursorPos()
|
2018-01-24 22:39:38 +00:00
|
|
|
return self.cursorX, self.cursorY
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:setCursorPos(x, y)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.cursorX = x
|
|
|
|
self.cursorY = y
|
|
|
|
self.parent:setCursorPos(self.x + x - 1, self.y + y - 1)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:setCursorBlink(blink)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.parent:setCursorBlink(blink)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs.draw = [[draw(VOID)
|
|
|
|
Redraws the window in the internal buffer.]]
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Window:draw()
|
2020-03-31 15:57:23 +00:00
|
|
|
self:clear()
|
|
|
|
self:drawChildren()
|
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:drawChildren()
|
|
|
|
for child in self:eachChild() do
|
|
|
|
if child.enabled then
|
|
|
|
child:draw()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs.getDoc = [[getDoc(STRING method)
|
|
|
|
Gets the documentation for a method.]]
|
|
|
|
function UI.Window:getDoc(method)
|
|
|
|
local m = getmetatable(self) -- get the class for this instance
|
|
|
|
repeat
|
|
|
|
if m.docs and m.docs[method] then
|
|
|
|
return m.docs[method]
|
|
|
|
end
|
|
|
|
m = m._base
|
|
|
|
until not m
|
|
|
|
end
|
|
|
|
|
|
|
|
UI.Window.docs.sync = [[sync(VOID)
|
|
|
|
Invoke a screen update. Automatically called at top level after an input event.
|
|
|
|
Call to force a screen update.]]
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Window:sync()
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.parent then
|
|
|
|
self.parent:sync()
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-01-28 22:54:00 +00:00
|
|
|
function UI.Window:enable(...)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.enabled = true
|
2020-03-31 15:57:23 +00:00
|
|
|
if self.transitionHint then
|
|
|
|
self:addTransition(self.transitionHint)
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.modal then
|
|
|
|
self:raise()
|
|
|
|
self:capture(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
for child in self:eachChild() do
|
|
|
|
child:enable(...)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:disable()
|
2018-01-24 22:39:38 +00:00
|
|
|
self.enabled = false
|
2020-03-31 15:57:23 +00:00
|
|
|
self.parent:dirty(true)
|
|
|
|
|
|
|
|
if self.modal then
|
|
|
|
self:release(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
for child in self:eachChild() do
|
|
|
|
child:disable()
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:setTextScale(textScale)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.textScale = textScale
|
|
|
|
self.parent:setTextScale(textScale)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs.clear = [[clear(opt COLOR bg, opt COLOR fg)
|
2020-03-31 15:57:23 +00:00
|
|
|
Clears the window using either the passed values or the defaults for that window.]]
|
2017-10-07 04:27:41 +00:00
|
|
|
function UI.Window:clear(bg, fg)
|
2020-03-31 15:57:23 +00:00
|
|
|
Canvas.clear(self, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:clearLine(y, bg)
|
2018-01-24 22:39:38 +00:00
|
|
|
self:write(1, y, _rep(' ', self.width), bg)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:clearArea(x, y, width, height, bg)
|
2020-03-31 15:57:23 +00:00
|
|
|
self:fillArea(x, y, width, height, ' ', bg)
|
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:fillArea(x, y, width, height, fillChar, bg, fg)
|
2018-01-24 22:39:38 +00:00
|
|
|
if width > 0 then
|
2020-03-31 15:57:23 +00:00
|
|
|
local filler = _rep(fillChar, width)
|
2018-01-24 22:39:38 +00:00
|
|
|
for i = 0, height - 1 do
|
2020-03-31 15:57:23 +00:00
|
|
|
self:write(x, y + i, filler, bg, fg)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
function UI.Window:write(x, y, text, bg, fg)
|
2020-03-31 15:57:23 +00:00
|
|
|
Canvas.write(self, x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:centeredWrite(y, text, bg, fg)
|
2018-01-24 22:39:38 +00:00
|
|
|
if #text >= self.width then
|
|
|
|
self:write(1, y, text, bg, fg)
|
|
|
|
else
|
|
|
|
local space = math.floor((self.width-#text) / 2)
|
|
|
|
local filler = _rep(' ', space + 1)
|
|
|
|
local str = _sub(filler, 1, space) .. text
|
|
|
|
str = str .. _sub(filler, self.width - #str + 1)
|
|
|
|
self:write(1, y, str, bg, fg)
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2017-10-08 03:03:18 +00:00
|
|
|
function UI.Window:print(text, bg, fg)
|
2018-01-24 22:39:38 +00:00
|
|
|
local marginLeft = self.marginLeft or 0
|
|
|
|
local marginRight = self.marginRight or 0
|
|
|
|
local width = self.width - marginLeft - marginRight
|
|
|
|
|
|
|
|
local function nextWord(line, cx)
|
|
|
|
local result = { line:find("(%w+)", cx) }
|
|
|
|
if #result > 1 and result[2] > cx then
|
|
|
|
return _sub(line, cx, result[2] + 1)
|
|
|
|
elseif #result > 0 and result[1] == cx then
|
|
|
|
result = { line:find("(%w+)", result[2]) }
|
|
|
|
if #result > 0 then
|
|
|
|
return _sub(line, cx, result[1] + 1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if cx <= #line then
|
|
|
|
return _sub(line, cx, #line)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function pieces(f, bg, fg)
|
|
|
|
local pos = 1
|
|
|
|
local t = { }
|
|
|
|
while true do
|
|
|
|
local s = string.find(f, '\027', pos, true)
|
|
|
|
if not s then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
if pos < s then
|
|
|
|
table.insert(t, _sub(f, pos, s - 1))
|
|
|
|
end
|
|
|
|
local seq = _sub(f, s)
|
|
|
|
seq = seq:match("\027%[([%d;]+)m")
|
|
|
|
local e = { }
|
|
|
|
for color in string.gmatch(seq, "%d+") do
|
|
|
|
color = tonumber(color)
|
|
|
|
if color == 0 then
|
|
|
|
e.fg = fg
|
|
|
|
e.bg = bg
|
|
|
|
elseif color > 20 then
|
|
|
|
e.bg = 2 ^ (color - 21)
|
|
|
|
else
|
|
|
|
e.fg = 2 ^ (color - 1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
table.insert(t, e)
|
|
|
|
pos = s + #seq + 3
|
|
|
|
end
|
|
|
|
if pos <= #f then
|
|
|
|
table.insert(t, _sub(f, pos))
|
|
|
|
end
|
|
|
|
return t
|
|
|
|
end
|
|
|
|
|
|
|
|
local lines = Util.split(text)
|
|
|
|
for k,line in pairs(lines) do
|
|
|
|
local fragments = pieces(line, bg, fg)
|
|
|
|
for _, fragment in ipairs(fragments) do
|
|
|
|
local lx = 1
|
|
|
|
if type(fragment) == 'table' then -- ansi sequence
|
|
|
|
fg = fragment.fg
|
|
|
|
bg = fragment.bg
|
|
|
|
else
|
|
|
|
while true do
|
|
|
|
local word = nextWord(fragment, lx)
|
|
|
|
if not word then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
local w = word
|
|
|
|
if self.cursorX + #word > width then
|
|
|
|
self.cursorX = marginLeft + 1
|
|
|
|
self.cursorY = self.cursorY + 1
|
|
|
|
w = word:gsub('^ ', '')
|
|
|
|
end
|
|
|
|
self:write(self.cursorX, self.cursorY, w, bg, fg)
|
|
|
|
self.cursorX = self.cursorX + #w
|
|
|
|
lx = lx + #word
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if lines[k + 1] then
|
|
|
|
self.cursorX = marginLeft + 1
|
|
|
|
self.cursorY = self.cursorY + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return self.cursorX, self.cursorY
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs.focus = [[focus(VOID)
|
|
|
|
If the function is present on a class, it indicates
|
|
|
|
that this element can accept focus. Called when receiving focus.]]
|
|
|
|
|
|
|
|
UI.Window.docs.setFocus = [[setFocus(ELEMENT el)
|
|
|
|
Set the page's focus to the passed element.]]
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Window:setFocus(focus)
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.parent then
|
|
|
|
self.parent:setFocus(focus)
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs.capture = [[capture(ELEMENT el)
|
|
|
|
Restricts input to the passed element's tree.]]
|
2017-12-13 06:37:31 +00:00
|
|
|
function UI.Window:capture(child)
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.parent then
|
|
|
|
self.parent:capture(child)
|
|
|
|
end
|
2017-12-13 06:37:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:release(child)
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.parent then
|
|
|
|
self.parent:release(child)
|
|
|
|
end
|
2017-12-13 06:37:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:pointToChild(x, y)
|
2019-02-06 04:03:57 +00:00
|
|
|
-- TODO: get rid of this offx/y mess and scroll canvas instead
|
2018-01-24 22:39:38 +00:00
|
|
|
x = x + self.offx - self.x + 1
|
|
|
|
y = y + self.offy - self.y + 1
|
|
|
|
if self.children then
|
2020-03-31 15:57:23 +00:00
|
|
|
for i = #self.children, 1, -1 do
|
|
|
|
local child = self.children[i]
|
2018-01-24 22:39:38 +00:00
|
|
|
if child.enabled and not child.inactive and
|
|
|
|
x >= child.x and x < child.x + child.width and
|
|
|
|
y >= child.y and y < child.y + child.height then
|
|
|
|
local c = child:pointToChild(x, y)
|
|
|
|
if c then
|
|
|
|
return c
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return {
|
|
|
|
element = self,
|
|
|
|
x = x,
|
|
|
|
y = y
|
|
|
|
}
|
2017-12-13 06:37:31 +00:00
|
|
|
end
|
|
|
|
|
2019-11-17 05:12:02 +00:00
|
|
|
UI.Window.docs.getFocusables = [[getFocusables(VOID)
|
|
|
|
Returns a list of children that can accept focus.]]
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Window:getFocusables()
|
2018-01-24 22:39:38 +00:00
|
|
|
local focusable = { }
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
local function focusSort(a, b)
|
|
|
|
if a.y == b.y then
|
|
|
|
return a.x < b.x
|
|
|
|
end
|
|
|
|
return a.y < b.y
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2019-02-06 04:03:57 +00:00
|
|
|
local function getFocusable(parent)
|
2018-01-24 22:39:38 +00:00
|
|
|
for _,child in Util.spairs(parent.children, focusSort) do
|
|
|
|
if child.enabled and child.focus and not child.inactive then
|
|
|
|
table.insert(focusable, child)
|
|
|
|
end
|
|
|
|
if child.children then
|
2019-02-06 04:03:57 +00:00
|
|
|
getFocusable(child)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.children then
|
|
|
|
getFocusable(self, self.x, self.y)
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
return focusable
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:focusFirst()
|
2018-01-24 22:39:38 +00:00
|
|
|
local focusables = self:getFocusables()
|
|
|
|
local focused = focusables[1]
|
|
|
|
if focused then
|
|
|
|
self:setFocus(focused)
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Window:scrollIntoView()
|
2018-01-24 22:39:38 +00:00
|
|
|
local parent = self.parent
|
2020-03-31 15:57:23 +00:00
|
|
|
local offx, offy = parent.offx, parent.offy
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.x <= parent.offx then
|
|
|
|
parent.offx = math.max(0, self.x - 1)
|
2020-03-31 15:57:23 +00:00
|
|
|
if offx ~= parent.offx then
|
|
|
|
parent:draw()
|
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
elseif self.x + self.width > parent.width + parent.offx then
|
|
|
|
parent.offx = self.x + self.width - parent.width - 1
|
2020-03-31 15:57:23 +00:00
|
|
|
if offx ~= parent.offx then
|
|
|
|
parent:draw()
|
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2019-02-10 02:41:51 +00:00
|
|
|
-- TODO: fix
|
|
|
|
local function setOffset(y)
|
|
|
|
parent.offy = y
|
2020-03-31 15:57:23 +00:00
|
|
|
if offy ~= parent.offy then
|
|
|
|
parent:draw()
|
2019-02-10 02:41:51 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.y <= parent.offy then
|
|
|
|
setOffset(math.max(0, self.y - 1))
|
2018-01-24 22:39:38 +00:00
|
|
|
elseif self.y + self.height > parent.height + parent.offy then
|
2019-02-10 02:41:51 +00:00
|
|
|
setOffset(self.y + self.height - parent.height - 1)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2017-10-03 04:50:54 +00:00
|
|
|
function UI.Window:addTransition(effect, args)
|
2018-01-24 22:39:38 +00:00
|
|
|
if self.parent then
|
|
|
|
args = args or { }
|
|
|
|
if not args.x then -- not good
|
2020-03-31 15:57:23 +00:00
|
|
|
args.x, args.y = self.x, self.y
|
2018-01-24 22:39:38 +00:00
|
|
|
args.width = self.width
|
|
|
|
args.height = self.height
|
2020-03-31 15:57:23 +00:00
|
|
|
args.canvas = self
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2017-10-03 04:50:54 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
self.parent:addTransition(effect, args)
|
|
|
|
end
|
2016-12-14 17:36:28 +00:00
|
|
|
end
|
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Window:emit(event)
|
2018-01-24 22:39:38 +00:00
|
|
|
local parent = self
|
|
|
|
while parent do
|
2019-11-14 04:50:00 +00:00
|
|
|
if parent.accelerators then
|
|
|
|
-- events types can be made unique via accelerators
|
|
|
|
local acc = parent.accelerators[event.key or event.type]
|
|
|
|
if acc and acc ~= event.type then -- don't get stuck in a loop
|
|
|
|
local event2 = Util.shallowCopy(event)
|
|
|
|
event2.type = acc
|
|
|
|
event2.key = nil
|
|
|
|
if parent:emit(event2) then
|
|
|
|
return true
|
|
|
|
end
|
2019-11-13 22:17:23 +00:00
|
|
|
end
|
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
if parent.eventHandler then
|
|
|
|
if parent:eventHandler(event) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
parent = parent.parent
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-07-11 01:02:46 +00:00
|
|
|
function UI.Window:getProperty(property)
|
|
|
|
return self[property] or self.parent and self.parent:getProperty(property)
|
|
|
|
end
|
|
|
|
|
2017-10-18 23:51:55 +00:00
|
|
|
function UI.Window:find(uid)
|
2020-03-31 15:57:23 +00:00
|
|
|
local el = self.children and Util.find(self.children, 'uid', uid)
|
|
|
|
if not el then
|
|
|
|
for child in self:eachChild() do
|
|
|
|
el = child:find(uid)
|
|
|
|
if el then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return el
|
2017-10-18 23:51:55 +00:00
|
|
|
end
|
|
|
|
|
2019-02-06 04:03:57 +00:00
|
|
|
function UI.Window:eventHandler()
|
2018-01-24 22:39:38 +00:00
|
|
|
return false
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--[[-- Terminal for computer / advanced computer / monitor --]]--
|
|
|
|
UI.Device = class(UI.Window)
|
|
|
|
UI.Device.defaults = {
|
2018-01-24 22:39:38 +00:00
|
|
|
UIElement = 'Device',
|
|
|
|
backgroundColor = colors.black,
|
|
|
|
textColor = colors.white,
|
|
|
|
textScale = 1,
|
|
|
|
effectsEnabled = true,
|
2016-12-11 19:24:52 +00:00
|
|
|
}
|
2017-10-11 15:37:52 +00:00
|
|
|
function UI.Device:postInit()
|
2018-01-24 22:39:38 +00:00
|
|
|
self.device = self.device or term.current()
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if not self.device.setTextScale then
|
|
|
|
self.device.setTextScale = function() end
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
self.device.setTextScale(self.textScale)
|
|
|
|
self.width, self.height = self.device.getSize()
|
|
|
|
self.isColor = self.device.isColor()
|
2020-03-31 15:57:23 +00:00
|
|
|
Canvas.init(self, { isColor = self.isColor })
|
2016-12-23 04:22:04 +00:00
|
|
|
|
2019-01-02 15:33:47 +00:00
|
|
|
UI.devices[self.device.side or 'terminal'] = self
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Device:resize()
|
2018-12-05 01:55:08 +00:00
|
|
|
self.device.setTextScale(self.textScale)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.width, self.height = self.device.getSize()
|
|
|
|
self.lines = { }
|
2019-01-30 20:11:41 +00:00
|
|
|
-- TODO: resize all pages added to this device
|
2020-03-31 15:57:23 +00:00
|
|
|
Canvas.resize(self, self.width, self.height)
|
|
|
|
Canvas.clear(self, self.backgroundColor, self.textColor)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Device:setCursorPos(x, y)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.cursorX = x
|
|
|
|
self.cursorY = y
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Device:getCursorBlink()
|
2018-01-24 22:39:38 +00:00
|
|
|
return self.cursorBlink
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Device:setCursorBlink(blink)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.cursorBlink = blink
|
|
|
|
self.device.setCursorBlink(blink)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Device:setTextScale(textScale)
|
2018-01-24 22:39:38 +00:00
|
|
|
self.textScale = textScale
|
|
|
|
self.device.setTextScale(self.textScale)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function UI.Device:reset()
|
2018-01-24 22:39:38 +00:00
|
|
|
self.device.setBackgroundColor(colors.black)
|
|
|
|
self.device.setTextColor(colors.white)
|
|
|
|
self.device.clear()
|
|
|
|
self.device.setCursorPos(1, 1)
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2017-10-03 04:50:54 +00:00
|
|
|
function UI.Device:addTransition(effect, args)
|
2018-01-24 22:39:38 +00:00
|
|
|
if not self.transitions then
|
|
|
|
self.transitions = { }
|
|
|
|
end
|
2016-12-18 19:38:48 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
args = args or { }
|
|
|
|
args.ex = args.x + args.width - 1
|
|
|
|
args.ey = args.y + args.height - 1
|
2020-03-31 15:57:23 +00:00
|
|
|
args.canvas = args.canvas or self
|
2017-10-03 04:50:54 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if type(effect) == 'string' then
|
|
|
|
effect = Transition[effect]
|
|
|
|
if not effect then
|
|
|
|
error('Invalid transition')
|
|
|
|
end
|
|
|
|
end
|
2017-10-03 04:50:54 +00:00
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
table.insert(self.transitions, { effect = effect, args = args })
|
2016-12-14 17:36:28 +00:00
|
|
|
end
|
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
function UI.Device:runTransitions(transitions)
|
|
|
|
for _,k in pairs(transitions) do
|
|
|
|
k.update = k.effect(k.args)
|
|
|
|
end
|
2018-01-24 22:39:38 +00:00
|
|
|
while true do
|
|
|
|
for _,k in ipairs(Util.keys(transitions)) do
|
|
|
|
local transition = transitions[k]
|
2019-02-06 04:03:57 +00:00
|
|
|
if not transition.update() then
|
2018-01-24 22:39:38 +00:00
|
|
|
transitions[k] = nil
|
|
|
|
end
|
|
|
|
end
|
2020-03-31 15:57:23 +00:00
|
|
|
self.currentPage:render(self.device)
|
2018-01-24 22:39:38 +00:00
|
|
|
if Util.empty(transitions) then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
os.sleep(0)
|
|
|
|
end
|
2016-12-14 17:36:28 +00:00
|
|
|
end
|
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
function UI.Device:sync()
|
2019-02-06 04:03:57 +00:00
|
|
|
local transitions = self.effectsEnabled and self.transitions
|
|
|
|
self.transitions = nil
|
2016-12-14 17:36:28 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if self:getCursorBlink() then
|
|
|
|
self.device.setCursorBlink(false)
|
|
|
|
end
|
2016-12-14 17:36:28 +00:00
|
|
|
|
2020-03-31 15:57:23 +00:00
|
|
|
self.currentPage:render(self.device)
|
2018-01-24 22:39:38 +00:00
|
|
|
if transitions then
|
2020-03-31 15:57:23 +00:00
|
|
|
self:runTransitions(transitions)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-14 17:36:28 +00:00
|
|
|
|
2018-01-24 22:39:38 +00:00
|
|
|
if self:getCursorBlink() then
|
|
|
|
self.device.setCursorPos(self.cursorX, self.cursorY)
|
|
|
|
self.device.setCursorBlink(true)
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
|
|
|
|
2019-11-18 21:32:10 +00:00
|
|
|
-- lazy load components
|
2019-02-06 04:03:57 +00:00
|
|
|
local function loadComponents()
|
|
|
|
local function load(name)
|
2019-06-28 17:50:02 +00:00
|
|
|
local s, m = Util.run(_ENV, 'sys/modules/opus/ui/components/' .. name .. '.lua')
|
2019-02-06 04:03:57 +00:00
|
|
|
if not s then
|
|
|
|
error(m)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-02-06 04:03:57 +00:00
|
|
|
if UI[name]._preload then
|
|
|
|
error('Error loading UI.' .. name)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-02-06 04:03:57 +00:00
|
|
|
if UI.theme[name] and UI[name].defaults then
|
|
|
|
Util.merge(UI[name].defaults, UI.theme[name])
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-02-06 04:03:57 +00:00
|
|
|
return UI[name]
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2019-06-28 17:50:02 +00:00
|
|
|
local components = fs.list('sys/modules/opus/ui/components')
|
2019-02-06 04:03:57 +00:00
|
|
|
for _, f in pairs(components) do
|
|
|
|
local name = f:match('(.+)%.')
|
2016-12-11 19:24:52 +00:00
|
|
|
|
2019-02-06 04:03:57 +00:00
|
|
|
UI[name] = setmetatable({ }, {
|
|
|
|
__call = function(self, ...)
|
|
|
|
load(name)
|
|
|
|
setmetatable(self, getmetatable(UI[name]))
|
|
|
|
return self(...)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
2019-02-06 04:03:57 +00:00
|
|
|
})
|
|
|
|
UI[name]._preload = function(self)
|
|
|
|
return load(name)
|
2018-01-24 22:39:38 +00:00
|
|
|
end
|
|
|
|
end
|
2016-12-11 19:24:52 +00:00
|
|
|
end
|
2017-10-07 04:27:41 +00:00
|
|
|
|
2019-02-06 04:03:57 +00:00
|
|
|
loadComponents()
|
2019-03-24 07:44:38 +00:00
|
|
|
UI:loadTheme('usr/config/ui.theme')
|
2019-07-10 17:06:29 +00:00
|
|
|
Util.merge(UI.Window.defaults, UI.theme.Window)
|
2016-12-15 14:45:27 +00:00
|
|
|
UI:setDefaultDevice(UI.Device({ device = term.current() }))
|
|
|
|
|
2016-12-11 19:24:52 +00:00
|
|
|
return UI
|