opus/sys/modules/opus/ui.lua

1132 lines
26 KiB
Lua
Raw Normal View History

local Array = require('opus.array')
local Blit = require('opus.ui.blit')
local Canvas = require('opus.ui.canvas')
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')
2016-12-11 19:24:52 +00:00
2017-10-11 15:37:52 +00:00
local _rep = string.rep
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
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 UI = { }
function UI:init()
2019-01-02 15:33:47 +00:00
self.devices = { }
self.theme = {
colors = {
primary = colors.green,
secondary = colors.lightGray,
tertiary = colors.gray,
}
}
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
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
dev:resize()
2019-01-02 15:33:47 +00:00
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, side)
local ie = Input:translate('mouse_scroll', direction, x, y)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
if currentPage and currentPage.parent.device.side == side then
2019-01-02 15:33:47 +00:00
local event = currentPage:pointToChild(x, y)
event.type = ie.code
event.ie = { code = ie.code, x = event.x, y = event.y }
event.element:emit(event)
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)
self:click(dev.currentPage, ie)
2018-01-24 22:39:38 +00:00
end
end,
mouse_click = function(_, button, x, y, side)
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 and currentPage.parent.device.side == side then
local event = currentPage:pointToChild(x, y)
if event.element.focus and not event.element.inactive then
currentPage:setFocus(event.element)
currentPage:sync()
2018-01-24 22:39:38 +00:00
end
self:click(currentPage, ie)
2018-01-24 22:39:38 +00:00
end
end,
mouse_up = function(_, button, x, y, side)
2018-01-24 22:39:38 +00:00
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)
_ENV.multishell.openTab(_ENV, {
2018-01-24 22:39:38 +00:00
path = 'sys/apps/Lua.lua',
2020-05-05 23:25:58 +00:00
args = { event.element, self, _ENV },
2018-01-24 22:39:38 +00:00
focused = true })
elseif ie and currentPage and currentPage.parent.device.side == side then
self:click(currentPage, ie)
2018-01-24 22:39:38 +00:00
end
end,
mouse_drag = function(_, button, x, y, side)
2018-01-24 22:39:38 +00:00
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
if ie and currentPage and currentPage.parent.device.side == side then
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 UI: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
Util.deepMerge(self.theme, defaults.theme)
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function UI:disableEffects()
self.term.effectsEnabled = false
2016-12-15 14:45:27 +00:00
end
function UI: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
for k,v in pairs(self.theme.colors) do
Canvas.colorPalette[k] = Canvas.colorPalette[v]
Canvas.grayscalePalette[k] = Canvas.grayscalePalette[v]
end
2016-12-11 19:24:52 +00:00
end
function UI:generateTheme(filename)
local t = { }
local function getName(d)
if type(d) == 'string' then
return string.format("'%s'", d)
end
for c, n in pairs(colors) do
if n == d then
return 'colors.' .. c
end
end
end
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
t[k][p] = getName(d)
end
end
end
end
end
t.colors = {
primary = getName(self.colors.primary),
secondary = getName(self.colors.secondary),
tertiary = getName(self.colors.tertiary),
}
Util.writeFile(filename, textutils.serialize(t):gsub('(")', ''))
end
function UI: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
function UI:click(target, ie)
local clickEvent
2016-12-11 19:24:52 +00:00
if ie.code == 'mouse_drag' then
local function getPosition(element, x, y)
repeat
x = x - element.x + 1
y = y - element.y + 1
element = element.parent
until not element
return x, y
end
local x, y = getPosition(self.lastClicked, ie.x, ie.y)
2018-01-24 22:39:38 +00:00
clickEvent = {
element = self.lastClicked,
x = x,
y = y,
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
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
self.lastClicked = clickEvent.element
2019-01-02 15:33:47 +00:00
end
2018-01-24 22:39:38 +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
clickEvent.element:emit(clickEvent)
2019-01-02 15:33:47 +00:00
target:sync()
2016-12-11 19:24:52 +00:00
end
function UI:setDefaultDevice(dev)
2018-01-24 22:39:38 +00:00
self.term = dev
2016-12-11 19:24:52 +00:00
end
function UI: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 UI:setPages(pages)
2018-01-24 22:39:38 +00:00
self.pages = pages
2016-12-11 19:24:52 +00:00
end
function UI: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
function UI:getActivePage(page)
2019-01-02 15:33:47 +00:00
if page then
return page.parent.currentPage
end
return self.term.currentPage
2019-01-02 15:33:47 +00:00
end
function UI:setActivePage(page)
2019-01-02 15:33:47 +00:00
page.parent.currentPage = page
end
function UI: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)
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 UI:getCurrentPage()
return self.term.currentPage
2016-12-11 19:24:52 +00:00
end
function UI:setPreviousPage()
if self.term.currentPage.previousPage then
local previousPage = self.term.currentPage.previousPage.previousPage
self:setPage(self.term.currentPage.previousPage)
self.term.currentPage.previousPage = previousPage
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function UI:getDefaults(element, args)
2018-01-24 22:39:38 +00:00
local defaults = Util.deepCopy(element.defaults)
if args then
UI:mergeProperties(defaults, args)
2018-01-24 22:39:38 +00:00
end
return defaults
2016-12-11 19:24:52 +00:00
end
function UI: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
function UI: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
UI.exitPullEvents = Event.exitPullEvents
UI.quit = Event.exitPullEvents
UI.start = UI.pullEvents
2017-10-12 21:58:35 +00:00
UI:init()
2016-12-11 19:24:52 +00:00
--[[-- Basic drawable area --]]--
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,
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
if m ~= Canvas 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
UI.Window.docs.postInit = [[postInit(VOID)
Called once the window has all the properties set.
Override to calculate properties or to dynamically add children]]
2017-10-11 15:37:52 +00:00
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()
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 %
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
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
self.width = math.max(self.width, 1)
self.height = math.max(self.height, 1)
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
self.oex, self.oey = self.ex, self.ey
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
self.ex, self.ey = self.oex, self.oey
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
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
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
UI.Window.docs.raise = [[raise(VOID)
Raise this window to the top]]
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
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)
self.cursorBlink = 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()
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)
2020-04-27 01:39:58 +00:00
Get the documentation for a method.]]
2019-11-17 05:12:02 +00:00
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(...)
if not self.enabled then
self.enabled = true
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
if not child.enabled then
child:enable(...)
end
2018-01-24 22:39:38 +00:00
end
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:disable()
if self.enabled then
self.enabled = false
self.parent:dirty(true)
if self.modal then
self:release(self)
end
for child in self:eachChild() do
if child.enabled then
child:disable()
end
2018-01-24 22:39:38 +00:00
end
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)
Clears the window using either the passed values or the defaults for that window.]]
function UI.Window:clear(bg, fg)
Canvas.clear(self, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
2016-12-11 19:24:52 +00:00
end
2020-04-27 01:39:58 +00:00
UI.Window.docs.clearLine = [[clearLine(NUMBER y, opt COLOR bg)
Clears the specified line.]]
2016-12-11 19:24:52 +00:00
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)
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
local filler = _rep(fillChar, width)
2018-01-24 22:39:38 +00:00
for i = 0, height - 1 do
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
2020-05-31 02:05:53 +00:00
UI.Window.docs.write = [[write(NUMBER x, NUMBER y, STRING text, opt COLOR bg, opt COLOR fg)
2020-04-27 01:39:58 +00:00
Write text to the canvas.
If colors are not specified, the colors from the base class will be used.
If the base class does not have colors defined, colors will be inherited from the parent container.]]
2019-11-17 05:12:02 +00:00
function UI.Window:write(x, y, text, bg, fg)
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 x = math.floor((self.width-#text) / 2) + 1
self:write(x, y, text, bg, fg)
2018-01-24 22:39:38 +00:00
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 cs = {
bg = bg or self:getProperty('backgroundColor'),
fg = fg or self:getProperty('textColor'),
palette = self.palette,
}
2018-01-24 22:39:38 +00:00
local y = (self.marginTop or 0) + 1
for _,line in pairs(Util.split(text)) do
for _, ln in ipairs(Blit(line, cs):wrap(width)) do
self:blit(marginLeft + 1, y, ln.text, ln.bg, ln.fg)
y = y + 1
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.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)
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
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)
2018-01-24 22:39:38 +00:00
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
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)
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
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
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
function UI.Window:addTransition(effect, args, canvas)
self.parent:addTransition(effect, args, canvas or self)
2016-12-14 17:36:28 +00:00
end
UI.Window.docs.emit = [[emit(TABLE event)
Send an event to the element. The event handler for the element is called.
If the event handler returns true, then no further processing is done.
If the event handler does not return true, then the event is sent to the parent element
and continues up the element tree.
If an accelerator is defined, the accelerated event is processed in the same manner.
Accelerators are useful for making events unique.]]
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
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)
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()
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
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
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
function UI.Device:addTransition(effect, args, canvas)
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
if type(effect) == 'string' then
effect = Transition[effect] or error('Invalid transition')
end
-- there can be only one
for k,v in pairs(self.transitions) do
if v.canvas == canvas then
table.remove(self.transitions, k)
break
2018-01-24 22:39:38 +00:00
end
end
2017-10-03 04:50:54 +00:00
table.insert(self.transitions, { effect = effect, args = args or { }, canvas = canvas })
2016-12-14 17:36:28 +00:00
end
function UI.Device:runTransitions(transitions)
for _,k in pairs(transitions) do
k.update = k.effect(k.canvas, 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
self.currentPage:render(self, true)
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
self.device.setCursorBlink(false)
2016-12-14 17:36:28 +00:00
2018-01-24 22:39:38 +00:00
if transitions then
self:runTransitions(transitions)
else
self.currentPage:render(self, true)
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)
if self.isColor then
self.device.setTextColor(colors.orange)
end
2018-01-24 22:39:38 +00:00
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)
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
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()
2019-02-06 04:03:57 +00:00
return load(name)
2018-01-24 22:39:38 +00:00
end
end
2016-12-11 19:24:52 +00:00
end
2019-02-06 04:03:57 +00:00
loadComponents()
2019-03-24 07:44:38 +00:00
UI:loadTheme('usr/config/ui.theme')
UI:setDefaultDevice(UI.Device())
2016-12-11 19:24:52 +00:00
return UI