opus/sys/modules/opus/ui.lua

1125 lines
25 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 = { }
self.extChars = Util.getVersion() >= 1.76
self.colors = {
primary = colors.green,
secondary = colors.lightGray,
tertiary = colors.gray,
}
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)
local ie = Input:translate('mouse_scroll', direction, x, y)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
if currentPage then
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)
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
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
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
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
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)
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(...)
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
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
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
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')
2019-07-10 17:06:29 +00:00
Util.merge(UI.Window.defaults, UI.theme.Window)
Util.merge(UI.colors, UI.theme.colors)
UI:setDefaultDevice(UI.Device())
for k,v in pairs(UI.colors) do
Canvas.colorPalette[k] = Canvas.colorPalette[v]
Canvas.grayscalePalette[k] = Canvas.grayscalePalette[v]
end
2016-12-15 14:45:27 +00:00
2016-12-11 19:24:52 +00:00
return UI