mirror of
https://github.com/kepler155c/opus
synced 2024-11-04 16:06:16 +00:00
3440 lines
80 KiB
Lua
3440 lines
80 KiB
Lua
local Util = require('util')
|
|
local class = require('class')
|
|
local Event = require('event')
|
|
local Tween = require('ui.tween')
|
|
local Region = require('ui.region')
|
|
|
|
local mapColorToGray = {
|
|
[ colors.white ] = colors.white,
|
|
[ colors.orange ] = colors.lightGray,
|
|
[ colors.magenta ] = colors.lightGray,
|
|
[ colors.lightBlue ] = colors.lightGray,
|
|
[ colors.yellow ] = colors.lightGray,
|
|
[ colors.lime ] = colors.lightGray,
|
|
[ colors.pink ] = colors.lightGray,
|
|
[ colors.gray ] = colors.gray,
|
|
[ colors.lightGray ] = colors.lightGray,
|
|
[ colors.cyan ] = colors.lightGray,
|
|
[ colors.purple ] = colors.gray,
|
|
[ colors.blue ] = colors.gray,
|
|
[ colors.brown ] = colors.gray,
|
|
[ colors.green ] = colors.lightGray,
|
|
[ colors.red ] = colors.gray,
|
|
[ colors.black ] = colors.black,
|
|
}
|
|
|
|
local mapColorToPaint = { }
|
|
for n = 1, 16 do
|
|
mapColorToPaint[2 ^ (n - 1)] = string.sub("0123456789abcdef", n, n)
|
|
end
|
|
|
|
local mapGrayToPaint = { }
|
|
for n = 0, 15 do
|
|
local gs = mapColorToGray[2 ^ n]
|
|
mapGrayToPaint[2 ^ n] = mapColorToPaint[gs]
|
|
end
|
|
|
|
local function safeValue(v)
|
|
local t = type(v)
|
|
if t == 'string' or t == 'number' then
|
|
return v
|
|
end
|
|
return tostring(v)
|
|
end
|
|
|
|
-- need to add offsets to this test
|
|
local function getPosition(element)
|
|
local x, y = 1, 1
|
|
repeat
|
|
x = element.x + x - 1
|
|
y = element.y + y - 1
|
|
element = element.parent
|
|
until not element
|
|
return x, y
|
|
end
|
|
|
|
local function assertElement(el, msg)
|
|
if not el or not type(el) == 'table' or not el.UIElement then
|
|
error(msg, 3)
|
|
end
|
|
end
|
|
|
|
--[[-- Top Level Manager --]]--
|
|
local Manager = class()
|
|
function Manager:init(args)
|
|
local control = false
|
|
local shift = false
|
|
local mouseDragged = false
|
|
local pages = { }
|
|
local running = false
|
|
|
|
-- single thread all input events
|
|
local function singleThread(event, fn)
|
|
Event.on(event, function(...)
|
|
if not running then
|
|
running = true
|
|
fn(...)
|
|
running = false
|
|
end
|
|
end)
|
|
end
|
|
|
|
singleThread('term_resize', function(h, side)
|
|
if self.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
|
|
if self.currentPage.parent.device.side == side then
|
|
self.currentPage.parent:resize()
|
|
self.currentPage:resize()
|
|
self.currentPage:draw()
|
|
self.currentPage:sync()
|
|
end
|
|
end
|
|
end)
|
|
|
|
singleThread('mouse_scroll', function(h, direction, x, y)
|
|
if self.target then
|
|
local event = self:pointToChild(self.target, x, y)
|
|
local directions = {
|
|
[ -1 ] = 'up',
|
|
[ 1 ] = 'down'
|
|
}
|
|
-- revisit - should send out scroll_up and scroll_down events
|
|
-- let the element convert them to up / down
|
|
self:inputEvent(event.element,
|
|
{ type = 'key', key = directions[direction] })
|
|
self.currentPage:sync()
|
|
end
|
|
end)
|
|
|
|
-- this should be moved to the device !
|
|
singleThread('monitor_touch', function(h, side, x, y)
|
|
if self.currentPage then
|
|
if self.currentPage.parent.device.side == side then
|
|
self:click(1, x, y)
|
|
end
|
|
end
|
|
end)
|
|
|
|
singleThread('mouse_click', function(h, button, x, y)
|
|
|
|
mouseDragged = false
|
|
if button == 1 and shift and control then -- debug hack
|
|
local event = self:pointToChild(self.target, x, y)
|
|
multishell.openTab({ path = 'sys/apps/Lua.lua', args = { event.element }, focused = true })
|
|
|
|
elseif self.currentPage then
|
|
if not self.currentPage.parent.device.side then
|
|
local event = self:pointToChild(self.target, x, y)
|
|
if event.element.focus then
|
|
self.currentPage:setFocus(event.element)
|
|
self.currentPage:sync()
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
singleThread('mouse_up', function(h, button, x, y)
|
|
|
|
if self.currentPage and not mouseDragged then
|
|
if not self.currentPage.parent.device.side then
|
|
self:click(button, x, y)
|
|
end
|
|
end
|
|
end)
|
|
|
|
singleThread('mouse_drag', function(h, button, x, y)
|
|
|
|
mouseDragged = true
|
|
if self.target then
|
|
local event = self:pointToChild(self.target, x, y)
|
|
|
|
-- revisit - should send out scroll_up and scroll_down events
|
|
-- let the element convert them to up / down
|
|
self:inputEvent(event.element,
|
|
{ type = 'mouse_drag', button = button, x = event.x, y = event.y })
|
|
self.currentPage:sync()
|
|
end
|
|
end)
|
|
|
|
singleThread('paste', function(h, text)
|
|
if clipboard.isInternal() then
|
|
text = clipboard.getData()
|
|
end
|
|
if text and type(text) == 'string' then
|
|
self:emitEvent({ type = 'paste', text = text })
|
|
self.currentPage:sync()
|
|
end
|
|
end)
|
|
|
|
singleThread('char', function(h, ch)
|
|
control = false
|
|
if self.currentPage then
|
|
self:inputEvent(self.currentPage.focused, { type = 'key', key = ch })
|
|
self.currentPage:sync()
|
|
end
|
|
end)
|
|
|
|
singleThread('key_up', function(h, code)
|
|
if code == keys.leftCtrl or code == keys.rightCtrl then
|
|
control = false
|
|
elseif code == keys.leftShift or code == keys.rightShift then
|
|
shift = false
|
|
end
|
|
end)
|
|
|
|
singleThread('key', function(h, code)
|
|
local ch = keys.getName(code)
|
|
if not ch then
|
|
return
|
|
end
|
|
|
|
if code == keys.leftCtrl or code == keys.rightCtrl then
|
|
control = true
|
|
elseif code == keys.leftShift or code == keys.rightShift then
|
|
shift = true
|
|
elseif control then
|
|
ch = 'control-' .. ch
|
|
elseif shift and ch == 'tab' then
|
|
ch = 'shiftTab'
|
|
end
|
|
|
|
-- filter out a through z and numbers as they will be get picked up
|
|
-- as char events
|
|
if ch and #ch > 1 and (code < 2 or code > 11) then
|
|
if self.currentPage then
|
|
self:inputEvent(self.currentPage.focused,
|
|
{ type = 'key', key = ch, element = self.currentPage.focused })
|
|
self.currentPage:sync()
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
function Manager:configure(appName, ...)
|
|
local options = {
|
|
device = { arg = 'd', type = 'string',
|
|
desc = 'Device type' },
|
|
textScale = { arg = 't', type = 'number',
|
|
desc = 'Text scale' },
|
|
}
|
|
local defaults = Util.loadTable('usr/config/' .. appName) or { }
|
|
if not defaults.device then
|
|
defaults.device = { }
|
|
end
|
|
|
|
Util.getOptions(options, { ... }, true)
|
|
local optionValues = {
|
|
name = options.device.value,
|
|
textScale = options.textScale.value,
|
|
}
|
|
|
|
Util.merge(defaults.device, optionValues)
|
|
|
|
if defaults.device.name then
|
|
|
|
local dev
|
|
|
|
if defaults.device.name == 'terminal' then
|
|
dev = term.current()
|
|
else
|
|
dev = device[defaults.device.name]
|
|
end
|
|
|
|
if not dev then
|
|
error('Invalid display device')
|
|
end
|
|
local device = self.Device({
|
|
device = dev,
|
|
textScale = defaults.device.textScale,
|
|
})
|
|
self:setDefaultDevice(device)
|
|
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
|
|
end
|
|
|
|
function Manager:disableEffects()
|
|
self.defaultDevice.effectsEnabled = false
|
|
end
|
|
|
|
function Manager:loadTheme(filename)
|
|
if fs.exists(filename) then
|
|
local theme, err = Util.loadTable(filename)
|
|
if not theme then
|
|
error(err)
|
|
end
|
|
for k,v in pairs(theme) do
|
|
if self[k] and self[k].defaults then
|
|
Util.merge(self[k].defaults, v)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Manager:emitEvent(event)
|
|
if self.currentPage and self.currentPage.focused then
|
|
return self.currentPage.focused:emit(event)
|
|
end
|
|
end
|
|
|
|
function Manager:inputEvent(parent, event)
|
|
|
|
while parent do
|
|
local acc = parent.accelerators[event.key]
|
|
if acc then
|
|
if parent:emit({ type = acc, element = parent }) then
|
|
return true
|
|
end
|
|
end
|
|
if parent.eventHandler then
|
|
if parent:eventHandler(event) then
|
|
return true
|
|
end
|
|
end
|
|
parent = parent.parent
|
|
end
|
|
end
|
|
|
|
function Manager:pointToChild(parent, x, y)
|
|
x = x + parent.offx - parent.x + 1
|
|
y = y + parent.offy - parent.y + 1
|
|
if parent.children then
|
|
for _,child in pairs(parent.children) do
|
|
if child.enabled and
|
|
x >= child.x and x < child.x + child.width and
|
|
y >= child.y and y < child.y + child.height then
|
|
local c = self:pointToChild(child, x, y)
|
|
if c then
|
|
return c
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return {
|
|
element = parent,
|
|
x = x,
|
|
y = y
|
|
}
|
|
end
|
|
|
|
function Manager:click(button, x, y)
|
|
if self.target then
|
|
|
|
local target = self.target
|
|
|
|
-- need to add offsets into this check
|
|
if x < self.target.x or y < self.target.y or
|
|
x > self.target.x + self.target.width - 1 or
|
|
y > self.target.y + self.target.height - 1 then
|
|
target:emit({ type = 'mouse_out' })
|
|
|
|
target = self.currentPage
|
|
end
|
|
|
|
local clickEvent = self:pointToChild(target, x, y)
|
|
|
|
if button == 1 then
|
|
local c = os.clock()
|
|
|
|
--if self.doubleClickTimer then
|
|
-- debug(c - self.doubleClickTimer)
|
|
--end
|
|
|
|
if self.doubleClickTimer and (c - self.doubleClickTimer < 1.9) and
|
|
self.doubleClickX == x and self.doubleClickY == y and
|
|
self.doubleClickElement == clickEvent.element then
|
|
button = 3
|
|
self.doubleClickTimer = nil
|
|
else
|
|
self.doubleClickTimer = c
|
|
self.doubleClickX = x
|
|
self.doubleClickY = y
|
|
self.doubleClickElement = clickEvent.element
|
|
end
|
|
else
|
|
self.doubleClickTimer = nil
|
|
end
|
|
|
|
local events = { 'mouse_click', 'mouse_rightclick', 'mouse_doubleclick' }
|
|
|
|
clickEvent.button = button
|
|
clickEvent.type = events[button]
|
|
clickEvent.key = events[button]
|
|
|
|
if clickEvent.element.focus then
|
|
self.currentPage:setFocus(clickEvent.element)
|
|
end
|
|
if not self:inputEvent(clickEvent.element, clickEvent) then
|
|
if button == 3 then
|
|
-- if the double-click was not captured
|
|
-- send through a single-click
|
|
clickEvent.button = 1
|
|
clickEvent.type = events[1]
|
|
clickEvent.key = events[1]
|
|
self:inputEvent(clickEvent.element, clickEvent)
|
|
end
|
|
end
|
|
|
|
self.currentPage:sync()
|
|
end
|
|
end
|
|
|
|
function Manager:setDefaultDevice(device)
|
|
self.defaultDevice = device
|
|
self.term = device
|
|
end
|
|
|
|
function Manager:addPage(name, page)
|
|
self.pages[name] = page
|
|
end
|
|
|
|
function Manager:setPages(pages)
|
|
self.pages = pages
|
|
end
|
|
|
|
function Manager:getPage(pageName)
|
|
local page = self.pages[pageName]
|
|
|
|
if not page then
|
|
error('UI:getPage: Invalid page: ' .. tostring(pageName), 2)
|
|
end
|
|
|
|
return page
|
|
end
|
|
|
|
function Manager:setPage(pageOrName, ...)
|
|
local page = pageOrName
|
|
|
|
if type(pageOrName) == 'string' then
|
|
page = self.pages[pageOrName]
|
|
end
|
|
|
|
if page == self.currentPage then
|
|
page:draw()
|
|
else
|
|
local needSync
|
|
if self.currentPage then
|
|
if self.currentPage.focused then
|
|
self.currentPage.focused.focused = false
|
|
self.currentPage.focused:focus()
|
|
end
|
|
self.currentPage:disable()
|
|
page.previousPage = self.currentPage
|
|
else
|
|
needSync = true
|
|
end
|
|
self.currentPage = page
|
|
self.currentPage:clear(page.backgroundColor)
|
|
page:enable(...)
|
|
page:draw()
|
|
if self.currentPage.focused then
|
|
self.currentPage.focused.focused = true
|
|
self.currentPage.focused:focus()
|
|
end
|
|
self:capture(self.currentPage)
|
|
if needSync then
|
|
page:sync() -- first time a page has been set
|
|
end
|
|
end
|
|
end
|
|
|
|
function Manager:getCurrentPage()
|
|
return self.currentPage
|
|
end
|
|
|
|
function Manager:setPreviousPage()
|
|
if self.currentPage.previousPage then
|
|
local previousPage = self.currentPage.previousPage.previousPage
|
|
self:setPage(self.currentPage.previousPage)
|
|
self.currentPage.previousPage = previousPage
|
|
end
|
|
end
|
|
|
|
function Manager:capture(child)
|
|
self.target = child
|
|
end
|
|
|
|
function Manager:release(child)
|
|
if self.target == child then
|
|
self.target = self.currentPage
|
|
end
|
|
end
|
|
|
|
function Manager:getDefaults(element, args)
|
|
local defaults = Util.deepCopy(element.defaults)
|
|
if args then
|
|
Manager.setProperties(defaults, args)
|
|
end
|
|
return defaults
|
|
end
|
|
|
|
function Manager:pullEvents(...)
|
|
Event.pullEvents(...)
|
|
self.term:reset()
|
|
end
|
|
|
|
function Manager:exitPullEvents()
|
|
Event.exitPullEvents()
|
|
end
|
|
|
|
-- inconsistent
|
|
function Manager.setProperties(obj, args)
|
|
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
|
|
end
|
|
|
|
local UI = Manager()
|
|
|
|
--[[-- Basic drawable area --]]--
|
|
UI.Window = class()
|
|
UI.Window.defaults = {
|
|
UIElement = 'Window',
|
|
x = 1,
|
|
y = 1,
|
|
-- z = 0, -- eventually...
|
|
offx = 0,
|
|
offy = 0,
|
|
cursorX = 1,
|
|
cursorY = 1,
|
|
accelerators = { },
|
|
}
|
|
function UI.Window:init(args)
|
|
local defaults = UI:getDefaults(UI.Window, args)
|
|
UI.setProperties(self, defaults)
|
|
|
|
if self.parent then
|
|
self:setParent()
|
|
end
|
|
end
|
|
|
|
function UI.Window:initChildren()
|
|
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()
|
|
-- child:reposition() -- maybe
|
|
if self.enabled then
|
|
child:enable()
|
|
end
|
|
end
|
|
end
|
|
self.children = children
|
|
end
|
|
end
|
|
|
|
-- bad name... should be called something like postInit
|
|
-- normally used to determine sizes since the parent is
|
|
-- only known at this point
|
|
function UI.Window:setParent()
|
|
self.oh, self.ow = self.height, self.width
|
|
|
|
if self.rx then
|
|
if self.rx > 0 then
|
|
self.x = self.rx
|
|
else
|
|
self.x = self.parent.width + self.rx
|
|
end
|
|
end
|
|
if self.ry then
|
|
if self.ry > 0 then
|
|
self.y = self.ry
|
|
else
|
|
self.y = self.parent.height + self.ry
|
|
end
|
|
end
|
|
|
|
if self.rex then
|
|
self.width = self.parent.width - self.x + self.rex + 2
|
|
end
|
|
if self.rey then
|
|
self.height = self.parent.height - self.y + self.rey + 2
|
|
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:initChildren()
|
|
end
|
|
|
|
function UI.Window:add(children)
|
|
UI.setProperties(self, children)
|
|
self:initChildren()
|
|
end
|
|
|
|
function UI.Window:getCursorPos()
|
|
return self.cursorX, self.cursorY
|
|
end
|
|
|
|
function UI.Window:setCursorPos(x, y)
|
|
self.cursorX = x
|
|
self.cursorY = y
|
|
self.parent:setCursorPos(self.x + x - 1, self.y + y - 1)
|
|
end
|
|
|
|
function UI.Window:setCursorBlink(blink)
|
|
self.parent:setCursorBlink(blink)
|
|
end
|
|
|
|
function UI.Window:draw()
|
|
self:clear(self.backgroundColor)
|
|
if self.children then
|
|
for k,child in pairs(self.children) do
|
|
if child.enabled then
|
|
child:draw()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Window:sync()
|
|
if self.parent then
|
|
self.parent:sync()
|
|
end
|
|
end
|
|
|
|
function UI.Window:resize()
|
|
|
|
if self.rx then
|
|
if self.rx > 0 then
|
|
self.x = self.rx
|
|
else
|
|
self.x = self.parent.width + self.rx
|
|
end
|
|
end
|
|
if self.ry then
|
|
if self.ry > 0 then
|
|
self.y = self.ry
|
|
else
|
|
self.y = self.parent.height + self.ry
|
|
end
|
|
end
|
|
|
|
if self.rex then
|
|
self.width = self.parent.width - self.x + self.rex + 2
|
|
elseif not self.ow and self.parent then
|
|
self.width = self.parent.width - self.x + 1
|
|
end
|
|
|
|
if self.rey then
|
|
self.height = self.parent.height - self.y + self.rey + 2
|
|
elseif not self.oh and self.parent then
|
|
self.height = self.parent.height - self.y + 1
|
|
end
|
|
|
|
if self.children then
|
|
for _,child in ipairs(self.children) do
|
|
child:resize()
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Window:enable()
|
|
self.enabled = true
|
|
if self.children then
|
|
for k,child in pairs(self.children) do
|
|
child:enable()
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Window:disable()
|
|
self.enabled = false
|
|
if self.children then
|
|
for k,child in pairs(self.children) do
|
|
child:disable()
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Window:setTextScale(textScale)
|
|
self.textScale = textScale
|
|
self.parent:setTextScale(textScale)
|
|
end
|
|
|
|
function UI.Window:clear(bg)
|
|
self:clearArea(1 + self.offx, 1 + self.offy, self.width, self.height, bg)
|
|
end
|
|
|
|
function UI.Window:clearLine(y, bg)
|
|
local filler = string.rep(' ', self.width)
|
|
self:write(1, y, filler, bg)
|
|
end
|
|
|
|
function UI.Window:clearArea(x, y, width, height, bg)
|
|
if width > 0 then
|
|
local filler = string.rep(' ', width)
|
|
for i = 0, height-1 do
|
|
self:write(x, y+i, filler, bg)
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Window:write(x, y, text, bg, tc)
|
|
bg = bg or self.backgroundColor
|
|
tc = tc or self.textColor
|
|
x = x - self.offx
|
|
y = y - self.offy
|
|
if y <= self.height and y > 0 then
|
|
if self.canvas then
|
|
self.canvas:write(x, y, text, bg, tc)
|
|
else
|
|
self.parent:write(
|
|
self.x + x - 1, self.y + y - 1, tostring(text), bg, tc)
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Window:centeredWrite(y, text, bg, fg)
|
|
if #text >= self.width then
|
|
self:write(1, y, text, bg)
|
|
else
|
|
local space = math.floor((self.width-#text) / 2)
|
|
local filler = string.rep(' ', space + 1)
|
|
local str = filler:sub(1, space) .. text
|
|
str = str .. filler:sub(self.width - #str + 1)
|
|
self:write(1, y, str, bg, fg)
|
|
end
|
|
end
|
|
|
|
-- deprecated - use print instead
|
|
function UI.Window:wrappedWrite(x, y, text, len, bg, fg)
|
|
for _,v in pairs(Util.wordWrap(text, len)) do
|
|
self:write(x, y, v, bg, fg)
|
|
y = y + 1
|
|
end
|
|
return y
|
|
end
|
|
|
|
function UI.Window:print(text, bg, fg, indent)
|
|
indent = indent or 1
|
|
|
|
local function nextWord(line, cx)
|
|
local result = { line:find("(%w+)", cx) }
|
|
if #result > 1 and result[2] > cx then
|
|
return line:sub(cx, result[2] + 1)
|
|
elseif #result > 0 and result[1] == cx then
|
|
result = { line:find("(%w+)", result[2] + 1) }
|
|
if #result > 0 then
|
|
return line:sub(cx, result[1] + 1)
|
|
end
|
|
end
|
|
if cx <= #line then
|
|
return line:sub(cx, #line)
|
|
end
|
|
end
|
|
|
|
--[[
|
|
TODO
|
|
local test = "\027[0;1;33mYou tell foo, \"// Test string.\"\027[0;37mbar"
|
|
for sequence, text in string.gmatch (test, "\027%[([0-9;]+)m([^\027]+)") do
|
|
for ansi in string.gmatch (sequence, "%d+") do
|
|
print ("ANSI code: ", ansi)
|
|
end -- for
|
|
print ("Text: ", text)
|
|
end
|
|
--]]
|
|
|
|
local lines = Util.split(text)
|
|
for k,line in pairs(lines) do
|
|
local lx = 1
|
|
while true do
|
|
local word = nextWord(line, lx)
|
|
if not word then
|
|
if lines[k + 1] then
|
|
self.cursorX = indent
|
|
self.cursorY = self.cursorY + 1
|
|
end
|
|
break
|
|
end
|
|
local w = word
|
|
if self.cursorX + #word > self.width then
|
|
self.cursorX = indent
|
|
self.cursorY = self.cursorY + 1
|
|
w = word:gsub(' ', '')
|
|
end
|
|
self:write(self.cursorX, self.cursorY, w, bg, fg)
|
|
self.cursorX = self.cursorX + #word
|
|
lx = lx + #word
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Window:setFocus(focus)
|
|
assertElement(focus, 'UI.Window:setFocus: Invalid element passed')
|
|
if self.parent then
|
|
self.parent:setFocus(focus)
|
|
end
|
|
end
|
|
|
|
function UI.Window:getFocusables()
|
|
local focusable = { }
|
|
|
|
local function focusSort(a, b)
|
|
if a.y == b.y then
|
|
return a.x < b.x
|
|
end
|
|
return a.y < b.y
|
|
end
|
|
|
|
local function getFocusable(parent, x, y)
|
|
for _,child in Util.spairs(parent.children, focusSort) do
|
|
if child.enabled and child.focus then
|
|
table.insert(focusable, child)
|
|
end
|
|
if child.children then
|
|
getFocusable(child, child.x + x, child.y + y)
|
|
end
|
|
end
|
|
end
|
|
|
|
if self.children then
|
|
getFocusable(self, self.x, self.y)
|
|
end
|
|
|
|
return focusable
|
|
end
|
|
|
|
function UI.Window:focusFirst()
|
|
|
|
local focusables = self:getFocusables()
|
|
local focused = focusables[1]
|
|
if focused then
|
|
self:setFocus(focused)
|
|
end
|
|
end
|
|
|
|
function UI.Window:scrollIntoView()
|
|
local parent = self.parent
|
|
|
|
if self.x <= parent.offx then
|
|
parent.offx = math.max(0, self.x - 1)
|
|
parent:draw()
|
|
elseif self.x + self.width > parent.width + parent.offx then
|
|
parent.offx = self.x + self.width - parent.width - 1
|
|
parent:draw()
|
|
end
|
|
|
|
if self.y <= parent.offy then
|
|
parent.offy = math.max(0, self.y - 1)
|
|
parent:draw()
|
|
elseif self.y + self.height > parent.height + parent.offy then
|
|
parent.offy = self.y + self.height - parent.height - 1
|
|
parent:draw()
|
|
end
|
|
end
|
|
|
|
function UI.Window:addTransition(effect, x, y, width, height)
|
|
if self.parent then
|
|
x = x or 1
|
|
y = y or 1
|
|
width = width or self.width
|
|
height = height or self.height
|
|
self.parent:addTransition(effect, x + self.x - 1, y + self.y - 1, width, height)
|
|
end
|
|
end
|
|
|
|
function UI.Window:emit(event)
|
|
local parent = self
|
|
--debug(self.UIElement .. ' emitting ' .. event.type)
|
|
while parent do
|
|
if parent.eventHandler then
|
|
--debug('calling ' .. parent.UIElement)
|
|
if parent:eventHandler(event) then
|
|
return true
|
|
end
|
|
end
|
|
parent = parent.parent
|
|
end
|
|
end
|
|
|
|
function UI.Window:eventHandler(event)
|
|
return false
|
|
end
|
|
|
|
--[[-- Blit data manipulation --]]--
|
|
local Canvas = class()
|
|
function Canvas:init(args)
|
|
self.x = 1
|
|
self.y = 1
|
|
|
|
Util.merge(self, args)
|
|
|
|
self.height = self.ey - self.y + 1
|
|
self.width = self.ex - self.x + 1
|
|
|
|
self.lines = { }
|
|
for i = 1, self.height do
|
|
self.lines[i] = { }
|
|
end
|
|
end
|
|
|
|
function Canvas:colorToPaintColor(c)
|
|
if self.isColor then
|
|
return mapColorToPaint[c]
|
|
end
|
|
return mapGrayToPaint[c]
|
|
end
|
|
|
|
function Canvas:copy()
|
|
local b = Canvas({ x = self.x, y = self.y, ex = self.ex, ey = self.ey })
|
|
for i = 1, self.ey - self.y + 1 do
|
|
b.lines[i].text = self.lines[i].text
|
|
b.lines[i].fg = self.lines[i].fg
|
|
b.lines[i].bg = self.lines[i].bg
|
|
end
|
|
return b
|
|
end
|
|
|
|
function Canvas:addLayer(layer, bg, fg)
|
|
local canvas = Canvas({
|
|
x = layer.x,
|
|
y = layer.y,
|
|
ex = layer.x + layer.width - 1,
|
|
ey = layer.y + layer.height - 1,
|
|
isColor = self.isColor,
|
|
})
|
|
canvas:clear(bg, fg)
|
|
|
|
canvas.parent = self
|
|
if not self.layers then
|
|
self.layers = { }
|
|
end
|
|
table.insert(self.layers, canvas)
|
|
return canvas
|
|
end
|
|
|
|
function Canvas:removeLayer()
|
|
for k, layer in pairs(self.parent.layers) do
|
|
if layer == self then
|
|
self:setVisible(false)
|
|
table.remove(self.parent.layers, k)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
function Canvas:setVisible(visible)
|
|
self.visible = visible
|
|
if not visible then
|
|
self.parent:dirty()
|
|
-- set parent's lines to dirty for each line in self
|
|
end
|
|
end
|
|
|
|
function Canvas:write(x, y, text, bg, tc)
|
|
|
|
if y > 0 and y <= self.height and x <= self.width then
|
|
|
|
local width = #text
|
|
|
|
if x < 1 then
|
|
text = text:sub(2 - x)
|
|
width = width + x - 1
|
|
x = 1
|
|
end
|
|
|
|
if x + width - 1 > self.width then
|
|
text = text:sub(1, self.width - x + 1)
|
|
width = #text
|
|
end
|
|
|
|
if width > 0 then
|
|
|
|
local function replace(sstr, pos, rstr, width)
|
|
return sstr:sub(1, pos-1) .. rstr .. sstr:sub(pos+width)
|
|
end
|
|
|
|
local function fill(sstr, pos, rstr, width)
|
|
return sstr:sub(1, pos-1) .. string.rep(rstr, width) .. sstr:sub(pos+width)
|
|
end
|
|
|
|
local line = self.lines[y]
|
|
line.dirty = true
|
|
line.text = replace(line.text, x, text, width)
|
|
if bg then
|
|
line.bg = fill(line.bg, x, self:colorToPaintColor(bg), width)
|
|
end
|
|
if tc then
|
|
line.fg = fill(line.fg, x, self:colorToPaintColor(tc), width)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Canvas:writeLine(y, text, fg, bg)
|
|
self.lines[y].dirty = true
|
|
self.lines[y].text = text
|
|
self.lines[y].fg = fg
|
|
self.lines[y].bg = bg
|
|
end
|
|
|
|
function Canvas:reset()
|
|
self.region = nil
|
|
end
|
|
|
|
function Canvas:clear(bg, fg)
|
|
local width = self.ex - self.x + 1
|
|
local text = string.rep(' ', width)
|
|
fg = string.rep(self:colorToPaintColor(fg), width)
|
|
bg = string.rep(self:colorToPaintColor(bg), width)
|
|
for i = 1, self.ey - self.y + 1 do
|
|
self:writeLine(i, text, fg, bg)
|
|
end
|
|
end
|
|
|
|
function Canvas:punch(rect)
|
|
if not self.regions then
|
|
self.regions = Region.new(self.x, self.y, self.ex, self.ey)
|
|
end
|
|
self.regions:subRect(rect.x, rect.y, rect.ex, rect.ey)
|
|
end
|
|
|
|
function Canvas:blitClipped(device)
|
|
for _,region in ipairs(self.regions.region) do
|
|
self:blit(device,
|
|
{ x = region[1] - self.x + 1,
|
|
y = region[2] - self.y + 1,
|
|
ex = region[3]- self.x + 1,
|
|
ey = region[4] - self.y + 1 },
|
|
{ x = region[1], y = region[2] })
|
|
end
|
|
end
|
|
|
|
function Canvas:dirty()
|
|
for _, line in pairs(self.lines) do
|
|
line.dirty = true
|
|
end
|
|
end
|
|
|
|
function Canvas:clean()
|
|
for y, line in ipairs(self.lines) do
|
|
line.dirty = false
|
|
end
|
|
end
|
|
|
|
function Canvas:render(device, layers)
|
|
layers = layers or self.layers
|
|
if layers then
|
|
self.regions = Region.new(self.x, self.y, self.ex, self.ey)
|
|
local l = Util.shallowCopy(layers)
|
|
for _, canvas in ipairs(layers) do
|
|
table.remove(l, 1)
|
|
if canvas.visible then
|
|
self:punch(canvas)
|
|
canvas:render(device, l)
|
|
end
|
|
end
|
|
self:blitClipped(device)
|
|
self:reset()
|
|
else
|
|
self:blit(device)
|
|
end
|
|
self:clean()
|
|
end
|
|
|
|
function Canvas:blit(device, src, tgt)
|
|
src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 }
|
|
tgt = tgt or self
|
|
|
|
for i = 0, src.ey - src.y do
|
|
local line = self.lines[src.y + i]
|
|
if line.dirty then
|
|
local t, fg, bg = line.text, line.fg, line.bg
|
|
if src.x > 1 or src.ex < self.ex then
|
|
t = t:sub(src.x, src.ex)
|
|
fg = fg:sub(src.x, src.ex)
|
|
bg = bg:sub(src.x, src.ex)
|
|
end
|
|
device.setCursorPos(tgt.x, tgt.y + i)
|
|
device.blit(t, fg, bg)
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- TransitionSlideLeft --]]--
|
|
UI.TransitionSlideLeft = class()
|
|
UI.TransitionSlideLeft.defaults = {
|
|
UIElement = 'TransitionSlideLeft',
|
|
ticks = 4,
|
|
easing = 'outQuint',
|
|
}
|
|
function UI.TransitionSlideLeft:init(args)
|
|
local defaults = UI:getDefaults(UI.TransitionSlideLeft, args)
|
|
UI.setProperties(self, defaults)
|
|
|
|
self.pos = { x = self.ex }
|
|
self.tween = Tween.new(self.ticks, self.pos, { x = self.x }, self.easing)
|
|
self.lastx = 0
|
|
self.lastScreen = self.canvas:copy()
|
|
end
|
|
|
|
function UI.TransitionSlideLeft:update(device)
|
|
self.tween:update(1)
|
|
local x = math.floor(self.pos.x)
|
|
if x ~= self.lastx then
|
|
self.lastx = x
|
|
self.lastScreen:dirty()
|
|
self.lastScreen:blit(device, {
|
|
x = self.ex - x + self.x,
|
|
y = self.y,
|
|
ex = self.ex,
|
|
ey = self.ey },
|
|
{ x = self.x, y = self.y })
|
|
|
|
self.canvas:blit(device, {
|
|
x = self.x,
|
|
y = self.y,
|
|
ex = self.ex - x + self.x,
|
|
ey = self.ey },
|
|
{ x = x, y = self.y })
|
|
end
|
|
return self.pos.x ~= self.x
|
|
end
|
|
|
|
--[[-- TransitionSlideRight --]]--
|
|
UI.TransitionSlideRight = class()
|
|
UI.TransitionSlideRight.defaults = {
|
|
UIElement = 'TransitionSlideRight',
|
|
ticks = 4,
|
|
easing = 'outQuint',
|
|
}
|
|
function UI.TransitionSlideRight:init(args)
|
|
local defaults = UI:getDefaults(UI.TransitionSlideRight, args)
|
|
UI.setProperties(self, defaults)
|
|
|
|
self.pos = { x = self.x }
|
|
self.tween = Tween.new(self.ticks, self.pos, { x = self.ex }, self.easing)
|
|
self.lastx = 0
|
|
self.lastScreen = self.canvas:copy()
|
|
end
|
|
|
|
function UI.TransitionSlideRight:update(device)
|
|
self.tween:update(1)
|
|
local x = math.floor(self.pos.x)
|
|
if x ~= self.lastx then
|
|
self.lastx = x
|
|
self.lastScreen:dirty()
|
|
self.lastScreen:blit(device, {
|
|
x = self.x,
|
|
y = self.y,
|
|
ex = self.ex - x + self.x,
|
|
ey = self.ey },
|
|
{ x = x, y = self.y })
|
|
self.canvas:blit(device, {
|
|
x = self.ex - x + self.x,
|
|
y = self.y,
|
|
ex = self.ex,
|
|
ey = self.ey },
|
|
{ x = self.x, y = self.y })
|
|
end
|
|
return self.pos.x ~= self.ex
|
|
end
|
|
|
|
--[[-- TransitionExpandUp --]]--
|
|
UI.TransitionExpandUp = class()
|
|
UI.TransitionExpandUp.defaults = {
|
|
UIElement = 'TransitionExpandUp',
|
|
ticks = 3,
|
|
easing = 'linear',
|
|
}
|
|
function UI.TransitionExpandUp:init(args)
|
|
local defaults = UI:getDefaults(UI.TransitionExpandUp, args)
|
|
UI.setProperties(self, defaults)
|
|
self.pos = { y = self.ey + 1 }
|
|
self.tween = Tween.new(self.ticks, self.pos, { y = self.y }, self.easing)
|
|
end
|
|
|
|
function UI.TransitionExpandUp:update(device)
|
|
self.tween:update(1)
|
|
self.canvas:blit(device, nil, { x = self.x, y = math.floor(self.pos.y) })
|
|
return self.pos.y ~= self.y
|
|
end
|
|
|
|
--[[-- Terminal for computer / advanced computer / monitor --]]--
|
|
UI.Device = class(UI.Window)
|
|
UI.Device.defaults = {
|
|
UIElement = 'Device',
|
|
backgroundColor = colors.black,
|
|
textColor = colors.white,
|
|
textScale = 1,
|
|
effectsEnabled = true,
|
|
}
|
|
function UI.Device:init(args)
|
|
local defaults = UI:getDefaults(UI.Device)
|
|
defaults.device = term.current()
|
|
UI.setProperties(defaults, args)
|
|
|
|
if defaults.deviceType then
|
|
defaults.device = device[defaults.deviceType]
|
|
end
|
|
|
|
if not defaults.device.setTextScale then
|
|
defaults.device.setTextScale = function(...) end
|
|
end
|
|
|
|
defaults.device.setTextScale(defaults.textScale)
|
|
defaults.width, defaults.height = defaults.device.getSize()
|
|
|
|
UI.Window.init(self, defaults)
|
|
|
|
self.isColor = self.device.isColor()
|
|
|
|
self.canvas = Canvas({
|
|
x = 1, y = 1, ex = self.width, ey = self.height,
|
|
isColor = self.isColor,
|
|
})
|
|
self.canvas:clear(self.backgroundColor, self.textColor)
|
|
end
|
|
|
|
function UI.Device:resize()
|
|
self.width, self.height = self.device.getSize()
|
|
self.lines = { } -- TODO -- resize canvas
|
|
UI.Window.resize(self)
|
|
end
|
|
|
|
function UI.Device:setCursorPos(x, y)
|
|
self.cursorX = x
|
|
self.cursorY = y
|
|
end
|
|
|
|
function UI.Device:getCursorBlink()
|
|
return self.cursorBlink
|
|
end
|
|
|
|
function UI.Device:setCursorBlink(blink)
|
|
self.cursorBlink = blink
|
|
self.device.setCursorBlink(blink)
|
|
end
|
|
|
|
function UI.Device:setTextScale(textScale)
|
|
self.textScale = textScale
|
|
self.device.setTextScale(self.textScale)
|
|
end
|
|
|
|
function UI.Device:reset()
|
|
self.device.setBackgroundColor(colors.black)
|
|
self.device.setTextColor(colors.white)
|
|
self.device.clear()
|
|
self.device.setCursorPos(1, 1)
|
|
end
|
|
|
|
function UI.Device:addTransition(effect, x, y, width, height)
|
|
if not self.transitions then
|
|
self.transitions = { }
|
|
end
|
|
|
|
if type(effect) == 'string' then
|
|
local c
|
|
if effect == 'slideLeft' then
|
|
c = UI.TransitionSlideLeft
|
|
else
|
|
c = UI.TransitionSlideRight
|
|
end
|
|
effect = c {
|
|
x = x,
|
|
y = y,
|
|
ex = x + width - 1,
|
|
ey = y + height - 1,
|
|
canvas = self.canvas,
|
|
}
|
|
end
|
|
table.insert(self.transitions, effect)
|
|
end
|
|
|
|
function UI.Device:runTransitions(transitions, canvas)
|
|
|
|
for _,t in ipairs(transitions) do
|
|
canvas:punch(t) -- punch out the effect areas
|
|
end
|
|
canvas:blitClipped(self.device) -- and blit the remainder
|
|
canvas:reset()
|
|
|
|
local queue = { } -- don't miss events while transition is running
|
|
-- especially timers
|
|
while true do
|
|
for _,k in ipairs(Util.keys(transitions)) do
|
|
local transition = transitions[k]
|
|
if not transition:update(self.device) then
|
|
transitions[k] = nil
|
|
end
|
|
end
|
|
if Util.empty(transitions) then
|
|
break
|
|
end
|
|
os.sleep(0)
|
|
--[[
|
|
local timerId = os.startTimer(0)
|
|
while true do
|
|
local e = { os.pullEvent() }
|
|
if e[1] == 'timer' and e[2] == timerId then
|
|
break
|
|
end
|
|
table.insert(queue, e)
|
|
end
|
|
--]]
|
|
end
|
|
-- for _, e in ipairs(queue) do
|
|
-- Event.processEvent(e)
|
|
-- end
|
|
end
|
|
|
|
function UI.Device:sync()
|
|
|
|
local transitions
|
|
if self.transitions and self.effectsEnabled then
|
|
transitions = self.transitions
|
|
self.transitions = nil
|
|
end
|
|
|
|
if self:getCursorBlink() then
|
|
self.device.setCursorBlink(false)
|
|
end
|
|
|
|
if transitions then
|
|
self:runTransitions(transitions, self.canvas)
|
|
else
|
|
self.canvas:render(self.device)
|
|
end
|
|
|
|
if self:getCursorBlink() then
|
|
self.device.setCursorBlink(true)
|
|
self.device.setCursorPos(self.cursorX, self.cursorY)
|
|
end
|
|
end
|
|
|
|
--[[-- StringBuffer --]]--
|
|
UI.StringBuffer = class()
|
|
function UI.StringBuffer:init(bufSize)
|
|
self.bufSize = bufSize
|
|
self.buffer = {}
|
|
end
|
|
|
|
function UI.StringBuffer:insert(s, width)
|
|
local len = #tostring(s)
|
|
if len > width then
|
|
s = s:sub(1, width)
|
|
end
|
|
table.insert(self.buffer, s)
|
|
if len < width then
|
|
table.insert(self.buffer, string.rep(' ', width - len))
|
|
end
|
|
end
|
|
|
|
function UI.StringBuffer:get(sep)
|
|
return Util.widthify(table.concat(self.buffer, sep or ''), self.bufSize)
|
|
end
|
|
|
|
function UI.StringBuffer:clear()
|
|
self.buffer = { }
|
|
end
|
|
|
|
--[[-- Page (focus manager) --]]--
|
|
UI.Page = class(UI.Window)
|
|
UI.Page.defaults = {
|
|
UIElement = 'Page',
|
|
accelerators = {
|
|
down = 'focus_next',
|
|
enter = 'focus_next',
|
|
tab = 'focus_next',
|
|
shiftTab = 'focus_prev',
|
|
up = 'focus_prev',
|
|
},
|
|
backgroundColor = colors.black,
|
|
textColor = colors.white,
|
|
}
|
|
function UI.Page:init(args)
|
|
local defaults = UI:getDefaults(UI.Page)
|
|
defaults.parent = UI.defaultDevice
|
|
UI.setProperties(defaults, args)
|
|
UI.Window.init(self, defaults)
|
|
|
|
if self.z then
|
|
self.canvas = self.parent.canvas:addLayer(self, self.backgroundColor, self.textColor)
|
|
else
|
|
self.canvas = self.parent.canvas
|
|
end
|
|
end
|
|
|
|
function UI.Page:enable()
|
|
self.canvas.visible = true
|
|
UI.Window.enable(self)
|
|
|
|
if not self.focused or not self.focused.enabled then
|
|
self:focusFirst()
|
|
end
|
|
end
|
|
|
|
function UI.Page:disable()
|
|
if self.z then
|
|
self.canvas.visible = false
|
|
end
|
|
end
|
|
|
|
function UI.Page:getFocused()
|
|
return self.focused
|
|
end
|
|
|
|
function UI.Page:focusPrevious()
|
|
|
|
local function getPreviousFocus(focused)
|
|
local focusables = self:getFocusables()
|
|
for k, v in ipairs(focusables) do
|
|
if v == focused then
|
|
if k > 1 then
|
|
return focusables[k - 1]
|
|
end
|
|
return focusables[#focusables]
|
|
end
|
|
end
|
|
end
|
|
|
|
local focused = getPreviousFocus(self.focused)
|
|
|
|
if focused then
|
|
self:setFocus(focused)
|
|
end
|
|
end
|
|
|
|
function UI.Page:focusNext()
|
|
|
|
local function getNextFocus(focused)
|
|
local focusables = self:getFocusables()
|
|
for k, v in ipairs(focusables) do
|
|
if v == focused then
|
|
if k < #focusables then
|
|
return focusables[k + 1]
|
|
end
|
|
return focusables[1]
|
|
end
|
|
end
|
|
end
|
|
|
|
local focused = getNextFocus(self.focused)
|
|
if focused then
|
|
self:setFocus(focused)
|
|
end
|
|
end
|
|
|
|
function UI.Page:setFocus(child)
|
|
assertElement(child, 'UI.Page:setFocus: Invalid element passed')
|
|
|
|
if not child.focus then
|
|
return
|
|
end
|
|
|
|
if self.focused and self.focused ~= child then
|
|
self.focused.focused = false
|
|
self.focused:focus()
|
|
self.focused:emit({ type = 'focus_lost', focused = child })
|
|
end
|
|
|
|
self.focused = child
|
|
if not child.focused then
|
|
child.focused = true
|
|
self:emit({ type = 'focus_change', focused = child })
|
|
end
|
|
|
|
child:focus()
|
|
end
|
|
|
|
function UI.Page:eventHandler(event)
|
|
if self.focused then
|
|
if event.type == 'focus_next' then
|
|
self:focusNext()
|
|
return true
|
|
elseif event.type == 'focus_prev' then
|
|
self:focusPrevious()
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--[[-- Grid --]]--
|
|
UI.Grid = class(UI.Window)
|
|
UI.Grid.defaults = {
|
|
UIElement = 'Grid',
|
|
index = 1,
|
|
inverseSort = false,
|
|
disableHeader = false,
|
|
selectable = true,
|
|
textColor = colors.white,
|
|
textSelectedColor = colors.white,
|
|
backgroundColor = colors.black,
|
|
backgroundSelectedColor = colors.gray,
|
|
headerBackgroundColor = colors.cyan,
|
|
headerTextColor = colors.white,
|
|
unfocusedTextSelectedColor = colors.white,
|
|
unfocusedBackgroundSelectedColor = colors.gray,
|
|
focusIndicator = '>',
|
|
sortIndicator = ' ',
|
|
inverseSortIndicator = '^',
|
|
values = { },
|
|
columns = { },
|
|
accelerators = {
|
|
enter = 'key_enter',
|
|
[ 'control-c' ] = 'copy',
|
|
down = 'scroll_down',
|
|
up = 'scroll_up',
|
|
home = 'scroll_top',
|
|
[ 'end' ] = 'scroll_bottom',
|
|
pageUp = 'scroll_pageUp',
|
|
[ 'control-b' ] = 'scroll_pageUp',
|
|
pageDown = 'scroll_pageDown',
|
|
[ 'control-f' ] = 'scroll_pageDown',
|
|
},
|
|
}
|
|
function UI.Grid:init(args)
|
|
local defaults = UI:getDefaults(UI.Grid, args)
|
|
UI.Window.init(self, defaults)
|
|
|
|
for _,h in pairs(self.columns) do
|
|
if not h.heading then
|
|
h.heading = ''
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Grid:setParent()
|
|
UI.Window.setParent(self)
|
|
self:update()
|
|
|
|
if not self.pageSize then
|
|
if self.disableHeader then
|
|
self.pageSize = self.height
|
|
else
|
|
self.pageSize = self.height - 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Grid:resize()
|
|
UI.Window.resize(self)
|
|
|
|
if self.disableHeader then
|
|
self.pageSize = self.height
|
|
else
|
|
self.pageSize = self.height - 1
|
|
end
|
|
end
|
|
|
|
function UI.Grid:adjustWidth()
|
|
if self.autospace then
|
|
for _,col in pairs(self.columns) do
|
|
col.width = #col.heading
|
|
end
|
|
|
|
for _,col in pairs(self.columns) do
|
|
for key,row in pairs(self.values) do
|
|
row = self:getDisplayValues(row, key)
|
|
local value = row[col.key]
|
|
if value then
|
|
value = tostring(value)
|
|
if #value > col.width then
|
|
col.width = #value
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local colswidth = 1
|
|
for _,c in pairs(self.columns) do
|
|
colswidth = colswidth + c.width
|
|
end
|
|
|
|
local spacing = (self.width - 1 - colswidth)
|
|
if spacing > 0 then
|
|
local left = self.width - 1
|
|
for k,c in pairs(self.columns) do
|
|
local totalSpacing = left - colswidth
|
|
local space = totalSpacing / (#self.columns - k + 1)
|
|
colswidth = colswidth - c.width
|
|
c.width = c.width + math.floor(space)
|
|
left = left - c.width
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Grid:setPageSize(pageSize)
|
|
self.pageSize = pageSize
|
|
end
|
|
|
|
function UI.Grid:getValues()
|
|
return self.values
|
|
end
|
|
|
|
function UI.Grid:setValues(t)
|
|
self.values = t
|
|
self:update()
|
|
end
|
|
|
|
function UI.Grid:setInverseSort(inverseSort)
|
|
self.inverseSort = inverseSort
|
|
self:update()
|
|
self:setIndex(self.index)
|
|
end
|
|
|
|
function UI.Grid:setSortColumn(column)
|
|
self.sortColumn = column
|
|
end
|
|
|
|
function UI.Grid:getDisplayValues(row, key)
|
|
return row
|
|
end
|
|
|
|
function UI.Grid:getSelected()
|
|
if self.sorted then
|
|
return self.values[self.sorted[self.index]], self.sorted[self.index]
|
|
end
|
|
end
|
|
|
|
function UI.Grid:focus()
|
|
self:drawRows()
|
|
end
|
|
|
|
function UI.Grid:draw()
|
|
if not self.disableHeader then
|
|
self:drawHeadings()
|
|
end
|
|
|
|
if self.index <= 0 then
|
|
self:setIndex(1)
|
|
elseif self.index > #self.sorted then
|
|
self:setIndex(#self.sorted)
|
|
end
|
|
self:drawRows()
|
|
end
|
|
|
|
-- Something about the displayed table has changed
|
|
-- resort the table
|
|
function UI.Grid:update()
|
|
|
|
local function sort(a, b)
|
|
if not a[self.sortColumn] then
|
|
return false
|
|
elseif not b[self.sortColumn] then
|
|
return true
|
|
end
|
|
return self:sortCompare(a, b)
|
|
end
|
|
|
|
local function inverseSort(a, b)
|
|
return not sort(a, b)
|
|
end
|
|
|
|
local order
|
|
if self.sortColumn then
|
|
order = sort
|
|
if self.inverseSort then
|
|
order = inverseSort
|
|
end
|
|
end
|
|
|
|
self.sorted = Util.keys(self.values)
|
|
if order then
|
|
table.sort(self.sorted, function(a,b)
|
|
return order(self.values[a], self.values[b])
|
|
end)
|
|
end
|
|
|
|
self:adjustWidth()
|
|
end
|
|
|
|
function UI.Grid:drawHeadings()
|
|
local sb = UI.StringBuffer(self.width)
|
|
for _,col in ipairs(self.columns) do
|
|
local ind = ' '
|
|
if col.key == self.sortColumn then
|
|
if self.inverseSort then
|
|
ind = self.inverseSortIndicator
|
|
else
|
|
ind = self.sortIndicator
|
|
end
|
|
end
|
|
sb:insert(ind .. col.heading, col.width + 1)
|
|
end
|
|
self:write(1, 1, sb:get(), self.headerBackgroundColor, self.headerTextColor)
|
|
end
|
|
|
|
function UI.Grid:sortCompare(a, b)
|
|
local a = safeValue(a[self.sortColumn])
|
|
local b = safeValue(b[self.sortColumn])
|
|
if type(a) == type(b) then
|
|
return a < b
|
|
end
|
|
return tostring(a) < tostring(b)
|
|
end
|
|
|
|
function UI.Grid:drawRows()
|
|
local y = 1
|
|
local startRow = math.max(1, self:getStartRow())
|
|
local sb = UI.StringBuffer(self.width)
|
|
|
|
if not self.disableHeader then
|
|
y = y + 1
|
|
end
|
|
|
|
local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted)
|
|
for index = startRow, lastRow do
|
|
|
|
local sindex = self.sorted[index]
|
|
local row = self.values[sindex]
|
|
local key = sindex
|
|
row = self:getDisplayValues(row, key)
|
|
|
|
sb:clear()
|
|
|
|
local ind = ' '
|
|
if self.focused and index == self.index and self.selectable then
|
|
ind = self.focusIndicator
|
|
end
|
|
|
|
for _,col in pairs(self.columns) do
|
|
sb:insert(ind .. safeValue(row[col.key] or ''), col.width + 1)
|
|
ind = ' '
|
|
end
|
|
|
|
local selected = index == self.index and self.selectable
|
|
|
|
self:write(1, y, sb:get(),
|
|
self:getRowBackgroundColor(row, selected),
|
|
self:getRowTextColor(row, selected))
|
|
|
|
y = y + 1
|
|
end
|
|
|
|
if y <= self.height then
|
|
self:clearArea(1, y, self.width, self.height - y + 1)
|
|
end
|
|
end
|
|
|
|
function UI.Grid:getRowTextColor(row, selected)
|
|
if selected then
|
|
if self.focused then
|
|
return self.textSelectedColor
|
|
end
|
|
return self.unfocusedTextSelectedColor
|
|
end
|
|
return self.textColor
|
|
end
|
|
|
|
function UI.Grid:getRowBackgroundColor(row, selected)
|
|
if selected then
|
|
if self.focused then
|
|
return self.backgroundSelectedColor
|
|
end
|
|
return self.unfocusedBackgroundSelectedColor
|
|
end
|
|
return self.backgroundColor
|
|
end
|
|
|
|
function UI.Grid:getIndex(index)
|
|
return self.index
|
|
end
|
|
|
|
function UI.Grid:setIndex(index)
|
|
index = math.max(1, index)
|
|
self.index = math.min(index, #self.sorted)
|
|
|
|
local selected = self:getSelected()
|
|
if selected ~= self.selected then
|
|
self:drawRows()
|
|
self.selected = selected
|
|
if selected then
|
|
self:emit({ type = 'grid_focus_row', selected = selected })
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Grid:getStartRow()
|
|
return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1
|
|
end
|
|
|
|
function UI.Grid:getPage()
|
|
return math.floor(self.index / self.pageSize) + 1
|
|
end
|
|
|
|
function UI.Grid:getPageCount()
|
|
local tableSize = Util.size(self.values)
|
|
local pc = math.floor(tableSize / self.pageSize)
|
|
if tableSize % self.pageSize > 0 then
|
|
pc = pc + 1
|
|
end
|
|
return pc
|
|
end
|
|
|
|
function UI.Grid:nextPage()
|
|
self:setPage(self:getPage() + 1)
|
|
end
|
|
|
|
function UI.Grid:previousPage()
|
|
self:setPage(self:getPage() - 1)
|
|
end
|
|
|
|
function UI.Grid:setPage(pageNo)
|
|
-- 1 based paging
|
|
self:setIndex((pageNo-1) * self.pageSize + 1)
|
|
end
|
|
|
|
function UI.Grid:eventHandler(event)
|
|
|
|
if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
|
|
if not self.disableHeader then
|
|
if event.y == 1 then
|
|
local col = 2
|
|
for _,c in ipairs(self.columns) do
|
|
if event.x < col + c.width then
|
|
if self.sortColumn == c.key then
|
|
self:setInverseSort(not self.inverseSort)
|
|
else
|
|
self.sortColumn = c.key
|
|
self:setInverseSort(false)
|
|
end
|
|
self:draw()
|
|
break
|
|
end
|
|
col = col + c.width + 1
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
local row = self:getStartRow() + event.y - 1
|
|
if not self.disableHeader then
|
|
row = row - 1
|
|
end
|
|
if row > 0 and row <= Util.size(self.values) then
|
|
self:setIndex(row)
|
|
if event.type == 'mouse_doubleclick' then
|
|
self:emit({ type = 'key_enter' })
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
|
|
elseif event.type == 'scroll_down' then
|
|
self:setIndex(self.index + 1)
|
|
elseif event.type == 'scroll_up' then
|
|
self:setIndex(self.index - 1)
|
|
elseif event.type == 'scroll_top' then
|
|
self:setIndex(1)
|
|
elseif event.type == 'scroll_bottom' then
|
|
self:setIndex(Util.size(self.values))
|
|
elseif event.type == 'scroll_pageUp' then
|
|
self:setIndex(self.index - self.pageSize)
|
|
elseif event.type == 'scroll_pageDown' then
|
|
self:setIndex(self.index + self.pageSize)
|
|
elseif event.type == 'key_enter' then
|
|
if self.selected then
|
|
self:emit({ type = 'grid_select', selected = self.selected })
|
|
end
|
|
elseif event.type == 'copy' then
|
|
if self.selected then
|
|
clipboard.setData(Util.tostring(self.selected))
|
|
clipboard.useInternal(true)
|
|
end
|
|
else
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
--[[-- ScrollingGrid --]]--
|
|
UI.ScrollingGrid = class(UI.Grid)
|
|
UI.ScrollingGrid.defaults = {
|
|
UIElement = 'ScrollingGrid',
|
|
scrollOffset = 1,
|
|
lineChar = '|',
|
|
sliderChar = '#',
|
|
upArrowChar = '^',
|
|
downArrowChar = 'v',
|
|
}
|
|
function UI.ScrollingGrid:init(args)
|
|
local defaults = UI:getDefaults(UI.ScrollingGrid, args)
|
|
UI.Grid.init(self, defaults)
|
|
end
|
|
|
|
function UI.ScrollingGrid:drawRows()
|
|
UI.Grid.drawRows(self)
|
|
self:drawScrollbar()
|
|
end
|
|
|
|
function UI.ScrollingGrid:drawScrollbar()
|
|
local ts = Util.size(self.values)
|
|
if ts > self.pageSize then
|
|
local sbSize = self.pageSize - 2
|
|
local sa = ts
|
|
sa = self.pageSize / sa
|
|
sa = math.floor(sbSize * sa)
|
|
if sa < 1 then
|
|
sa = 1
|
|
end
|
|
if sa > sbSize then
|
|
sa = sbSize
|
|
end
|
|
local sp = ts-self.pageSize
|
|
sp = self.scrollOffset / sp
|
|
sp = math.floor(sp * (sbSize-sa + 0.5))
|
|
|
|
local x = self.width
|
|
if self.scrollOffset > 1 then
|
|
self:write(x, 2, self.upArrowChar)
|
|
else
|
|
self:write(x, 2, ' ')
|
|
end
|
|
local row = 0
|
|
for i = 1, sp - 1 do
|
|
self:write(x, row+3, self.lineChar)
|
|
row = row + 1
|
|
end
|
|
for i = 1, sa do
|
|
self:write(x, row+3, self.sliderChar)
|
|
row = row + 1
|
|
end
|
|
for i = row, sbSize do
|
|
self:write(x, row+3, self.lineChar)
|
|
row = row + 1
|
|
end
|
|
if self.scrollOffset + self.pageSize - 1 < Util.size(self.values) then
|
|
self:write(x, self.pageSize + 1, self.downArrowChar)
|
|
else
|
|
self:write(x, self.pageSize + 1, ' ')
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.ScrollingGrid:getStartRow()
|
|
local ts = Util.size(self.values)
|
|
if ts < self.pageSize then
|
|
self.scrollOffset = 1
|
|
end
|
|
return self.scrollOffset
|
|
end
|
|
|
|
function UI.ScrollingGrid:setIndex(index)
|
|
if index < self.scrollOffset then
|
|
self.scrollOffset = index
|
|
elseif index - (self.scrollOffset - 1) > self.pageSize then
|
|
self.scrollOffset = index - self.pageSize + 1
|
|
end
|
|
|
|
if self.scrollOffset < 1 then
|
|
self.scrollOffset = 1
|
|
else
|
|
local ts = Util.size(self.values)
|
|
if self.pageSize + self.scrollOffset > ts then
|
|
self.scrollOffset = math.max(1, ts - self.pageSize + 1)
|
|
end
|
|
end
|
|
UI.Grid.setIndex(self, index)
|
|
end
|
|
|
|
--[[-- Menu --]]--
|
|
UI.Menu = class(UI.Grid)
|
|
UI.Menu.defaults = {
|
|
UIElement = 'Menu',
|
|
disableHeader = true,
|
|
columns = { { heading = 'Prompt', key = 'prompt', width = 20 } },
|
|
}
|
|
function UI.Menu:init(args)
|
|
local defaults = UI:getDefaults(UI.Menu)
|
|
defaults.values = args['menuItems']
|
|
UI.setProperties(defaults, args)
|
|
UI.Grid.init(self, defaults)
|
|
self.pageSize = #args.menuItems
|
|
end
|
|
|
|
function UI.Menu:setParent()
|
|
UI.Grid.setParent(self)
|
|
self.itemWidth = 1
|
|
for _,v in pairs(self.values) do
|
|
if #v.prompt > self.itemWidth then
|
|
self.itemWidth = #v.prompt
|
|
end
|
|
end
|
|
self.columns[1].width = self.itemWidth
|
|
|
|
if self.centered then
|
|
self:center()
|
|
else
|
|
self.width = self.itemWidth + 2
|
|
end
|
|
end
|
|
|
|
function UI.Menu:center()
|
|
self.x = (self.width - self.itemWidth + 2) / 2
|
|
self.width = self.itemWidth + 2
|
|
end
|
|
|
|
function UI.Menu:eventHandler(event)
|
|
if event.type == 'key' then
|
|
if event.key == 'enter' then
|
|
local selected = self.menuItems[self.index]
|
|
self:emit({
|
|
type = selected.event or 'menu_select',
|
|
selected = selected
|
|
})
|
|
return true
|
|
end
|
|
elseif event.type == 'mouse_click' then
|
|
if event.y <= #self.menuItems then
|
|
UI.Grid.setIndex(self, event.y)
|
|
local selected = self.menuItems[self.index]
|
|
self:emit({
|
|
type = selected.event or 'menu_select',
|
|
selected = selected
|
|
})
|
|
return true
|
|
end
|
|
end
|
|
return UI.Grid.eventHandler(self, event)
|
|
end
|
|
|
|
--[[-- ViewportWindow --]]--
|
|
UI.ViewportWindow = class(UI.Window)
|
|
UI.ViewportWindow.defaults = {
|
|
UIElement = 'ViewportWindow',
|
|
backgroundColor = colors.cyan,
|
|
accelerators = {
|
|
down = 'scroll_down',
|
|
up = 'scroll_up',
|
|
home = 'scroll_top',
|
|
[ 'end' ] = 'scroll_bottom',
|
|
pageUp = 'scroll_pageUp',
|
|
[ 'control-b' ] = 'scroll_pageUp',
|
|
pageDown = 'scroll_pageDown',
|
|
[ 'control-f' ] = 'scroll_pageDown',
|
|
},
|
|
}
|
|
function UI.ViewportWindow:init(args)
|
|
local defaults = UI:getDefaults(UI.ViewportWindow, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.ViewportWindow:setScrollPosition(offset)
|
|
local oldOffset = self.offy
|
|
self.offy = math.max(offset, 0)
|
|
local max = self.ymax or self.height
|
|
if self.children then
|
|
for _, child in ipairs(self.children) do
|
|
max = math.max(child.y + child.height - 1, max)
|
|
end
|
|
end
|
|
self.offy = math.min(self.offy, math.max(max, self.height) - self.height)
|
|
if self.offy ~= oldOffset then
|
|
self:draw()
|
|
end
|
|
end
|
|
|
|
function UI.ViewportWindow:reset()
|
|
self.offy = 0
|
|
end
|
|
|
|
function UI.ViewportWindow:eventHandler(event)
|
|
|
|
if event.type == 'scroll_down' then
|
|
self:setScrollPosition(self.offy + 1)
|
|
elseif event.type == 'scroll_up' then
|
|
self:setScrollPosition(self.offy - 1)
|
|
elseif event.type == 'scroll_top' then
|
|
self:setScrollPosition(0)
|
|
elseif event.type == 'scroll_bottom' then
|
|
self:setScrollPosition(10000000)
|
|
elseif event.type == 'scroll_pageUp' then
|
|
self:setScrollPosition(self.offy - self.height)
|
|
elseif event.type == 'scroll_pageDown' then
|
|
self:setScrollPosition(self.offy + self.height)
|
|
else
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
--[[-- ScrollingText --]]--
|
|
UI.ScrollingText = class(UI.Window)
|
|
UI.ScrollingText.defaults = {
|
|
UIElement = 'ScrollingText',
|
|
backgroundColor = colors.black,
|
|
buffer = { },
|
|
}
|
|
function UI.ScrollingText:init(args)
|
|
local defaults = UI:getDefaults(UI.ScrollingText, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.ScrollingText:appendLine(text)
|
|
if #self.buffer+1 >= self.height then
|
|
table.remove(self.buffer, 1)
|
|
end
|
|
table.insert(self.buffer, text)
|
|
end
|
|
|
|
function UI.ScrollingText:clear()
|
|
self.buffer = { }
|
|
UI.Window.clear(self)
|
|
end
|
|
|
|
function UI.ScrollingText:draw()
|
|
for k,text in ipairs(self.buffer) do
|
|
self:write(1, k, Util.widthify(text, self.width), self.backgroundColor)
|
|
end
|
|
end
|
|
|
|
--[[-- TitleBar --]]--
|
|
UI.TitleBar = class(UI.Window)
|
|
UI.TitleBar.defaults = {
|
|
UIElement = 'TitleBar',
|
|
height = 1,
|
|
backgroundColor = colors.brown,
|
|
title = ''
|
|
}
|
|
function UI.TitleBar:init(args)
|
|
local defaults = UI:getDefaults(UI.TitleBar, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.TitleBar:draw()
|
|
self:clear()
|
|
local centered = math.ceil((self.width - #self.title) / 2)
|
|
self:write(1 + centered, 1, self.title, self.backgroundColor)
|
|
if self.previousPage or self.event then
|
|
self:write(self.width, 1, '*', self.backgroundColor, colors.black)
|
|
end
|
|
--self:write(self.width-1, 1, '?', self.backgroundColor)
|
|
end
|
|
|
|
function UI.TitleBar:eventHandler(event)
|
|
if event.type == 'mouse_click' then
|
|
if (self.previousPage or self.event) and event.x == self.width then
|
|
if self.event then
|
|
self:emit({ type = self.event, element = self })
|
|
elseif type(self.previousPage) == 'string' or
|
|
type(self.previousPage) == 'table' then
|
|
UI:setPage(self.previousPage)
|
|
else
|
|
UI:setPreviousPage()
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- MenuBar --]]--
|
|
UI.MenuBar = class(UI.Window)
|
|
UI.MenuBar.defaults = {
|
|
UIElement = 'MenuBar',
|
|
buttons = { },
|
|
height = 1,
|
|
backgroundColor = colors.lightGray,
|
|
textColor = colors.black,
|
|
spacing = 2,
|
|
showBackButton = false,
|
|
}
|
|
|
|
function UI.MenuBar:init(args)
|
|
local defaults = UI:getDefaults(UI.MenuBar, args)
|
|
UI.setProperties(self, defaults)
|
|
|
|
if not self.children then
|
|
self.children = { }
|
|
end
|
|
|
|
local x = 1
|
|
for k,button in pairs(self.buttons) do
|
|
if button.UIElement then
|
|
table.insert(self.children, button)
|
|
else
|
|
local buttonProperties = {
|
|
x = x,
|
|
width = #button.text + self.spacing,
|
|
backgroundColor = self.backgroundColor,
|
|
textColor = self.textColor,
|
|
centered = false,
|
|
}
|
|
x = x + buttonProperties.width
|
|
UI.setProperties(buttonProperties, button)
|
|
if button.name then
|
|
self[button.name] = UI.Button(buttonProperties)
|
|
else
|
|
table.insert(self.children, UI.Button(buttonProperties))
|
|
end
|
|
end
|
|
end
|
|
if self.showBackButton then
|
|
table.insert(self.children, UI.Button({
|
|
x = UI.term.width - 2,
|
|
width = 3,
|
|
backgroundColor = self.backgroundColor,
|
|
textColor = self.textColor,
|
|
text = '^-',
|
|
event = 'back',
|
|
}))
|
|
end
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.MenuBar:eventHandler(event)
|
|
if event.type == 'dropdown' then
|
|
-- better, but still a bad implementation
|
|
-- this at least will allow overrides
|
|
-- on the button and menubar
|
|
if event.button and event.button.dropdown then
|
|
local dropdown = self.parent[event.button.dropdown]
|
|
if dropdown then
|
|
if dropdown.enabled then
|
|
dropdown:hide(event.button)
|
|
else
|
|
dropdown:show(event.button)
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- DropMenu --]]--
|
|
UI.DropMenu = class(UI.MenuBar)
|
|
UI.DropMenu.defaults = {
|
|
UIElement = 'DropMenu',
|
|
backgroundColor = colors.white,
|
|
}
|
|
function UI.DropMenu:init(args)
|
|
local defaults = UI:getDefaults(UI.DropMenu, args)
|
|
UI.MenuBar.init(self, defaults)
|
|
end
|
|
|
|
function UI.DropMenu:setParent()
|
|
|
|
UI.MenuBar.setParent(self)
|
|
|
|
local maxWidth = 1
|
|
for y,child in ipairs(self.children) do
|
|
child.x = 1
|
|
child.y = y
|
|
if #(child.text or '') > maxWidth then
|
|
maxWidth = #child.text
|
|
end
|
|
end
|
|
for _,child in ipairs(self.children) do
|
|
child.width = maxWidth + 2
|
|
end
|
|
|
|
self.height = #self.children
|
|
self.width = maxWidth + 2
|
|
self.ow = self.width
|
|
end
|
|
|
|
function UI.DropMenu:enable()
|
|
self.enabled = false
|
|
end
|
|
|
|
function UI.DropMenu:show(button) -- the x, y should be passed instead of button
|
|
self.button = button
|
|
self.x, self.y = getPosition(button)
|
|
self.y = self.y + 1
|
|
if self.x + self.width > self.parent.width then
|
|
self.x = self.parent.width - self.width + 1
|
|
end
|
|
self.enabled = true
|
|
for _,child in ipairs(self.children) do
|
|
child:enable()
|
|
end
|
|
self:setFocus(self.children[1])
|
|
self:draw()
|
|
UI:capture(self)
|
|
end
|
|
|
|
function UI.DropMenu:hide()
|
|
self:disable()
|
|
self.parent:draw()
|
|
UI:release(self)
|
|
end
|
|
|
|
function UI.DropMenu:eventHandler(event)
|
|
if event.type == 'focus_lost' then
|
|
for _,child in ipairs(self.children) do
|
|
if child == event.focused then
|
|
return
|
|
end
|
|
end
|
|
self:hide()
|
|
elseif event.type == 'mouse_out' then
|
|
self:hide()
|
|
if self.button then
|
|
self:setFocus(self.button)
|
|
end
|
|
else
|
|
return UI.MenuBar.eventHandler(self, event)
|
|
end
|
|
return true
|
|
end
|
|
|
|
--[[-- TabBar --]]--
|
|
UI.TabBar = class(UI.MenuBar)
|
|
UI.TabBar.defaults = {
|
|
UIElement = 'TabBar',
|
|
selectedBackgroundColor = colors.cyan,
|
|
focusBackgroundColor = colors.green,
|
|
}
|
|
function UI.TabBar:init(args)
|
|
local defaults = UI:getDefaults(UI.TabBar, args)
|
|
UI.MenuBar.init(self, defaults)
|
|
end
|
|
|
|
function UI.TabBar:selectTab(text)
|
|
local selected, lastSelected
|
|
for k,child in pairs(self.children) do
|
|
if child.selected then
|
|
lastSelected = k
|
|
end
|
|
child.selected = child.text == text
|
|
if child.selected then
|
|
selected = k
|
|
child.backgroundColor = self.selectedBackgroundColor
|
|
child.backgroundFocusColor = self.selectedBackgroundColor
|
|
else
|
|
child.backgroundColor = self.backgroundColor
|
|
child.backgroundFocusColor = self.backgroundColor
|
|
end
|
|
end
|
|
if selected and lastSelected and selected ~= lastSelected then
|
|
self:emit({ type = 'tab_change', current = selected, last = lastSelected })
|
|
end
|
|
UI.MenuBar.draw(self)
|
|
end
|
|
|
|
--[[-- Tabs --]]--
|
|
UI.Tabs = class(UI.Window)
|
|
UI.Tabs.defaults = {
|
|
UIElement = 'Tabs',
|
|
}
|
|
function UI.Tabs:init(args)
|
|
local defaults = UI:getDefaults(UI.Tabs, args)
|
|
|
|
local buttons = { }
|
|
for k,child in pairs(defaults) do
|
|
if type(child) == 'table' and child.UIElement then
|
|
table.insert(buttons, {
|
|
text = child.tabTitle or '', event = 'tab_select',
|
|
})
|
|
end
|
|
end
|
|
|
|
self.tabBar = UI.TabBar({
|
|
buttons = buttons,
|
|
})
|
|
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.Tabs:setParent()
|
|
UI.Window.setParent(self)
|
|
|
|
for _,child in pairs(self.children) do
|
|
if child ~= self.tabBar then
|
|
child.y = 2
|
|
child.height = self.height - 1
|
|
child:resize()
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Tabs:enable()
|
|
self.enabled = true
|
|
for _,child in ipairs(self.children) do
|
|
if child.tabTitle == self.tabBar.buttons[1].text then
|
|
self:activateTab(child)
|
|
break
|
|
end
|
|
end
|
|
self.tabBar:enable()
|
|
end
|
|
|
|
function UI.Tabs:activateTab(tab)
|
|
for _,child in ipairs(self.children) do
|
|
if child ~= self.tabBar then
|
|
child:disable()
|
|
end
|
|
end
|
|
self.tabBar:selectTab(tab.tabTitle)
|
|
tab:enable()
|
|
tab:draw()
|
|
self:emit({ type = 'tab_activate', activated = tab, element = self })
|
|
end
|
|
|
|
function UI.Tabs:eventHandler(event)
|
|
if event.type == 'tab_select' then
|
|
for _,child in ipairs(self.children) do
|
|
if child.tabTitle == event.button.text then
|
|
self:activateTab(child)
|
|
break
|
|
end
|
|
end
|
|
elseif event.type == 'tab_change' then
|
|
for _,tab in ipairs(self.children) do
|
|
if tab ~= self.tabBar then
|
|
if event.current > event.last then
|
|
tab:addTransition('slideLeft')
|
|
else
|
|
tab:addTransition('slideRight')
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- WindowScroller --]]--
|
|
UI.WindowScroller = class(UI.Window)
|
|
UI.WindowScroller.defaults = {
|
|
UIElement = 'WindowScroller',
|
|
children = { },
|
|
}
|
|
function UI.WindowScroller:init(args)
|
|
local defaults = UI:getDefaults(UI.WindowScroller, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.WindowScroller:enable()
|
|
self.enabled = true
|
|
if #self.children > 0 then
|
|
self.children[1]:enable()
|
|
end
|
|
end
|
|
|
|
function UI.WindowScroller:nextChild()
|
|
for i = 1, #self.children do
|
|
if self.children[i].enabled then
|
|
if i < #self.children then
|
|
self:addTransition('slideLeft')
|
|
self.children[i]:disable()
|
|
self.children[i + 1]:enable()
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.WindowScroller:prevChild()
|
|
for i = 1, #self.children do
|
|
if self.children[i].enabled then
|
|
if i - 1 > 0 then
|
|
self:addTransition('slideRight')
|
|
self.children[i]:disable()
|
|
self.children[i - 1]:enable()
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- Notification --]]--
|
|
UI.Notification = class(UI.Window)
|
|
UI.Notification.defaults = {
|
|
UIElement = 'Notification',
|
|
backgroundColor = colors.gray,
|
|
height = 3,
|
|
}
|
|
function UI.Notification:init(args)
|
|
local defaults = UI:getDefaults(UI.Notification, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.Notification:draw()
|
|
end
|
|
|
|
function UI.Notification:enable()
|
|
self.enabled = false
|
|
end
|
|
|
|
function UI.Notification:error(value, timeout)
|
|
self.backgroundColor = colors.red
|
|
self:display(value, timeout)
|
|
end
|
|
|
|
function UI.Notification:info(value, timeout)
|
|
self.backgroundColor = colors.gray
|
|
self:display(value, timeout)
|
|
end
|
|
|
|
function UI.Notification:success(value, timeout)
|
|
self.backgroundColor = colors.green
|
|
self:display(value, timeout)
|
|
end
|
|
|
|
function UI.Notification:cancel()
|
|
if self.canvas then
|
|
Event.cancelNamedTimer('notificationTimer')
|
|
self.enabled = false
|
|
self.canvas:removeLayer()
|
|
self.canvas = nil
|
|
end
|
|
end
|
|
|
|
function UI.Notification:display(value, timeout)
|
|
self.enabled = true
|
|
local lines = Util.wordWrap(value, self.width - 2)
|
|
self.height = #lines + 1
|
|
self.y = self.parent.height - self.height + 1
|
|
if self.canvas then
|
|
self.canvas:removeLayer()
|
|
end
|
|
|
|
-- need to get the current canvas - not ui.term.canvas
|
|
self.canvas = UI.term.canvas:addLayer(self, self.backgroundColor, self.textColor or colors.white)
|
|
self:addTransition(UI.TransitionExpandUp {
|
|
x = self.x,
|
|
y = self.y,
|
|
ex = self.x + self.width - 1,
|
|
ey = self.y + self.height - 1,
|
|
canvas = self.canvas,
|
|
ticks = self.height,
|
|
})
|
|
self.canvas:setVisible(true)
|
|
self:clear()
|
|
for k,v in pairs(lines) do
|
|
self:write(2, k, v)
|
|
end
|
|
|
|
Event.addNamedTimer('notificationTimer', timeout or 3, false, function()
|
|
self:cancel()
|
|
self:sync()
|
|
end)
|
|
end
|
|
|
|
--[[-- Throttle --]]--
|
|
UI.Throttle = class(UI.Window)
|
|
UI.Throttle.defaults = {
|
|
UIElement = 'Throttle',
|
|
backgroundColor = colors.gray,
|
|
height = 6,
|
|
width = 10,
|
|
timeout = .095,
|
|
ctr = 0,
|
|
image = {
|
|
' //) (O )~@ &~&-( ?Q ',
|
|
' //) (O )- @ \-( ?) && ',
|
|
' //) (O ), @ \-(?) && ',
|
|
' //) (O ). @ \-d ) (@ '
|
|
}
|
|
}
|
|
|
|
function UI.Throttle:init(args)
|
|
local defaults = UI:getDefaults(UI.Throttle, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.Throttle:setParent()
|
|
self.x = math.ceil((self.parent.width - self.width) / 2)
|
|
self.y = math.ceil((self.parent.height - self.height) / 2)
|
|
UI.Window.setParent(self)
|
|
end
|
|
|
|
function UI.Throttle:enable()
|
|
self.enabled = false
|
|
end
|
|
|
|
function UI.Throttle:disable()
|
|
if self.canvas then
|
|
self.enabled = false
|
|
self.canvas:removeLayer()
|
|
self.canvas = nil
|
|
self.c = nil
|
|
end
|
|
end
|
|
|
|
function UI.Throttle:update()
|
|
local cc = os.clock()
|
|
if not self.c then
|
|
self.c = cc
|
|
elseif cc > self.c + self.timeout then
|
|
os.sleep(0)
|
|
self.c = os.clock()
|
|
self.enabled = true
|
|
if not self.canvas then
|
|
self.canvas = UI.term.canvas:addLayer(self, self.backgroundColor, colors.cyan)
|
|
self.canvas:setVisible(true)
|
|
self:clear(colors.cyan)
|
|
end
|
|
local image = self.image[self.ctr + 1]
|
|
local width = self.width - 2
|
|
for i = 0, #self.image do
|
|
self:write(2, i + 2, image:sub(width * i + 1, width * i + width), colors.black, colors.white)
|
|
end
|
|
|
|
self.ctr = (self.ctr + 1) % #self.image
|
|
|
|
self:sync()
|
|
end
|
|
end
|
|
|
|
--[[-- GridLayout --]]--
|
|
UI.GridLayout = class(UI.Window)
|
|
UI.GridLayout.defaults = {
|
|
UIElement = 'GridLayout',
|
|
x = 1,
|
|
y = 1,
|
|
textColor = colors.white,
|
|
backgroundColor = colors.black,
|
|
values = { },
|
|
columns = { },
|
|
}
|
|
function UI.GridLayout:init(args)
|
|
local defaults = UI:getDefaults(UI.GridLayout, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.GridLayout:setParent()
|
|
UI.Window.setParent(self)
|
|
self:adjustWidth()
|
|
end
|
|
|
|
function UI.GridLayout:adjustWidth()
|
|
if not self.width then
|
|
self.width = self:calculateWidth()
|
|
end
|
|
if self.autospace then
|
|
local width
|
|
for _,col in pairs(self.columns) do
|
|
width = 1
|
|
for _,row in pairs(self.values) do
|
|
local value = row[col[2]]
|
|
if value then
|
|
value = tostring(value)
|
|
if #value > width then
|
|
width = #value
|
|
end
|
|
end
|
|
end
|
|
col[3] = width
|
|
end
|
|
|
|
local colswidth = 0
|
|
for _,c in pairs(self.columns) do
|
|
colswidth = colswidth + c[3] + 1
|
|
end
|
|
|
|
local spacing = (self.width - colswidth - 1)
|
|
if spacing > 0 then
|
|
spacing = math.floor(spacing / (#self.columns - 1) )
|
|
for _,c in pairs(self.columns) do
|
|
c[3] = c[3] + spacing
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.GridLayout:calculateWidth()
|
|
-- gutters on each side
|
|
local width = 2
|
|
for _,col in pairs(self.columns) do
|
|
width = width + col[3] + 1
|
|
end
|
|
return width - 1
|
|
end
|
|
|
|
function UI.GridLayout:drawRow(row, y)
|
|
local sb = UI.StringBuffer(self.width)
|
|
for _,col in pairs(self.columns) do
|
|
local value = row[col[2]]
|
|
sb:insert(' ' .. (value or ''), col[3] + 1)
|
|
end
|
|
|
|
local selected = index == self.index and self.selectable
|
|
if selected then
|
|
self:setSelected(row)
|
|
end
|
|
|
|
self:write(1, y, sb:get())
|
|
end
|
|
|
|
function UI.GridLayout:draw()
|
|
|
|
local size = #self.values
|
|
local startRow = self:getStartRow()
|
|
local endRow = startRow + self.height - 1
|
|
if endRow > size then
|
|
endRow = size
|
|
end
|
|
|
|
for i = startRow, endRow do
|
|
self:drawRow(self.values[i], i)
|
|
end
|
|
|
|
if endRow - startRow < self.height - 1 then
|
|
self:clearArea(1, endRow, self.width, self.height - endRow)
|
|
end
|
|
end
|
|
|
|
function UI.GridLayout:getStartRow()
|
|
return 1
|
|
end
|
|
|
|
--[[-- StatusBar --]]--
|
|
UI.StatusBar = class(UI.GridLayout)
|
|
UI.StatusBar.defaults = {
|
|
UIElement = 'StatusBar',
|
|
backgroundColor = colors.gray,
|
|
columns = {
|
|
{ '', 'status', 10 },
|
|
},
|
|
values = { },
|
|
status = { status = '' },
|
|
}
|
|
function UI.StatusBar:init(args)
|
|
local defaults = UI:getDefaults(UI.StatusBar, args)
|
|
UI.GridLayout.init(self, defaults)
|
|
self:setStatus(self.status, true)
|
|
end
|
|
|
|
function UI.StatusBar:setParent()
|
|
UI.GridLayout.setParent(self)
|
|
self.y = self.height
|
|
self.height = 1
|
|
if #self.columns == 1 then
|
|
self.columns[1][3] = self.width
|
|
end
|
|
end
|
|
|
|
function UI.StatusBar:setStatus(status, noDraw)
|
|
if type(status) == 'string' then
|
|
self.values[1] = { status = status }
|
|
else
|
|
self.values[1] = status
|
|
end
|
|
if not noDraw then
|
|
self:draw()
|
|
end
|
|
end
|
|
|
|
function UI.StatusBar:setValue(name, value)
|
|
self.status[name] = value
|
|
end
|
|
|
|
function UI.StatusBar:getValue(name)
|
|
return self.status[name]
|
|
end
|
|
|
|
function UI.StatusBar:timedStatus(status, timeout)
|
|
timeout = timeout or 3
|
|
self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor)
|
|
Event.addNamedTimer('statusTimer', timeout, false, function()
|
|
if self.parent.enabled then
|
|
self:draw()
|
|
self:sync()
|
|
end
|
|
end)
|
|
end
|
|
|
|
function UI.StatusBar:getColumnWidth(name)
|
|
for _,v in pairs(self.columns) do
|
|
if v[2] == name then
|
|
return v[3]
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.StatusBar:setColumnWidth(name, width)
|
|
for _,v in pairs(self.columns) do
|
|
if v[2] == name then
|
|
v[3] = width
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- ProgressBar --]]--
|
|
UI.ProgressBar = class(UI.Window)
|
|
UI.ProgressBar.defaults = {
|
|
UIElement = 'ProgressBar',
|
|
progressColor = colors.lime,
|
|
backgroundColor = colors.gray,
|
|
height = 1,
|
|
value = 0,
|
|
}
|
|
function UI.ProgressBar:init(args)
|
|
local defaults = UI:getDefaults(UI.ProgressBar, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.ProgressBar:draw()
|
|
local width = math.ceil(self.value / 100 * self.width)
|
|
if width > 0 then
|
|
self:write(1, 1, string.rep(' ', width), self.progressColor)
|
|
end
|
|
local x = width
|
|
width = self.width - width
|
|
if width > 0 then
|
|
self:write(x + 1,
|
|
1, string.rep(' ', width), self.backgroundColor)
|
|
end
|
|
end
|
|
|
|
function UI.ProgressBar:setProgress(progress)
|
|
self.value = progress
|
|
end
|
|
|
|
--[[-- VerticalMeter --]]--
|
|
UI.VerticalMeter = class(UI.Window)
|
|
UI.VerticalMeter.defaults = {
|
|
UIElement = 'VerticalMeter',
|
|
meterColor = colors.lime,
|
|
height = 1,
|
|
value = 0,
|
|
}
|
|
function UI.VerticalMeter:init(args)
|
|
local defaults = UI:getDefaults(UI.VerticalMeter, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.VerticalMeter:draw()
|
|
local height = self.height - math.ceil(self.value / 100 * self.height)
|
|
local filler = string.rep(' ', self.width)
|
|
|
|
for i = 1, height do
|
|
self:write(1, i, filler, self.backgroundColor)
|
|
end
|
|
|
|
for i = height+1, self.height do
|
|
self:write(1, i, filler, self.meterColor)
|
|
end
|
|
end
|
|
|
|
function UI.VerticalMeter:setPercent(percent)
|
|
self.value = percent
|
|
end
|
|
|
|
--[[-- Button --]]--
|
|
UI.Button = class(UI.Window)
|
|
UI.Button.defaults = {
|
|
UIElement = 'Button',
|
|
text = 'button',
|
|
backgroundColor = colors.gray,
|
|
backgroundFocusColor = colors.green,
|
|
textFocusColor = colors.black,
|
|
centered = true,
|
|
height = 1,
|
|
focusIndicator = '>',
|
|
event = 'button_press',
|
|
accelerators = {
|
|
space = 'button_activate',
|
|
enter = 'button_activate',
|
|
mouse_click = 'button_activate',
|
|
}
|
|
}
|
|
function UI.Button:init(args)
|
|
local defaults = UI:getDefaults(UI.Button, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.Button:setParent()
|
|
if not self.width then
|
|
self.width = #self.text + 2
|
|
end
|
|
UI.Window.setParent(self)
|
|
end
|
|
|
|
function UI.Button:draw()
|
|
local fg = self.textColor
|
|
local bg = self.backgroundColor
|
|
local ind = ' '
|
|
if self.focused then
|
|
bg = self.backgroundFocusColor
|
|
fg = self.textFocusColor
|
|
ind = self.focusIndicator
|
|
end
|
|
self:clear(bg)
|
|
local text = ind .. self.text .. ' '
|
|
if self.centered then
|
|
self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg)
|
|
else
|
|
self:write(1, 1, Util.widthify(text, self.width), bg, fg)
|
|
end
|
|
end
|
|
|
|
function UI.Button:focus()
|
|
if self.focused then
|
|
self:scrollIntoView()
|
|
end
|
|
self:draw()
|
|
end
|
|
|
|
function UI.Button:eventHandler(event)
|
|
if event.type == 'button_activate' then
|
|
self:emit({ type = self.event, button = self })
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--[[-- TextEntry --]]--
|
|
UI.TextEntry = class(UI.Window)
|
|
UI.TextEntry.defaults = {
|
|
UIElement = 'TextEntry',
|
|
value = '',
|
|
shadowText = '',
|
|
focused = false,
|
|
backgroundColor = colors.lightGray,
|
|
backgroundFocusColor = colors.lightGray,
|
|
height = 1,
|
|
limit = 6,
|
|
pos = 0,
|
|
accelerators = {
|
|
[ 'control-c' ] = 'copy',
|
|
}
|
|
}
|
|
function UI.TextEntry:init(args)
|
|
local defaults = UI:getDefaults(UI.TextEntry, args)
|
|
UI.Window.init(self, defaults)
|
|
self.value = tostring(self.value)
|
|
end
|
|
|
|
function UI.TextEntry:setValue(value)
|
|
self.value = value
|
|
end
|
|
|
|
function UI.TextEntry:setPosition(pos)
|
|
self.pos = pos
|
|
end
|
|
|
|
function UI.TextEntry:updateScroll()
|
|
if not self.scroll then
|
|
self.scroll = 0
|
|
end
|
|
|
|
if not self.pos then
|
|
self.pos = #tostring(self.value)
|
|
self.scroll = 0
|
|
elseif self.pos > #tostring(self.value) then
|
|
self.pos = #tostring(self.value)
|
|
self.scroll = 0
|
|
end
|
|
|
|
if self.pos - self.scroll > self.width - 2 then
|
|
self.scroll = self.pos - (self.width - 2)
|
|
elseif self.pos < self.scroll then
|
|
self.scroll = self.pos
|
|
end
|
|
|
|
--debug('p:%d s:%d w:%d l:%d', self.pos, self.scroll, self.width, self.limit)
|
|
end
|
|
|
|
function UI.TextEntry:draw()
|
|
local bg = self.backgroundColor
|
|
local tc = self.textColor
|
|
if self.focused then
|
|
bg = self.backgroundFocusColor
|
|
end
|
|
|
|
self:updateScroll()
|
|
local text = tostring(self.value)
|
|
if #text > 0 then
|
|
if self.scroll and self.scroll > 0 then
|
|
text = text:sub(1 + self.scroll)
|
|
end
|
|
else
|
|
tc = colors.gray
|
|
text = self.shadowText
|
|
end
|
|
|
|
self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc)
|
|
if self.focused then
|
|
self:setCursorPos(self.pos-self.scroll+2, 1)
|
|
end
|
|
end
|
|
|
|
function UI.TextEntry:reset()
|
|
self.pos = 0
|
|
self.value = ''
|
|
self:draw()
|
|
self:updateCursor()
|
|
end
|
|
|
|
function UI.TextEntry:updateCursor()
|
|
self:updateScroll()
|
|
self:setCursorPos(self.pos-self.scroll+2, 1)
|
|
end
|
|
|
|
function UI.TextEntry:focus()
|
|
self:draw()
|
|
if self.focused then
|
|
self:setCursorBlink(true)
|
|
else
|
|
self:setCursorBlink(false)
|
|
end
|
|
end
|
|
|
|
--[[
|
|
A few lines below from theoriginalbit
|
|
http://www.computercraft.info/forums2/index.php?/topic/16070-read-and-limit-length-of-the-input-field/
|
|
--]]
|
|
function UI.TextEntry:eventHandler(event)
|
|
if event.type == 'key' then
|
|
local ch = event.key
|
|
if ch == 'left' then
|
|
if self.pos > 0 then
|
|
self.pos = math.max(self.pos-1, 0)
|
|
self:draw()
|
|
end
|
|
elseif ch == 'right' then
|
|
local input = tostring(self.value)
|
|
if self.pos < #input then
|
|
self.pos = math.min(self.pos+1, #input)
|
|
self:draw()
|
|
end
|
|
elseif ch == 'home' then
|
|
self.pos = 0
|
|
self:draw()
|
|
elseif ch == 'end' then
|
|
self.pos = #tostring(self.value)
|
|
self:draw()
|
|
elseif ch == 'backspace' then
|
|
if self.pos > 0 then
|
|
local input = tostring(self.value)
|
|
self.value = input:sub(1, self.pos-1) .. input:sub(self.pos+1)
|
|
self.pos = self.pos - 1
|
|
self:draw()
|
|
self:emit({ type = 'text_change', text = self.value })
|
|
end
|
|
elseif ch == 'delete' then
|
|
local input = tostring(self.value)
|
|
if self.pos < #input then
|
|
self.value = input:sub(1, self.pos) .. input:sub(self.pos+2)
|
|
self:draw()
|
|
self:emit({ type = 'text_change', text = self.value })
|
|
end
|
|
elseif #ch == 1 then
|
|
local input = tostring(self.value)
|
|
if #input < self.limit then
|
|
self.value = input:sub(1, self.pos) .. ch .. input:sub(self.pos+1)
|
|
self.pos = self.pos + 1
|
|
self:draw()
|
|
self:emit({ type = 'text_change', text = self.value })
|
|
end
|
|
else
|
|
return false
|
|
end
|
|
return true
|
|
|
|
elseif event.type == 'copy' then
|
|
clipboard.setData(self.value)
|
|
|
|
elseif event.type == 'paste' then
|
|
local input = tostring(self.value)
|
|
local text = event.text
|
|
if #input + #text > self.limit then
|
|
text = text:sub(1, self.limit-#input)
|
|
end
|
|
self.value = input:sub(1, self.pos) .. text .. input:sub(self.pos+1)
|
|
self.pos = self.pos + #text
|
|
self:draw()
|
|
self:updateCursor()
|
|
self:emit({ type = 'text_change', text = self.value })
|
|
return true
|
|
|
|
elseif event.type == 'mouse_click' then
|
|
if self.focused and event.x > 1 then
|
|
self.pos = event.x + self.scroll - 2
|
|
self:updateCursor()
|
|
return true
|
|
end
|
|
elseif event.type == 'mouse_rightclick' then
|
|
local input = tostring(self.value)
|
|
if #input > 0 then
|
|
self:reset()
|
|
self:emit({ type = 'text_change', text = self.value })
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--[[-- Chooser --]]--
|
|
UI.Chooser = class(UI.Window)
|
|
UI.Chooser.defaults = {
|
|
UIElement = 'Chooser',
|
|
choices = { },
|
|
nochoice = 'Select',
|
|
backgroundColor = colors.lightGray,
|
|
backgroundFocusColor = colors.green,
|
|
height = 1,
|
|
}
|
|
function UI.Chooser:init(args)
|
|
local defaults = UI:getDefaults(UI.Chooser, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.Chooser:setParent()
|
|
if not self.width then
|
|
self.width = 1
|
|
for _,v in pairs(self.choices) do
|
|
if #v.name > self.width then
|
|
self.width = #v.name
|
|
end
|
|
end
|
|
self.width = self.width + 4
|
|
end
|
|
UI.Window.setParent(self)
|
|
end
|
|
|
|
function UI.Chooser:draw()
|
|
local bg = self.backgroundColor
|
|
if self.focused then
|
|
bg = self.backgroundFocusColor
|
|
end
|
|
local choice = Util.find(self.choices, 'value', self.value)
|
|
local value = self.nochoice
|
|
if choice then
|
|
value = choice.name
|
|
end
|
|
self:write(1, 1, '<', bg, colors.black)
|
|
self:write(2, 1, ' ' .. Util.widthify(value, self.width-4) .. ' ', bg)
|
|
self:write(self.width, 1, '>', bg, colors.black)
|
|
end
|
|
|
|
function UI.Chooser:focus()
|
|
self:draw()
|
|
end
|
|
|
|
function UI.Chooser:eventHandler(event)
|
|
if event.type == 'key' then
|
|
if event.key == 'right' or event.key == 'space' then
|
|
local choice,k = Util.find(self.choices, 'value', self.value)
|
|
if k and k < #self.choices then
|
|
self.value = self.choices[k+1].value
|
|
else
|
|
self.value = self.choices[1].value
|
|
end
|
|
self:emit({ type = 'choice_change', value = self.value })
|
|
self:draw()
|
|
return true
|
|
elseif event.key == 'left' then
|
|
local choice,k = Util.find(self.choices, 'value', self.value)
|
|
if k and k > 1 then
|
|
self.value = self.choices[k-1].value
|
|
else
|
|
self.value = self.choices[#self.choices].value
|
|
end
|
|
self:emit({ type = 'choice_change', value = self.value })
|
|
self:draw()
|
|
return true
|
|
end
|
|
elseif event.type == 'mouse_click' then
|
|
if event.x == 1 then
|
|
self:emit({ type = 'key', key = 'left' })
|
|
return true
|
|
elseif event.x == self.width then
|
|
self:emit({ type = 'key', key = 'right' })
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[-- Text --]]--
|
|
UI.Text = class(UI.Window)
|
|
UI.Text.defaults = {
|
|
UIElement = 'Text',
|
|
value = '',
|
|
height = 1,
|
|
}
|
|
function UI.Text:init(args)
|
|
local defaults = UI:getDefaults(UI.Text, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.Text:setParent()
|
|
if not self.width then
|
|
self.width = #tostring(self.value)
|
|
end
|
|
UI.Window.setParent(self)
|
|
end
|
|
|
|
function UI.Text:draw()
|
|
local value = self.value or ''
|
|
self:write(1, 1, Util.widthify(value, self.width), self.backgroundColor)
|
|
end
|
|
|
|
--[[-- Form --]]--
|
|
UI.Form = class(UI.Window)
|
|
UI.Form.defaults = {
|
|
UIElement = 'Form',
|
|
values = { },
|
|
margin = 2,
|
|
event = 'form_complete',
|
|
}
|
|
function UI.Form:init(args)
|
|
local defaults = UI:getDefaults(UI.Form, args)
|
|
UI.Window.init(self, defaults)
|
|
self:createForm()
|
|
end
|
|
|
|
function UI.Form:reset()
|
|
for _,child in pairs(self.children) do
|
|
if child.reset then
|
|
child:reset()
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Form:setValues(values)
|
|
self:reset()
|
|
self.values = values
|
|
for k,child in pairs(self.children) do
|
|
if child.formKey then
|
|
-- this should be child:setValue(self.values[child.formKey])
|
|
-- so chooser can set default choice if null
|
|
-- null should be valid as well
|
|
child.value = self.values[child.formKey] or ''
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Form:createForm()
|
|
self.children = self.children or { }
|
|
|
|
if not self.labelWidth then
|
|
self.labelWidth = 1
|
|
for _, child in pairs(self) do
|
|
if type(child) == 'table' and child.UIElement then
|
|
if child.formLabel then
|
|
self.labelWidth = math.max(self.labelWidth, #child.formLabel + 2)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local y = self.margin
|
|
for _, child in pairs(self) do
|
|
if type(child) == 'table' and child.UIElement then
|
|
if child.formKey then
|
|
child.x = self.labelWidth + self.margin - 1
|
|
child.y = y
|
|
if not child.width and not child.rex then
|
|
child.rex = -self.margin
|
|
end
|
|
child.value = self.values[child.formKey] or ''
|
|
end
|
|
if child.formLabel then
|
|
table.insert(self.children, UI.Text {
|
|
x = self.margin,
|
|
y = y,
|
|
textColor = colors.black,
|
|
width = #child.formLabel,
|
|
value = child.formLabel,
|
|
})
|
|
end
|
|
if child.formKey or child.formLabel then
|
|
y = y + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
table.insert(self.children, UI.Button {
|
|
ry = -self.margin + 1, rx = -12 - self.margin + 1,
|
|
text = 'Ok',
|
|
event = 'form_ok',
|
|
})
|
|
table.insert(self.children, UI.Button {
|
|
ry = -self.margin + 1, rx = -7 - self.margin + 1,
|
|
text = 'Cancel',
|
|
event = 'form_cancel',
|
|
})
|
|
end
|
|
|
|
function UI.Form:validateField(field)
|
|
if field.required then
|
|
if not field.value or #field.value == 0 then
|
|
return false, 'Field is required'
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function UI.Form:eventHandler(event)
|
|
if event.type == 'form_ok' then
|
|
for _,child in pairs(self.children) do
|
|
if child.formKey then
|
|
local s, m = self:validateField(child)
|
|
if not s then
|
|
self:setFocus(child)
|
|
self:emit({ type = 'form_invalid', message = m, field = child })
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
for _,child in pairs(self.children) do
|
|
if child.formKey then
|
|
self.values[child.formKey] = child.value
|
|
end
|
|
end
|
|
self:emit({ type = self.event, UIElement = self })
|
|
else
|
|
return UI.Window.eventHandler(self, event)
|
|
end
|
|
return true
|
|
end
|
|
|
|
--[[-- Dialog --]]--
|
|
UI.Dialog = class(UI.Page)
|
|
UI.Dialog.defaults = {
|
|
UIElement = 'Dialog',
|
|
x = 7,
|
|
y = 4,
|
|
z = 2,
|
|
height = 7,
|
|
backgroundColor = colors.white,
|
|
}
|
|
function UI.Dialog:init(args)
|
|
local defaults = UI:getDefaults(UI.Dialog, args)
|
|
|
|
if not defaults.width then
|
|
defaults.width = UI.term.width-11
|
|
end
|
|
defaults.titleBar = UI.TitleBar({ previousPage = true, title = defaults.title })
|
|
|
|
--UI.setProperties(defaults, args)
|
|
UI.Page.init(self, defaults)
|
|
end
|
|
|
|
function UI.Dialog:setParent()
|
|
UI.Window.setParent(self)
|
|
self.x = math.floor((self.parent.width - self.width) / 2) + 1
|
|
self.y = math.floor((self.parent.height - self.height) / 2) + 1
|
|
end
|
|
|
|
function UI.Dialog:eventHandler(event)
|
|
if event.type == 'cancel' then
|
|
UI:setPreviousPage()
|
|
end
|
|
return UI.Page.eventHandler(self, event)
|
|
end
|
|
|
|
--[[-- Image --]]--
|
|
UI.Image = class(UI.Window)
|
|
UI.Image.defaults = {
|
|
UIElement = 'Image',
|
|
event = 'button_press',
|
|
}
|
|
function UI.Image:init(args)
|
|
local defaults = UI:getDefaults(UI.Image, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.Image:setParent()
|
|
if self.image then
|
|
self.height = #self.image
|
|
end
|
|
if self.image and not self.width then
|
|
self.width = #self.image[1]
|
|
end
|
|
UI.Window.setParent(self)
|
|
end
|
|
|
|
function UI.Image:draw()
|
|
self:clear()
|
|
if self.image then
|
|
for y = 1, #self.image do
|
|
local line = self.image[y]
|
|
for x = 1, #line do
|
|
local ch = line[x]
|
|
if type(ch) == 'number' then
|
|
if ch > 0 then
|
|
self:write(x, y, ' ', ch)
|
|
end
|
|
else
|
|
self:write(x, y, ch)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function UI.Image:setImage(image)
|
|
self.image = image
|
|
end
|
|
|
|
--[[-- NftImage --]]--
|
|
UI.NftImage = class(UI.Window)
|
|
UI.NftImage.defaults = {
|
|
UIElement = 'NftImage',
|
|
event = 'button_press',
|
|
}
|
|
function UI.NftImage:init(args)
|
|
local defaults = UI:getDefaults(UI.NftImage, args)
|
|
UI.Window.init(self, defaults)
|
|
end
|
|
|
|
function UI.NftImage:setParent()
|
|
if self.image then
|
|
self.height = self.image.height
|
|
end
|
|
if self.image and not self.width then
|
|
self.width = self.image.width
|
|
end
|
|
UI.Window.setParent(self)
|
|
end
|
|
|
|
function UI.NftImage:draw()
|
|
-- self:clear()
|
|
if self.image then
|
|
for y = 1, self.image.height do
|
|
for x = 1, #self.image.text[y] do
|
|
self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x])
|
|
end
|
|
end
|
|
else
|
|
self:clear()
|
|
end
|
|
end
|
|
|
|
function UI.NftImage:setImage(image)
|
|
self.image = image
|
|
end
|
|
|
|
UI:loadTheme('usr/config/ui.theme')
|
|
if Util.getVersion() >= 1.79 then
|
|
UI:loadTheme('sys/etc/ext.theme')
|
|
end
|
|
|
|
UI:setDefaultDevice(UI.Device({ device = term.current() }))
|
|
|
|
return UI
|