Ui enhancements 2.0 (#31)

* canvas overhaul

* minor tweaks

* list mode for overview

* bugfixes + tweaks for editor 2.0

* minor tweaks

* more editor work

* refactor + new transitions

* use layout() where appropriate and cleanup

* mouse triple click + textEntry scroll ind

* cleanup

* cleanup + theme editor

* color rework + cleanup

* changes for deprecated ui methods

* can now use named colors
This commit is contained in:
kepler155c 2020-04-21 22:40:59 -06:00 committed by GitHub
parent cdd0b6c4d2
commit 7224d441ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 2471 additions and 1773 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/ignore /ignore
.project

View File

@ -82,16 +82,60 @@ local Browser = UI.Page {
}, },
sortColumn = 'name', sortColumn = 'name',
y = 2, ey = -2, y = 2, ey = -2,
sortCompare = function(self, a, b)
if self.sortColumn == 'fsize' then
return a.size < b.size
elseif self.sortColumn == 'flags' then
return a.flags < b.flags
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end,
getRowTextColor = function(_, file)
if file.marked then
return colors.green
end
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end,
eventHandler = function(self, event)
if event.type == 'copy' then -- let copy be handled by parent
return false
end
return UI.ScrollingGrid.eventHandler(self, event)
end
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar {
columns = { columns = {
{ key = 'status' }, { key = 'status' },
{ key = 'totalSize', width = 6 }, { key = 'totalSize', width = 6 },
}, },
draw = function(self)
if self.parent.dir then
local info = '#:' .. Util.size(self.parent.dir.files)
local numMarked = Util.size(marked)
if numMarked > 0 then
info = info .. ' M:' .. numMarked
end
self:setValue('info', info)
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
UI.StatusBar.draw(self)
end
end,
},
question = UI.Question {
y = -2, x = -19,
label = 'Delete',
}, },
notification = UI.Notification { }, notification = UI.Notification { },
associations = UI.SlideOut { associations = UI.SlideOut {
backgroundColor = colors.cyan,
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = 'Save', event = 'save' }, { text = 'Save', event = 'save' },
@ -99,7 +143,7 @@ local Browser = UI.Page {
}, },
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
x = 2, ex = -6, y = 3, ey = -5, x = 2, ex = -6, y = 3, ey = -8,
columns = { columns = {
{ heading = 'Extension', key = 'name' }, { heading = 'Extension', key = 'name' },
{ heading = 'Program', key = 'value' }, { heading = 'Program', key = 'value' },
@ -114,8 +158,11 @@ local Browser = UI.Page {
x = -4, y = 6, x = -4, y = 6,
text = '-', event = 'remove_entry', help = 'Remove', text = '-', event = 'remove_entry', help = 'Remove',
}, },
[1] = UI.Window {
x = 2, y = -6, ex = -6, ey = -3,
},
form = UI.Form { form = UI.Form {
x = 3, y = -3, ey = -2, x = 3, y = -5, ex = -7, ey = -3,
margin = 1, margin = 1,
manualControls = true, manualControls = true,
[1] = UI.TextEntry { [1] = UI.TextEntry {
@ -137,9 +184,7 @@ local Browser = UI.Page {
text = 'Add', event = 'add_association', text = 'Add', event = 'add_association',
}, },
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar { },
backgroundColor = colors.cyan,
},
}, },
accelerators = { accelerators = {
[ 'control-q' ] = 'quit', [ 'control-q' ] = 'quit',
@ -175,51 +220,6 @@ function Browser.menuBar:getActive(menuItem)
return true return true
end end
function Browser.grid:sortCompare(a, b)
if self.sortColumn == 'fsize' then
return a.size < b.size
elseif self.sortColumn == 'flags' then
return a.flags < b.flags
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end
function Browser.grid:getRowTextColor(file)
if file.marked then
return colors.green
end
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end
function Browser.grid:eventHandler(event)
if event.type == 'copy' then -- let copy be handled by parent
return false
end
return UI.ScrollingGrid.eventHandler(self, event)
end
function Browser.statusBar:draw()
if self.parent.dir then
local info = '#:' .. Util.size(self.parent.dir.files)
local numMarked = Util.size(marked)
if numMarked > 0 then
info = info .. ' M:' .. numMarked
end
self:setValue('info', info)
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
UI.StatusBar.draw(self)
end
end
function Browser:setStatus(status, ...) function Browser:setStatus(status, ...)
self.notification:info(string.format(status, ...)) self.notification:info(string.format(status, ...))
end end
@ -255,7 +255,6 @@ function Browser:getDirectory(directory)
end end
function Browser:updateDirectory(dir) function Browser:updateDirectory(dir)
dir.size = 0 dir.size = 0
dir.totalSize = 0 dir.totalSize = 0
Util.clear(dir.files) Util.clear(dir.files)
@ -344,7 +343,7 @@ function Browser:eventHandler(event)
local file = self.grid:getSelected() local file = self.grid:getSelected()
if event.type == 'quit' then if event.type == 'quit' then
Event.exitPullEvents() UI:quit()
elseif event.type == 'edit' and file then elseif event.type == 'edit' and file then
self:run('edit', file.name) self:run('edit', file.name)
@ -432,28 +431,25 @@ function Browser:eventHandler(event)
elseif event.type == 'delete' then elseif event.type == 'delete' then
if self:hasMarked() then if self:hasMarked() then
local width = self.statusBar:getColumnWidth('status') self.question:show()
self.statusBar:setColumnWidth('status', UI.term.width)
self.statusBar:setValue('status', 'Delete marked? (y/n)')
self.statusBar:draw()
self.statusBar:sync()
local _, ch = os.pullEvent('char')
if ch == 'y' or ch == 'Y' then
for _,m in pairs(marked) do
pcall(function()
fs.delete(m.fullName)
end)
end
end
marked = { }
self.statusBar:setColumnWidth('status', width)
self.statusBar:setValue('status', '/' .. self.dir.name)
self:updateDirectory(self.dir)
self.statusBar:draw()
self.grid:draw()
self:setFocus(self.grid)
end end
return true
elseif event.type == 'question_yes' then
for _,m in pairs(marked) do
pcall(fs.delete, m.fullName)
end
marked = { }
self:updateDirectory(self.dir)
self.question:hide()
self.statusBar:draw()
self.grid:draw()
self:setFocus(self.grid)
elseif event.type == 'question_no' then
self.question:hide()
self:setFocus(self.grid)
elseif event.type == 'copy' or event.type == 'cut' then elseif event.type == 'copy' or event.type == 'cut' then
if self:hasMarked() then if self:hasMarked() then
@ -549,6 +545,4 @@ local args = Util.parse(...)
Browser:setDir(args[1] or shell.dir()) Browser:setDir(args[1] or shell.dir())
UI:setPage(Browser) UI:setPage(Browser)
UI:start()
Event.pullEvents()
UI.term:reset()

View File

@ -1,7 +1,6 @@
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local help = _G.help local help = _G.help
UI:configure('Help', ...) UI:configure('Help', ...)
@ -12,11 +11,11 @@ for _,topic in pairs(help.topics()) do
end end
UI:addPage('main', UI.Page { UI:addPage('main', UI.Page {
labelText = UI.Text { UI.Text {
x = 3, y = 2, x = 3, y = 2,
value = 'Search', value = 'Search',
}, },
filter = UI.TextEntry { UI.TextEntry {
x = 10, y = 2, ex = -3, x = 10, y = 2, ex = -3,
limit = 32, limit = 32,
}, },
@ -38,9 +37,7 @@ UI:addPage('main', UI.Page {
elseif event.type == 'grid_select' then elseif event.type == 'grid_select' then
if self.grid:getSelected() then if self.grid:getSelected() then
local name = self.grid:getSelected().name UI:setPage('topic', self.grid:getSelected().name)
UI:setPage('topic', name)
end end
elseif event.type == 'text_change' then elseif event.type == 'text_change' then
@ -57,6 +54,7 @@ UI:addPage('main', UI.Page {
self.grid:update() self.grid:update()
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:draw() self.grid:draw()
else else
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
end end
@ -64,13 +62,12 @@ UI:addPage('main', UI.Page {
}) })
UI:addPage('topic', UI.Page { UI:addPage('topic', UI.Page {
backgroundColor = colors.black, backgroundColor = 'black',
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'text', title = 'text',
event = 'back', event = 'back',
}, },
helpText = UI.TextArea { helpText = UI.TextArea {
backgroundColor = colors.black,
x = 2, ex = -1, y = 3, ey = -2, x = 2, ex = -1, y = 3, ey = -2,
}, },
accelerators = { accelerators = {

View File

@ -58,11 +58,16 @@ local page = UI.Page {
}, },
[2] = UI.Tab { [2] = UI.Tab {
tabTitle = 'Output', tabTitle = 'Output',
backgroundColor = 'black',
output = UI.Embedded { output = UI.Embedded {
visible = true, y = 2,
maxScroll = 1000, maxScroll = 1000,
backgroundColor = colors.black, backgroundColor = 'black',
}, },
draw = function(self)
self:write(1, 1, string.rep('\131', self.width), 'black', 'primary')
self:drawChildren()
end,
}, },
}, },
} }
@ -157,7 +162,7 @@ function page:eventHandler(event)
local sz = #value local sz = #value
local pos = self.prompt.entry.pos local pos = self.prompt.entry.pos
self:setPrompt(autocomplete(sandboxEnv, value, self.prompt.entry.pos)) self:setPrompt(autocomplete(sandboxEnv, value, self.prompt.entry.pos))
self.prompt:setPosition(pos + #value - sz) self.prompt:setPosition(pos + #(self.prompt.value or '') - sz)
self.prompt:updateCursor() self.prompt:updateCursor()
elseif event.type == 'device' then elseif event.type == 'device' then
@ -196,7 +201,6 @@ function page:eventHandler(event)
command = nil command = nil
self.grid:setValues(t) self.grid:setValues(t)
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw() self:draw()
end end
return true return true
@ -243,7 +247,6 @@ function page:setResult(result)
end end
self.grid:setValues(t) self.grid:setValues(t)
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw() self:draw()
end end
@ -373,7 +376,7 @@ function page:executeStatement(statement)
end end
if _exit then if _exit then
UI:exitPullEvents() UI:quit()
end end
end end
@ -382,7 +385,8 @@ if args[1] then
command = 'args[1]' command = 'args[1]'
sandboxEnv.args = args sandboxEnv.args = args
page:setResult(args[1]) page:setResult(args[1])
page:setPrompt(command)
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@ -4,7 +4,6 @@ local Socket = require('opus.socket')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local device = _G.device local device = _G.device
local network = _G.network local network = _G.network
local os = _G.os local os = _G.os
@ -56,6 +55,31 @@ local page = UI.Page {
columns = gridColumns, columns = gridColumns,
sortColumn = 'label', sortColumn = 'label',
autospace = true, autospace = true,
getRowTextColor = function(self, row, selected)
if not row.active then
return 'lightGray'
end
return UI.Grid.getRowTextColor(self, row, selected)
end,
getDisplayValues = function(_, row)
row = Util.shallowCopy(row)
if row.uptime then
if row.uptime < 60 then
row.uptime = string.format("%ds", math.floor(row.uptime))
elseif row.uptime < 3600 then
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
else
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
end
end
if row.fuel then
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
end
if row.distance then
row.distance = Util.toBytes(Util.round(row.distance, 1))
end
return row
end,
}, },
ports = UI.SlideOut { ports = UI.SlideOut {
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
@ -72,17 +96,22 @@ local page = UI.Page {
sortColumn = 'port', sortColumn = 'port',
autospace = true, autospace = true,
}, },
eventHandler = function(self, event)
if event.type == 'grid_select' then
shell.openForegroundTab('Sniff ' .. event.selected.port)
end
return UI.SlideOut.eventHandler(self, event)
end,
}, },
help = UI.SlideOut { help = UI.SlideOut {
backgroundColor = colors.cyan,
x = 5, ex = -5, height = 8, y = -8, x = 5, ex = -5, height = 8, y = -8,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'Network Help', title = 'Network Help',
event = 'slide_hide', event = 'slide_hide',
}, },
text = UI.TextArea { text = UI.TextArea {
x = 2, y = 2, x = 1, y = 2,
backgroundColor = colors.cyan, marginLeft = 1,
value = [[ value = [[
In order to connect to another computer: In order to connect to another computer:
@ -127,13 +156,6 @@ local function sendCommand(host, command)
end end
end end
function page.ports:eventHandler(event)
if event.type == 'grid_select' then
shell.openForegroundTab('Sniff ' .. event.selected.port)
end
return UI.SlideOut.eventHandler(self, event)
end
function page.ports.grid:update() function page.ports.grid:update()
local transport = network:getTransport() local transport = network:getTransport()
@ -230,7 +252,7 @@ function page:eventHandler(event)
Config.update('network', config) Config.update('network', config)
elseif event.type == 'quit' then elseif event.type == 'quit' then
Event.exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@ -243,33 +265,6 @@ function page.menuBar:getActive(menuItem)
return menuItem.noCheck or not not t return menuItem.noCheck or not not t
end end
function page.grid:getRowTextColor(row, selected)
if not row.active then
return colors.lightGray
end
return UI.Grid.getRowTextColor(self, row, selected)
end
function page.grid:getDisplayValues(row)
row = Util.shallowCopy(row)
if row.uptime then
if row.uptime < 60 then
row.uptime = string.format("%ds", math.floor(row.uptime))
elseif row.uptime < 3600 then
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
else
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
end
end
if row.fuel then
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
end
if row.distance then
row.distance = Util.toBytes(Util.round(row.distance, 1))
end
return row
end
Event.onInterval(1, function() Event.onInterval(1, function()
page.grid:update() page.grid:update()
page.grid:draw() page.grid:draw()
@ -295,4 +290,4 @@ if not device.wireless_modem then
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@ -1,4 +1,5 @@
local Alt = require('opus.alternate') local Alt = require('opus.alternate')
local Array = require('opus.array')
local class = require('opus.class') local class = require('opus.class')
local Config = require('opus.config') local Config = require('opus.config')
local Event = require('opus.event') local Event = require('opus.event')
@ -9,7 +10,6 @@ local Tween = require('opus.ui.tween')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local device = _G.device local device = _G.device
local fs = _G.fs local fs = _G.fs
local os = _G.os local os = _G.os
@ -18,6 +18,12 @@ local shell = _ENV.shell
local term = _G.term local term = _G.term
local turtle = _G.turtle local turtle = _G.turtle
--[[
turtle: 39x13
computer: 51x19
pocket: 26x20
]]
if not _ENV.multishell then if not _ENV.multishell then
error('multishell is required') error('multishell is required')
end end
@ -26,6 +32,9 @@ local REGISTRY_DIR = 'usr/.registry'
local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\ local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\
\0307\0318\153\153\153\153\153\ \0307\0318\153\153\153\153\153\
\0308\0317\153\153\153\153\153") \0308\0317\153\153\153\153\153")
local TRANS_ICON = NFT.parse("\0302\0312\32\32\32\32\32\
\0302\0312\32\32\32\32\32\
\0302\0312\32\32\32\32\32")
-- overview -- overview
local uid = _ENV.multishell.getCurrent() local uid = _ENV.multishell.getCurrent()
@ -65,6 +74,7 @@ local function parseIcon(iconText)
if icon.height > 3 or icon.width > 8 then if icon.height > 3 or icon.width > 8 then
error('Must be an NFT image - 3 rows, 8 cols max') error('Must be an NFT image - 3 rows, 8 cols max')
end end
NFT.transparency(icon)
end end
return icon return icon
end) end)
@ -76,45 +86,38 @@ local function parseIcon(iconText)
return s, m return s, m
end end
UI.VerticalTabBar = class(UI.TabBar)
function UI.VerticalTabBar:setParent()
self.x = 1
self.width = 8
self.height = nil
self.ey = -2
UI.TabBar.setParent(self)
for k,c in pairs(self.children) do
c.x = 1
c.y = k + 1
c.ox, c.oy = c.x, c.y
c.ow = 8
c.width = 8
end
end
local cx = 9
local cy = 1
local page = UI.Page { local page = UI.Page {
container = UI.Viewport { container = UI.Viewport {
x = cx, x = 9, y = 1,
y = cy, },
tabBar = UI.TabBar {
ey = -2,
width = 8,
selectedBackgroundColor = 'primary',
backgroundColor = 'tertiary',
layout = function(self)
self.height = nil
UI.TabBar.layout(self)
end,
}, },
tray = UI.Window { tray = UI.Window {
y = -1, width = 8, y = -1, width = 8,
backgroundColor = colors.lightGray, backgroundColor = 'tertiary',
newApp = UI.Button { newApp = UI.FlatButton {
x = 2,
text = '+', event = 'new', text = '+', event = 'new',
}, },
--[[ mode = UI.FlatButton {
volume = UI.Button { x = 4,
x = 3, text = '=', event = 'display_mode',
text = '\15', event = 'volume', },
},]] help = UI.FlatButton {
x = 6,
text = '?', event = 'help',
},
}, },
editor = UI.SlideOut { editor = UI.SlideOut {
y = -12, height = 12, y = -12, height = 12,
backgroundColor = colors.cyan,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'Edit Application', title = 'Edit Application',
event = 'slide_hide', event = 'slide_hide',
@ -122,7 +125,7 @@ local page = UI.Page {
form = UI.Form { form = UI.Form {
y = 2, ey = -2, y = 2, ey = -2,
[1] = UI.TextEntry { [1] = UI.TextEntry {
formLabel = 'Title', formKey = 'title', limit = 11, help = 'Application title', formLabel = 'Title', formKey = 'title', limit = 11, width = 13, help = 'Application title',
required = true, required = true,
}, },
[2] = UI.TextEntry { [2] = UI.TextEntry {
@ -130,23 +133,50 @@ local page = UI.Page {
required = true, required = true,
}, },
[3] = UI.TextEntry { [3] = UI.TextEntry {
formLabel = 'Category', formKey = 'category', limit = 11, help = 'Category of application', formLabel = 'Category', formKey = 'category', limit = 6, width = 8, help = 'Category of application',
required = true, required = true,
}, },
iconFile = UI.TextEntry { editIcon = UI.Button {
x = 11, ex = -12, y = 7, x = 11, y = 6,
limit = 128, help = 'Path to icon file', text = 'Edit', event = 'editIcon', help = 'Edit icon file',
shadowText = 'Path to icon file',
}, },
loadIcon = UI.Button { loadIcon = UI.Button {
x = 11, y = 9, x = 11, y = 8,
text = 'Load', event = 'loadIcon', help = 'Load icon file',
},
helpIcon = UI.Button {
x = 11, y = 8,
text = 'Load', event = 'loadIcon', help = 'Load icon file', text = 'Load', event = 'loadIcon', help = 'Load icon file',
}, },
image = UI.NftImage { image = UI.NftImage {
backgroundColor = colors.black, backgroundColor = 'black',
y = 7, x = 2, height = 3, width = 8, y = 6, x = 2, height = 3, width = 8,
}, },
}, },
file_open = UI.FileSelect {
modal = true,
enable = function() end,
transitionHint = 'expandUp',
show = function(self)
UI.FileSelect.enable(self)
self:focusFirst()
self:draw()
end,
disable = function(self)
UI.FileSelect.disable(self)
self.parent:focusFirst()
-- need to recapture as we are opening a modal within another modal
self.parent:capture(self.parent)
end,
eventHandler = function(self, event)
if event.type == 'select_cancel' then
self:disable()
elseif event.type == 'select_file' then
self:disable()
end
return UI.FileSelect.eventHandler(self, event)
end,
},
notification = UI.Notification(), notification = UI.Notification(),
statusBar = UI.StatusBar(), statusBar = UI.StatusBar(),
}, },
@ -205,7 +235,7 @@ local function loadApplications()
return requirements[a.requires] return requirements[a.requires]
end end
return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run) return true
end) end)
local categories = { } local categories = { }
@ -215,6 +245,7 @@ local function loadApplications()
categories[f.category] = true categories[f.category] = true
table.insert(buttons, { table.insert(buttons, {
text = f.category, text = f.category,
width = 8,
selected = config.currentCategory == f.category selected = config.currentCategory == f.category
}) })
end end
@ -222,13 +253,13 @@ local function loadApplications()
table.sort(buttons, function(a, b) return a.text < b.text end) table.sort(buttons, function(a, b) return a.text < b.text end)
table.insert(buttons, 1, { text = 'Recent' }) table.insert(buttons, 1, { text = 'Recent' })
Util.removeByValue(page.children, page.tabBar) for k,v in pairs(buttons) do
v.x = 1
v.y = k + 1
end
page:add { page.tabBar.children = { }
tabBar = UI.VerticalTabBar { page.tabBar:addButtons(buttons)
buttons = buttons,
},
}
--page.tabBar:selectTab(config.currentCategory or 'Apps') --page.tabBar:selectTab(config.currentCategory or 'Apps')
page.container:setCategory(config.currentCategory or 'Apps') page.container:setCategory(config.currentCategory or 'Apps')
@ -243,7 +274,6 @@ UI.Icon.defaults = {
function UI.Icon:eventHandler(event) function UI.Icon:eventHandler(event)
if event.type == 'mouse_click' then if event.type == 'mouse_click' then
self:setFocus(self.button) self:setFocus(self.button)
--self:emit({ type = self.button.event, button = self.button })
return true return true
elseif event.type == 'mouse_doubleclick' then elseif event.type == 'mouse_doubleclick' then
self:emit({ type = self.button.event, button = self.button }) self:emit({ type = self.button.event, button = self.button })
@ -259,37 +289,23 @@ function page.container:setCategory(categoryName, animate)
self.children = { } self.children = { }
self:reset() self:reset()
local function filter(it, f) local filtered = { }
local ot = { }
for _,v in pairs(it) do
if f(v) then
table.insert(ot, v)
end
end
return ot
end
local filtered
if categoryName == 'Recent' then if categoryName == 'Recent' then
filtered = { }
for _,v in ipairs(config.Recent) do for _,v in ipairs(config.Recent) do
local app = Util.find(applications, 'key', v) local app = Util.find(applications, 'key', v)
if app then -- and fs.exists(app.run) then if app then
table.insert(filtered, app) table.insert(filtered, app)
end end
end end
else else
filtered = filter(applications, function(a) filtered = Array.filter(applications, function(a)
return a.category == categoryName -- and fs.exists(a.run) return a.category == categoryName
end) end)
table.sort(filtered, function(a, b) return a.title < b.title end) table.sort(filtered, function(a, b) return a.title < b.title end)
end end
for _,program in ipairs(filtered) do for _,program in ipairs(filtered) do
local icon local icon
if extSupport and program.iconExt then if extSupport and program.iconExt then
icon = parseIcon(program.iconExt) icon = parseIcon(program.iconExt)
@ -304,27 +320,43 @@ function page.container:setCategory(categoryName, animate)
local title = ellipsis(program.title, 8) local title = ellipsis(program.title, 8)
local width = math.max(icon.width + 2, #title + 2) local width = math.max(icon.width + 2, #title + 2)
table.insert(self.children, UI.Icon({ if config.listMode then
width = width, table.insert(self.children, UI.Icon {
image = UI.NftImage({ width = self.width - 2,
x = math.floor((width - icon.width) / 2) + 1, height = 1,
image = icon, UI.Button {
width = 5, x = 1, ex = -1,
height = 3, text = program.title,
}), centered = false,
button = UI.Button({ backgroundColor = self:getProperty('backgroundColor'),
x = math.floor((width - #title - 2) / 2) + 1, backgroundFocusColor = 'gray',
y = 4, textColor = 'white',
text = title, textFocusColor = 'white',
backgroundColor = self.backgroundColor, event = 'button',
backgroundFocusColor = colors.gray, app = program,
textColor = colors.white, }
textFocusColor = colors.white, })
width = #title + 2, else
event = 'button', table.insert(self.children, UI.Icon({
app = program, width = width,
}), image = UI.NftImage({
})) x = math.floor((width - icon.width) / 2) + 1,
image = icon,
}),
button = UI.Button({
x = math.floor((width - #title - 2) / 2) + 1,
y = 4,
text = title,
backgroundColor = self:getProperty('backgroundColor'),
backgroundFocusColor = 'gray',
textColor = 'white',
textFocusColor = 'white',
width = #title + 2,
event = 'button',
app = program,
}),
}))
end
end end
local gutter = 2 local gutter = 2
@ -334,7 +366,8 @@ function page.container:setCategory(categoryName, animate)
local col, row = gutter, 2 local col, row = gutter, 2
local count = #self.children local count = #self.children
local r = math.random(1, 5) local r = math.random(1, 7)
local frames = 5
-- reposition all children -- reposition all children
for k,child in ipairs(self.children) do for k,child in ipairs(self.children) do
if r == 1 then if r == 1 then
@ -356,19 +389,27 @@ function page.container:setCategory(categoryName, animate)
child.x = self.width child.x = self.width
child.y = self.height - 3 child.y = self.height - 3
end end
elseif r == 6 then
child.x = col
child.y = 1
elseif r == 7 then
child.x = 1
child.y = self.height - 3
end end
child.tween = Tween.new(6, child, { x = col, y = row }, 'linear') child.tween = Tween.new(frames, child, { x = col, y = row }, 'inQuad')
if not animate then if not animate then
child.x = col child.x = col
child.y = row child.y = row
end end
self:setViewHeight(row + (config.listMode and 1 or 4))
if k < count then if k < count then
col = col + child.width col = col + child.width
if col + self.children[k + 1].width + gutter - 2 > self.width then if col + self.children[k + 1].width + gutter - 2 > self.width then
col = gutter col = gutter
row = row + 5 row = row + (config.listMode and 1 or 5)
end end
end end
end end
@ -378,15 +419,12 @@ function page.container:setCategory(categoryName, animate)
local function transition() local function transition()
local i = 1 local i = 1
return function() return function()
self:clear()
for _,child in pairs(self.children) do for _,child in pairs(self.children) do
child.tween:update(1) child.tween:update(1)
child.x = math.floor(child.x) child:move(math.floor(child.x), math.floor(child.y))
child.y = math.floor(child.y)
child:draw()
end end
i = i + 1 i = i + 1
return i < 7 return i <= frames
end end
end end
self:addTransition(transition) self:addTransition(transition)
@ -439,6 +477,9 @@ function page:eventHandler(event)
elseif event.type == 'network' then elseif event.type == 'network' then
shell.switchTab(shell.openTab('network')) shell.switchTab(shell.openTab('network'))
elseif event.type == 'help' then
shell.switchTab(shell.openTab('Help Overview'))
elseif event.type == 'focus_change' then elseif event.type == 'focus_change' then
if event.focused.parent.UIElement == 'Icon' then if event.focused.parent.UIElement == 'Icon' then
event.focused.parent:scrollIntoView() event.focused.parent:scrollIntoView()
@ -473,6 +514,13 @@ function page:eventHandler(event)
end end
self.editor:show({ category = category }) self.editor:show({ category = category })
elseif event.type == 'display_mode' then
config.listMode = not config.listMode
Config.update('Overview', config)
loadApplications()
self:refresh()
self:draw()
elseif event.type == 'edit' then elseif event.type == 'edit' then
local focused = page:getFocused() local focused = page:getFocused()
if focused.app then if focused.app then
@ -480,7 +528,7 @@ function page:eventHandler(event)
end end
else else
UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
end end
return true return true
end end
@ -502,11 +550,6 @@ function page.editor:show(app)
self:focusFirst() self:focusFirst()
end end
function page.editor.form.image:draw()
self:clear()
UI.NftImage.draw(self)
end
function page.editor:updateApplications(app) function page.editor:updateApplications(app)
if not app.key then if not app.key then
app.key = SHA.compute(app.title) app.key = SHA.compute(app.title)
@ -516,36 +559,51 @@ function page.editor:updateApplications(app)
loadApplications() loadApplications()
end end
function page.editor:loadImage(filename)
local s, m = pcall(function()
local iconLines = Util.readFile(filename)
if not iconLines then
error('Must be an NFT image - 3 rows, 8 cols max')
end
local icon, m = parseIcon(iconLines)
if not icon then
error(m)
end
if extSupport then
self.form.values.iconExt = iconLines
else
self.form.values.icon = iconLines
end
self.form.image:setImage(icon)
self.form.image:draw()
end)
if not s and m then
local msg = m:gsub('.*: (.*)', '%1')
self.notification:error(msg)
end
end
function page.editor:eventHandler(event) function page.editor:eventHandler(event)
if event.type == 'form_cancel' or event.type == 'cancel' then if event.type == 'form_cancel' or event.type == 'cancel' then
self:hide() self:hide()
elseif event.type == 'focus_change' then elseif event.type == 'focus_change' then
self.statusBar:setStatus(event.focused.help or '') self.statusBar:setStatus(event.focused.help or '')
self.statusBar:draw()
elseif event.type == 'editIcon' then
local filename = '/tmp/editing.nft'
NFT.save(self.form.image.image or TRANS_ICON, filename)
local success = shell.run('pain.lua ' .. filename)
self.parent:dirty(true)
if success then
self:loadImage(filename)
end
elseif event.type == 'select_file' then
self:loadImage(event.file)
elseif event.type == 'loadIcon' then elseif event.type == 'loadIcon' then
local s, m = pcall(function() self.file_open:show()
local iconLines = Util.readFile(self.form.iconFile.value)
if not iconLines then
error('Must be an NFT image - 3 rows, 8 cols max')
end
local icon, m = parseIcon(iconLines)
if not icon then
error(m)
end
if extSupport then
self.form.values.iconExt = iconLines
else
self.form.values.icon = iconLines
end
self.form.image:setImage(icon)
self.form.image:draw()
end)
if not s and m then
local msg = m:gsub('.*: (.*)', '%1')
self.notification:error(msg)
end
elseif event.type == 'form_invalid' then elseif event.type == 'form_invalid' then
self.notification:error(event.message) self.notification:error(event.message)
@ -554,8 +612,6 @@ function page.editor:eventHandler(event)
local values = self.form.values local values = self.form.values
self:hide() self:hide()
self:updateApplications(values) self:updateApplications(values)
--page:refresh()
--page:draw()
config.currentCategory = values.category config.currentCategory = values.category
Config.update('Overview', config) Config.update('Overview', config)
os.queueEvent('overview_refresh') os.queueEvent('overview_refresh')
@ -565,10 +621,6 @@ function page.editor:eventHandler(event)
return true return true
end end
UI:setPages({
main = page,
})
local function reload() local function reload()
loadApplications() loadApplications()
page:refresh() page:refresh()
@ -594,5 +646,4 @@ end)
loadApplications() loadApplications()
UI:setPage(page) UI:setPage(page)
UI:start()
UI:pullEvents()

View File

@ -44,7 +44,6 @@ local page = UI.Page {
marginRight = 0, marginLeft = 0, marginRight = 0, marginLeft = 0,
}, },
action = UI.SlideOut { action = UI.SlideOut {
backgroundColor = colors.cyan,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
event = 'hide-action', event = 'hide-action',
}, },
@ -184,7 +183,7 @@ function page:eventHandler(event)
self.action.button:draw() self.action.button:draw()
elseif event.type == 'quit' then elseif event.type == 'quit' then
UI:exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@ -196,4 +195,4 @@ Packages:downloadList()
page:loadPackages() page:loadPackages()
page:sync() page:sync()
UI:pullEvents() UI:start()

View File

@ -5,14 +5,13 @@ local Util = require('opus.util')
local colors = _G.colors local colors = _G.colors
local device = _G.device local device = _G.device
local textutils = _G.textutils local textutils = _G.textutils
local peripheral = _G.peripheral
local multishell = _ENV.multishell local multishell = _ENV.multishell
local gridColumns = {} local gridColumns = {}
table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' }) table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' }) table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' }) table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' })
if UI.defaultDevice.width > 50 then if UI.term.width > 50 then
table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' }) table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' })
end end
table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' }) table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' })
@ -42,12 +41,13 @@ local page = UI.Page {
configSlide = UI.SlideOut { configSlide = UI.SlideOut {
y = -11, y = -11,
titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close' }, titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close', backgroundColor = colors.black },
accelerators = { ['backspace'] = 'config_close' }, accelerators = { ['backspace'] = 'config_close' },
configTabs = UI.Tabs { configTabs = UI.Tabs {
y = 2, y = 2,
filterTab = UI.Tab { filterTab = UI.Tab {
tabTitle = 'Filter', tabTitle = 'Filter',
noFill = true,
filterGridText = UI.Text { filterGridText = UI.Text {
x = 2, y = 2, x = 2, y = 2,
value = 'ID filter', value = 'ID filter',
@ -130,7 +130,6 @@ local page = UI.Page {
title = 'Packet Information', title = 'Packet Information',
event = 'packet_close', event = 'packet_close',
}, },
backgroundColor = colors.cyan,
accelerators = { accelerators = {
['backspace'] = 'packet_close', ['backspace'] = 'packet_close',
['left'] = 'prev_packet', ['left'] = 'prev_packet',
@ -280,7 +279,7 @@ function page.packetSlide:eventHandler(event)
end end
function page.packetGrid:getDisplayValues(row) function page.packetGrid:getDisplayValues(row)
local row = Util.shallowCopy(row) row = Util.shallowCopy(row)
row.distance = Util.toBytes(Util.round(row.distance), 2) row.distance = Util.toBytes(Util.round(row.distance), 2)
return row return row
end end
@ -356,7 +355,7 @@ function page:eventHandler(event)
self.packetSlide:show(event.selected) self.packetSlide:show(event.selected)
elseif event.type == 'quit' then elseif event.type == 'quit' then
Event.exitPullEvents() UI:quit()
else return UI.Page.eventHandler(self, event) else return UI.Page.eventHandler(self, event)
end end
@ -386,4 +385,4 @@ if args[1] then
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@ -11,7 +11,7 @@ local systemPage = UI.Page {
settings = UI.Tab { settings = UI.Tab {
tabTitle = 'Category', tabTitle = 'Category',
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 2, x = 2, y = 2, ex = -2, ey = -2,
columns = { columns = {
{ heading = 'Name', key = 'name' }, { heading = 'Name', key = 'name' },
{ heading = 'Description', key = 'description' }, { heading = 'Description', key = 'description' },
@ -35,14 +35,14 @@ function systemPage.tabs.settings:eventHandler(event)
tab:disable() tab:disable()
end end
systemPage.tabs:selectTab(tab) systemPage.tabs:selectTab(tab)
self.parent:draw() --self.parent:draw()
return true return true
end end
end end
function systemPage:eventHandler(event) function systemPage:eventHandler(event)
if event.type == 'quit' then if event.type == 'quit' then
UI:exitPullEvents() UI:quit()
elseif event.type == 'success_message' then elseif event.type == 'success_message' then
self.notification:success(event.message) self.notification:success(event.message)
@ -82,4 +82,4 @@ local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
systemPage.tabs.settings.grid:setValues(plugins) systemPage.tabs.settings.grid:setValues(plugins)
UI:setPage(systemPage) UI:setPage(systemPage)
UI:pullEvents() UI:start()

View File

@ -3,6 +3,7 @@ local UI = require('opus.ui')
local kernel = _G.kernel local kernel = _G.kernel
local multishell = _ENV.multishell local multishell = _ENV.multishell
local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines
UI:configure('Tasks', ...) UI:configure('Tasks', ...)
@ -21,7 +22,7 @@ local page = UI.Page {
{ heading = 'Status', key = 'status' }, { heading = 'Status', key = 'status' },
{ heading = 'Time', key = 'timestamp' }, { heading = 'Time', key = 'timestamp' },
}, },
values = kernel.routines, values = tasks,
sortColumn = 'uid', sortColumn = 'uid',
autospace = true, autospace = true,
getDisplayValues = function (_, row) getDisplayValues = function (_, row)
@ -51,7 +52,7 @@ local page = UI.Page {
end end
end end
if event.type == 'quit' then if event.type == 'quit' then
Event.exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@ -64,4 +65,4 @@ Event.onInterval(1, function()
end) end)
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@ -131,7 +131,7 @@ function page:eventHandler(event)
shell.openForegroundTab('PackageManager') shell.openForegroundTab('PackageManager')
elseif event.type == 'wizard_complete' or event.type == 'cancel' then elseif event.type == 'wizard_complete' or event.type == 'cancel' then
UI.exitPullEvents() UI:quit()
else else
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
@ -140,4 +140,4 @@ function page:eventHandler(event)
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

39
sys/apps/fileui.lua Normal file
View File

@ -0,0 +1,39 @@
local UI = require('opus.ui')
local Util = require('opus.util')
local shell = _ENV.shell
local multishell = _ENV.multishell
-- fileui [--path=path] [--exec=filename] [--title=title]
local page = UI.Page {
fileselect = UI.FileSelect { },
eventHandler = function(self, event)
if event.type == 'select_file' then
self.selected = event.file
UI:quit()
elseif event.type == 'select_cancel' then
UI:quit()
end
return UI.FileSelect.eventHandler(self, event)
end,
}
local _, args = Util.parse(...)
if args.title and multishell then
multishell.setTitle(multishell.getCurrent(), args.title)
end
UI:setPage(page, args.path)
UI:start()
UI.term:setCursorBlink(false)
if args.exec and page.selected then
shell.openForegroundTab(string.format('%s %s', args.exec, page.selected))
return
end
return page.selected

View File

@ -133,6 +133,12 @@ page = UI.Page {
}, },
}, },
}, },
accelerators = {
['shift-right'] = 'size',
['shift-left' ] = 'size',
['shift-up' ] = 'size',
['shift-down' ] = 'size',
},
eventHandler = function (self, event) eventHandler = function (self, event)
if event.type == 'focus_change' and isRelevant(event.focused) then if event.type == 'focus_change' and isRelevant(event.focused) then
focused = event.focused focused = event.focused
@ -144,7 +150,6 @@ page = UI.Page {
}) })
end end
self.tabs.properties.grid:setValues(t) self.tabs.properties.grid:setValues(t)
self.tabs.properties.grid:update()
self.tabs.properties.grid:draw() self.tabs.properties.grid:draw()
t = { } t = { }
@ -156,7 +161,6 @@ page = UI.Page {
end end
end end
self.tabs.methodsTab.grid:setValues(t) self.tabs.methodsTab.grid:setValues(t)
self.tabs.methodsTab.grid:update()
self.tabs.methodsTab.grid:draw() self.tabs.methodsTab.grid:draw()
elseif event.type == 'edit_property' then elseif event.type == 'edit_property' then
@ -168,6 +172,19 @@ page = UI.Page {
elseif event.type == 'editor_apply' then elseif event.type == 'editor_apply' then
self.editor:hide() self.editor:hide()
elseif event.type == 'size' then
local sizing = {
['shift-right'] = { 1, 0 },
['shift-left' ] = { -1, 0 },
['shift-up' ] = { 0, -1 },
['shift-down' ] = { 0, 1 },
}
self.ox = math.max(self.ox + sizing[event.ie.code][1], 1)
self.oy = math.max(self.oy + sizing[event.ie.code][2], 1)
UI.term:clear()
self:resize()
self:draw()
end end
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
@ -175,4 +192,4 @@ page = UI.Page {
} }
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@ -33,7 +33,7 @@ Event.on('generate_keypair', function()
table.insert(keyPairs, { generateKeyPair() }) table.insert(keyPairs, { generateKeyPair() })
_G._syslog('Generated keypair in ' .. timer()) _G._syslog('Generated keypair in ' .. timer())
if #keyPairs >= 3 then if #keyPairs >= 3 then
break break
end end
end end
end) end)

View File

@ -66,11 +66,11 @@ local function run(env, ...)
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)')) _ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)'))
end end
if isUrl then tProgramStack[#tProgramStack + 1] = {
tProgramStack[#tProgramStack + 1] = path -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$") path = path, -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$")
else env = env,
tProgramStack[#tProgramStack + 1] = path args = args,
end }
env[ "arg" ] = { [0] = path, table.unpack(args) } env[ "arg" ] = { [0] = path, table.unpack(args) }
local r = { fn(table.unpack(args)) } local r = { fn(table.unpack(args)) }
@ -278,6 +278,10 @@ function shell.getCompletionInfo()
end end
function shell.getRunningProgram() function shell.getRunningProgram()
return tProgramStack[#tProgramStack] and tProgramStack[#tProgramStack].path
end
function shell.getRunningInfo()
return tProgramStack[#tProgramStack] return tProgramStack[#tProgramStack]
end end

View File

@ -20,7 +20,7 @@ local aliasTab = UI.Tab {
}, },
}, },
grid = UI.Grid { grid = UI.Grid {
y = 5, x = 2, y = 5, ex = -2, ey = -2,
sortColumn = 'alias', sortColumn = 'alias',
columns = { columns = {
{ heading = 'Alias', key = 'alias' }, { heading = 'Alias', key = 'alias' },

View File

@ -2,8 +2,6 @@ local Array = require('opus.array')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Preferred', tabTitle = 'Preferred',
description = 'Select preferred applications', description = 'Select preferred applications',
@ -22,20 +20,19 @@ local tab = UI.Tab {
disableHeader = true, disableHeader = true,
columns = { columns = {
{ key = 'file' }, { key = 'file' },
} },
getRowTextColor = function(self, row)
if row == self.values[1] then
return 'yellow'
end
return UI.Grid.getRowTextColor(self, row)
end,
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar {
values = 'Double-click to set as preferred' values = 'Double-click to set as preferred'
}, },
} }
function tab.choices:getRowTextColor(row)
if row == self.values[1] then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
function tab:updateChoices() function tab:updateChoices()
local app = self.apps:getSelected().name local app = self.apps:getSelected().name
local choices = { } local choices = { }

View File

@ -2,18 +2,17 @@ local Ansi = require('opus.ansi')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
-- -t80x30
if _G.http.websocket then if _G.http.websocket then
local config = Config.load('cloud') local config = Config.load('cloud')
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Cloud', tabTitle = 'Cloud',
description = 'Cloud Catcher options', description = 'Cloud Catcher options',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
key = UI.TextEntry { key = UI.TextEntry {
x = 3, ex = -3, y = 2, x = 3, ex = -3, y = 3,
limit = 32, limit = 32,
value = config.key, value = config.key,
shadowText = 'Cloud key', shadowText = 'Cloud key',
@ -22,14 +21,15 @@ if _G.http.websocket then
}, },
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 4, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update_key', event = 'update_key',
}, },
labelText = UI.TextArea { labelText = UI.TextArea {
x = 3, ex = -3, y = 6, x = 2, ex = -2, y = 5, ey = -4,
textColor = colors.yellow, textColor = 'yellow',
marginLeft = 0, marginRight = 0, backgroundColor = 'black',
marginLeft = 1, marginRight = 1, marginTop = 1,
value = string.format( value = string.format(
[[Use a non-changing cloud key. Note that only a single computer can use this session at one time. [[Use a non-changing cloud key. Note that only a single computer can use this session at one time.
To obtain a key, visit: To obtain a key, visit:

View File

@ -20,8 +20,8 @@ local tab = UI.Tab {
description = 'Visualise HDD and disks usage', description = 'Visualise HDD and disks usage',
drives = UI.ScrollingGrid { drives = UI.ScrollingGrid {
x = 2, y = 1, x = 2, y = 2,
ex = '47%', ey = -7, ex = '47%', ey = -8,
columns = { columns = {
{ heading = 'Drive', key = 'name' }, { heading = 'Drive', key = 'name' },
{ heading = 'Side' ,key = 'side', textColor = colors.yellow } { heading = 'Side' ,key = 'side', textColor = colors.yellow }
@ -30,7 +30,7 @@ local tab = UI.Tab {
}, },
infos = UI.Grid { infos = UI.Grid {
x = '52%', y = 2, x = '52%', y = 2,
ex = -2, ey = -4, ex = -2, ey = -8,
disableHeader = true, disableHeader = true,
unfocusedBackgroundSelectedColor = colors.black, unfocusedBackgroundSelectedColor = colors.black,
inactive = true, inactive = true,
@ -40,18 +40,23 @@ local tab = UI.Tab {
{ key = 'value', align = 'right', textColor = colors.yellow }, { key = 'value', align = 'right', textColor = colors.yellow },
} }
}, },
[1] = UI.Window {
x = 2, y = -6, ex = -2, ey = -2,
backgroundColor = colors.black,
},
progress = UI.ProgressBar { progress = UI.ProgressBar {
x = 11, y = -2, x = 11, y = -3,
ex = -2, ex = -3,
}, },
percentage = UI.Text { percentage = UI.Text {
x = 11, y = -3, y = -4, width = 5,
ex = '47%', x = 12,
align = 'center', --align = 'center',
backgroundColor = colors.black,
}, },
icon = UI.NftImage { icon = UI.NftImage {
x = 2, y = -5, x = 2, y = -6, ey = -2,
backgroundColor = colors.black,
image = NFT.parse(NftImages.blank) image = NFT.parse(NftImages.blank)
}, },
} }

View File

@ -4,11 +4,11 @@ local colors = _G.colors
local peripheral = _G.peripheral local peripheral = _G.peripheral
local settings = _G.settings local settings = _G.settings
local tab = UI.Tab { return peripheral.find('monitor') and UI.Tab {
tabTitle = 'Kiosk', tabTitle = 'Kiosk',
description = 'Kiosk options', description = 'Kiosk options',
form = UI.Form { form = UI.Form {
x = 2, ex = -2, x = 2, y = 2, ex = -2, ey = 5,
manualControls = true, manualControls = true,
monitor = UI.Chooser { monitor = UI.Chooser {
formLabel = 'Monitor', formKey = 'monitor', formLabel = 'Monitor', formKey = 'monitor',
@ -22,41 +22,36 @@ local tab = UI.Tab {
}, },
help = 'Adjust text scaling', help = 'Adjust text scaling',
}, },
labelText = UI.TextArea {
x = 2, ex = -2, y = 5,
textColor = colors.yellow,
value = 'Settings apply to kiosk mode selected during startup'
},
}, },
} labelText = UI.TextArea {
x = 2, ex = -2, y = 7, ey = -2,
textColor = colors.yellow,
backgroundColor = colors.black,
value = 'Settings apply to kiosk mode selected during startup'
},
enable = function(self)
local choices = { }
function tab:enable() peripheral.find('monitor', function(side)
local choices = { } table.insert(choices, { name = side, value = side })
end)
peripheral.find('monitor', function(side) self.form.monitor.choices = choices
table.insert(choices, { name = side, value = side }) self.form.monitor.value = settings.get('kiosk.monitor')
end)
self.form.monitor.choices = choices self.form.textScale.value = settings.get('kiosk.textscale')
self.form.monitor.value = settings.get('kiosk.monitor')
self.form.textScale.value = settings.get('kiosk.textscale') UI.Tab.enable(self)
end,
UI.Tab.enable(self) eventHandler = function(self, event)
end if event.type == 'choice_change' then
if self.form.monitor.value then
function tab:eventHandler(event) settings.set('kiosk.monitor', self.form.monitor.value)
if event.type == 'choice_change' then end
if self.form.monitor.value then if self.form.textScale.value then
settings.set('kiosk.monitor', self.form.monitor.value) settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end end
if self.form.textScale.value then
settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end end
end }
if peripheral.find('monitor') then
return tab
end

View File

@ -4,23 +4,26 @@ local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local os = _G.os local os = _G.os
local labelTab = UI.Tab { return UI.Tab {
tabTitle = 'Label', tabTitle = 'Label',
description = 'Set the computer label', description = 'Set the computer label',
labelText = UI.Text { labelText = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Label' value = 'Label'
}, },
label = UI.TextEntry { label = UI.TextEntry {
x = 9, y = 2, ex = -4, x = 9, y = 3, ex = -4,
limit = 32, limit = 32,
value = os.getComputerLabel(), value = os.getComputerLabel(),
accelerators = { accelerators = {
enter = 'update_label', enter = 'update_label',
}, },
}, },
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 3, x = 2, y = 5, ex = -2, ey = -2,
values = { values = {
{ name = '', value = '' }, { name = '', value = '' },
{ name = 'CC version', value = Util.getVersion() }, { name = 'CC version', value = Util.getVersion() },
@ -30,20 +33,18 @@ local labelTab = UI.Tab {
{ name = 'Computer ID', value = tostring(os.getComputerID()) }, { name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) }, { name = 'Day', value = tostring(os.day()) },
}, },
disableHeader = true,
inactive = true, inactive = true,
columns = { columns = {
{ key = 'name', width = 12 }, { key = 'name', width = 12 },
{ key = 'value' }, { key = 'value', textColor = colors.yellow },
}, },
}, },
eventHandler = function(self, event)
if event.type == 'update_label' and self.label.value then
os.setComputerLabel(self.label.value)
self:emit({ type = 'success_message', message = 'Label updated' })
return true
end
end,
} }
function labelTab:eventHandler(event)
if event.type == 'update_label' and self.label.value then
os.setComputerLabel(self.label.value)
self:emit({ type = 'success_message', message = 'Label updated' })
return true
end
end
return labelTab

View File

@ -9,12 +9,15 @@ local config = Config.load('multishell')
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Launcher', tabTitle = 'Launcher',
description = 'Set the application launcher', description = 'Set the application launcher',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 5,
},
launcherLabel = UI.Text { launcherLabel = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Launcher', value = 'Launcher',
}, },
launcher = UI.Chooser { launcher = UI.Chooser {
x = 13, y = 2, width = 12, x = 13, y = 3, width = 12,
choices = { choices = {
{ name = 'Overview', value = 'sys/apps/Overview.lua' }, { name = 'Overview', value = 'sys/apps/Overview.lua' },
{ name = 'Shell', value = 'sys/apps/ShellLauncher.lua' }, { name = 'Shell', value = 'sys/apps/ShellLauncher.lua' },
@ -22,18 +25,20 @@ local tab = UI.Tab {
}, },
}, },
custom = UI.TextEntry { custom = UI.TextEntry {
x = 13, ex = -3, y = 3, x = 13, ex = -3, y = 4,
limit = 128, limit = 128,
shadowText = 'File name', shadowText = 'File name',
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 5, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update', event = 'update',
}, },
labelText = UI.TextArea { labelText = UI.TextArea {
x = 3, ex = -3, y = 7, x = 2, ex = -2, y = 6, ey = -4,
backgroundColor = colors.black,
textColor = colors.yellow, textColor = colors.yellow,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Choose an application launcher', value = 'Choose an application launcher',
}, },
} }

View File

@ -2,59 +2,61 @@ local Ansi = require('opus.ansi')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local device = _G.device local device = _G.device
local tab = UI.Tab { return UI.Tab {
tabTitle = 'Network', tabTitle = 'Network',
description = 'Networking options', description = 'Networking options',
info = UI.TextArea { info = UI.TextArea {
x = 3, y = 4, x = 2, y = 5, ex = -2, ey = -2,
backgroundColor = colors.black,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = string.format( value = string.format(
[[%sSet the primary modem used for wireless communications.%s [[%sSet the primary modem used for wireless communications.%s
Reboot to take effect.]], Ansi.yellow, Ansi.reset) Reboot to take effect.]], Ansi.yellow, Ansi.reset)
}, },
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
label = UI.Text { label = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Modem', value = 'Modem',
}, },
modem = UI.Chooser { modem = UI.Chooser {
x = 10, ex = -3, y = 2, x = 10, ex = -3, y = 3,
nochoice = 'auto', nochoice = 'auto',
}, },
} enable = function(self)
local width = 7
local choices = {
{ name = 'auto', value = 'auto' },
{ name = 'disable', value = 'none' },
}
function tab:enable() for k,v in pairs(device) do
local width = 7 if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
local choices = { table.insert(choices, { name = k, value = v.name })
{ name = 'auto', value = 'auto' }, width = math.max(width, #k)
{ name = 'disable', value = 'none' }, end
} end
for k,v in pairs(device) do self.modem.choices = choices
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then --self.modem.width = width + 4
table.insert(choices, { name = k, value = v.name })
width = math.max(width, #k) local config = Config.load('os')
self.modem.value = config.wirelessModem or 'auto'
UI.Tab.enable(self)
end,
eventHandler = function(self, event)
if event.type == 'choice_change' then
local config = Config.load('os')
config.wirelessModem = self.modem.value
Config.update('os', config)
self:emit({ type = 'success_message', message = 'reboot to take effect' })
return true
end end
end end
}
self.modem.choices = choices
--self.modem.width = width + 4
local config = Config.load('os')
self.modem.value = config.wirelessModem or 'auto'
UI.Tab.enable(self)
end
function tab:eventHandler(event)
if event.type == 'choice_change' then
local config = Config.load('os')
config.wirelessModem = self.modem.value
Config.update('os', config)
self:emit({ type = 'success_message', message = 'reboot to take effect' })
return true
end
end
return tab

View File

@ -2,11 +2,12 @@ local Security = require('opus.security')
local SHA = require('opus.crypto.sha2') local SHA = require('opus.crypto.sha2')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors return UI.Tab {
local passwordTab = UI.Tab {
tabTitle = 'Password', tabTitle = 'Password',
description = 'Wireless network password', description = 'Wireless network password',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
newPass = UI.TextEntry { newPass = UI.TextEntry {
x = 3, ex = -3, y = 3, x = 3, ex = -3, y = 3,
limit = 32, limit = 32,
@ -17,28 +18,28 @@ local passwordTab = UI.Tab {
}, },
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 5, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update_password', event = 'update_password',
}, },
info = UI.TextArea { info = UI.TextArea {
x = 3, ex = -3, y = 7, x = 2, ex = -2, y = 5, ey = -4,
textColor = colors.yellow, backgroundColor = 'black',
textColor = 'yellow',
inactive = true, inactive = true,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Add a password to enable other computers to connect to this one.', value = 'Add a password to enable other computers to connect to this one.',
} },
} eventHandler = function(self, event)
function passwordTab:eventHandler(event) if event.type == 'update_password' then
if event.type == 'update_password' then if not self.newPass.value or #self.newPass.value == 0 then
if not self.newPass.value or #self.newPass.value == 0 then self:emit({ type = 'error_message', message = 'Invalid password' })
self:emit({ type = 'error_message', message = 'Invalid password' })
else else
Security.updatePassword(SHA.compute(self.newPass.value)) Security.updatePassword(SHA.compute(self.newPass.value))
self:emit({ type = 'success_message', message = 'Password updated' }) self:emit({ type = 'success_message', message = 'Password updated' })
end
return true
end end
return true
end end
end }
return passwordTab

View File

@ -6,8 +6,11 @@ local tab = UI.Tab {
tabTitle = 'Path', tabTitle = 'Path',
description = 'Set the shell path', description = 'Set the shell path',
tabClose = true, tabClose = true,
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
entry = UI.TextEntry { entry = UI.TextEntry {
x = 2, y = 2, ex = -2, x = 3, y = 3, ex = -3,
limit = 256, limit = 256,
shadowText = 'enter new path', shadowText = 'enter new path',
accelerators = { accelerators = {
@ -16,7 +19,7 @@ local tab = UI.Tab {
help = 'add a new path', help = 'add a new path',
}, },
grid = UI.Grid { grid = UI.Grid {
y = 4, ey = -3, x = 2, y = 6, ex = -2, ey = -3,
disableHeader = true, disableHeader = true,
columns = { { key = 'value' } }, columns = { { key = 'value' } },
autospace = true, autospace = true,

View File

@ -2,48 +2,94 @@ local UI = require('opus.ui')
local settings = _G.settings local settings = _G.settings
if settings then local transform = {
local settingsTab = UI.Tab { string = tostring,
tabTitle = 'Settings', number = tonumber,
description = 'Computercraft configurable settings', }
grid = UI.Grid {
y = 2,
autospace = true,
sortColumn = 'name',
columns = {
{ heading = 'Setting', key = 'name' },
{ heading = 'Value', key = 'value' },
},
},
}
function settingsTab:enable() return settings and UI.Tab {
tabTitle = 'Settings',
description = 'Computercraft settings',
grid = UI.Grid {
x = 2, y = 2, ex = -2, ey = -2,
sortColumn = 'name',
columns = {
{ heading = 'Setting', key = 'name' },
{ heading = 'Value', key = 'value' },
},
},
editor = UI.SlideOut {
y = -6, height = 6,
titleBar = UI.TitleBar {
event = 'slide_hide',
title = 'Enter value',
},
form = UI.Form {
y = 2,
value = UI.TextEntry {
limit = 256,
formIndex = 1,
formLabel = 'Value',
formKey = 'value',
},
validateField = function(self, entry)
if entry.value then
return transform[self.type](entry.value)
end
return true
end,
},
accelerators = {
form_cancel = 'slide_hide',
},
show = function(self, entry)
self.form.type = type(entry.value) or 'string'
self.form:setValues(entry)
self.titleBar.title = entry.name
UI.SlideOut.show(self)
end,
eventHandler = function(self, event)
if event.type == 'form_complete' then
if not event.values.value then
settings.unset(event.values.name)
self.parent:reload()
else
event.values.value = transform[self.form.type](event.values.value)
settings.set(event.values.name, event.values.value)
end
self.parent.grid:draw()
self:hide()
settings.save('.settings')
end
return UI.SlideOut.eventHandler(self, event)
end,
},
reload = function(self)
local values = { } local values = { }
for _,v in pairs(settings.getNames()) do for _,v in pairs(settings.getNames()) do
local value = settings.get(v)
if not value then
value = false
end
table.insert(values, { table.insert(values, {
name = v, name = v,
value = value, value = settings.get(v) or false,
}) })
end end
self.grid:setValues(values) self.grid:setValues(values)
self.grid:setIndex(1)
end,
enable = function(self)
self:reload()
UI.Tab.enable(self) UI.Tab.enable(self)
end end,
eventHandler = function(self, event)
function settingsTab:eventHandler(event)
if event.type == 'grid_select' then if event.type == 'grid_select' then
if not event.selected.value or type(event.selected.value) == 'boolean' then if type(event.selected.value) == 'boolean' then
event.selected.value = not event.selected.value event.selected.value = not event.selected.value
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
else
self.editor:show(event.selected)
end end
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
return true return true
end end
end end,
}
return settingsTab
end

View File

@ -28,7 +28,7 @@ local defaults = {
local _colors = config.color or Util.shallowCopy(defaults) local _colors = config.color or Util.shallowCopy(defaults)
local allSettings = { } local allSettings = { }
for k, v in pairs(defaults) do for k in pairs(defaults) do
table.insert(allSettings, { name = k }) table.insert(allSettings, { name = k })
end end
@ -38,29 +38,34 @@ if not _colors.backgroundColor then
_colors.fileColor = colors.white _colors.fileColor = colors.white
end end
local tab = UI.Tab { return UI.Tab {
tabTitle = 'Shell', tabTitle = 'Shell',
description = 'Shell options', description = 'Shell options',
grid1 = UI.ScrollingGrid { grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 3, ex = -16, y = 2, ey = -10, x = 2, ex = -17,
disableHeader = true, disableHeader = true,
columns = { { key = 'name' } }, columns = { { key = 'name' } },
values = allSettings, values = allSettings,
sortColumn = 'name', sortColumn = 'name',
}, },
grid2 = UI.ScrollingGrid { grid2 = UI.ScrollingGrid {
y = 2, ey = -10, x = -14, ex = -3, y = 2, ey = -10, x = -14, ex = -2,
disableHeader = true, disableHeader = true,
columns = { { key = 'name' } }, columns = { { key = 'name' } },
values = allColors, values = allColors,
sortColumn = 'name', sortColumn = 'name',
}, getRowTextColor = function(self, row)
directoryLabel = UI.Text { local selected = self.parent.grid1:getSelected()
x = 2, y = -2, if _colors[selected.name] == row.value then
value = 'Display directory', return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
}, },
directory = UI.Checkbox { directory = UI.Checkbox {
x = 20, y = -2, x = 2, y = -2,
labelBackgroundColor = colors.black,
label = 'Directory',
value = config.displayDirectory value = config.displayDirectory
}, },
reset = UI.Button { reset = UI.Button {
@ -74,69 +79,57 @@ local tab = UI.Tab {
event = 'update', event = 'update',
}, },
display = UI.Window { display = UI.Window {
x = 3, ex = -3, y = -8, height = 5, x = 2, ex = -2, y = -8, height = 5,
draw = function(self)
self:clear(_colors.backgroundColor)
local offset = 0
if config.displayDirectory then
self:write(1, 1,
'==' .. os.getComputerLabel() .. ':/dir/etc',
_colors.directoryBackgroundColor, _colors.directoryTextColor)
offset = 1
end
self:write(1, 1 + offset, '$ ',
_colors.promptBackgroundColor, _colors.promptTextColor)
self:write(3, 1 + offset, 'ls /',
_colors.backgroundColor, _colors.commandTextColor)
self:write(1, 2 + offset, 'sys usr',
_colors.backgroundColor, _colors.directoryColor)
self:write(1, 3 + offset, 'startup',
_colors.backgroundColor, _colors.fileColor)
end,
}, },
eventHandler = function(self, event)
if event.type =='checkbox_change' then
config.displayDirectory = not not event.checked
self.display:draw()
elseif event.type == 'grid_focus_row' and event.element == self.grid1 then
self.grid2:draw()
elseif event.type == 'grid_select' and event.element == self.grid2 then
_colors[self.grid1:getSelected().name] = event.selected.value
self.display:draw()
self.grid2:draw()
elseif event.type == 'reset' then
config.color = defaults
config.displayDirectory = true
self.directory.value = true
_colors = Util.shallowCopy(defaults)
Config.update('shellprompt', config)
self:draw()
elseif event.type == 'update' then
config.color = _colors
Config.update('shellprompt', config)
end
return UI.Tab.eventHandler(self, event)
end
} }
function tab.grid2:getRowTextColor(row)
local selected = tab.grid1:getSelected()
if _colors[selected.name] == row.value then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
function tab.display:draw()
self:clear(_colors.backgroundColor)
local offset = 0
if config.displayDirectory then
self:write(1, 1,
'==' .. os.getComputerLabel() .. ':/dir/etc',
_colors.directoryBackgroundColor, _colors.directoryTextColor)
offset = 1
end
self:write(1, 1 + offset, '$ ',
_colors.promptBackgroundColor, _colors.promptTextColor)
self:write(3, 1 + offset, 'ls /',
_colors.backgroundColor, _colors.commandTextColor)
self:write(1, 2 + offset, 'sys usr',
_colors.backgroundColor, _colors.directoryColor)
self:write(1, 3 + offset, 'startup',
_colors.backgroundColor, _colors.fileColor)
end
function tab:eventHandler(event)
if event.type =='checkbox_change' then
config.displayDirectory = not not event.checked
self.display:draw()
elseif event.type == 'grid_focus_row' and event.element == self.grid1 then
self.grid2:draw()
elseif event.type == 'grid_select' and event.element == self.grid2 then
_colors[tab.grid1:getSelected().name] = event.selected.value
self.display:draw()
self.grid2:draw()
elseif event.type == 'reset' then
config.color = defaults
config.displayDirectory = true
self.directory.value = true
_colors = Util.shallowCopy(defaults)
Config.update('shellprompt', config)
self:draw()
elseif event.type == 'update' then
config.color = _colors
Config.update('shellprompt', config)
end
return UI.Tab.eventHandler(self, event)
end
return tab

89
sys/apps/system/theme.lua Normal file
View File

@ -0,0 +1,89 @@
local Config = require('opus.config')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local allColors = { }
for k,v in pairs(colors) do
if type(v) == 'number' then
table.insert(allColors, { name = k, value = v })
end
end
local allSettings = { }
for k,v in pairs(UI.colors) do
allSettings[k] = { name = k, value = v }
end
return UI.Tab {
tabTitle = 'Theme',
description = 'Theme colors',
grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 2, ex = -17,
disableHeader = true,
columns = { { key = 'name' } },
values = allSettings,
sortColumn = 'name',
},
grid2 = UI.ScrollingGrid {
y = 2, ey = -10, x = -14, ex = -2,
disableHeader = true,
columns = { { key = 'name' } },
values = allColors,
sortColumn = 'name',
getRowTextColor = function(self, row)
local selected = self.parent.grid1:getSelected()
if selected.value == row.value then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
},
button = UI.Button {
x = -9, y = -2,
text = 'Update',
event = 'update',
},
display = UI.Window {
x = 2, ex = -2, y = -8, height = 5,
textColor = colors.black,
backgroundColor = colors.black,
draw = function(self)
self:clear()
self:write(1, 1, Util.widthify(' Local Global Device', self.width),
allSettings.secondary.value)
self:write(2, 2, 'enter command ',
colors.black, colors.gray)
self:write(1, 3, ' Formatted ',
allSettings.primary.value)
self:write(12, 3, Util.widthify(' Output ', self.width - 11),
allSettings.tertiary.value)
self:write(1, 4, Util.widthify(' Key', self.width),
allSettings.primary.value)
end,
},
eventHandler = function(self, event)
if event.type == 'grid_focus_row' and event.element == self.grid1 then
self.grid2:draw()
elseif event.type == 'grid_select' and event.element == self.grid2 then
self.grid1:getSelected().value = event.selected.value
self.display:draw()
self.grid2:draw()
elseif event.type == 'update' then
local config = Config.load('ui.theme', { colors = { } })
for k,v in pairs(allSettings) do
config.colors[k] = v.value
end
Config.update('ui.theme', config)
end
return UI.Tab.eventHandler(self, event)
end
}

View File

@ -9,7 +9,7 @@ kernel.hook('clipboard_copy', function(_, args)
keyboard.clipboard = args[1] keyboard.clipboard = args[1]
end) end)
keyboard.addHotkey('shift-paste', function() local function queuePaste()
local data = keyboard.clipboard local data = keyboard.clipboard
if type(data) == 'table' then if type(data) == 'table' then
@ -20,4 +20,7 @@ keyboard.addHotkey('shift-paste', function()
if data then if data then
os.queueEvent('paste', data) os.queueEvent('paste', data)
end end
end) end
kernel.hook('clipboard_paste', queuePaste)
keyboard.addHotkey('shift-paste', queuePaste)

View File

@ -1,10 +1,10 @@
local fs = _G.fs local fs = _G.fs
local function deleteIfExists(path) local function deleteIfExists(path)
if fs.exists(path) then if fs.exists(path) then
fs.delete(path) fs.delete(path)
print("Deleted outdated file at: "..path) print("Deleted outdated file at: "..path)
end end
end end
-- cleanup outdated files -- cleanup outdated files
deleteIfExists('sys/apps/shell') deleteIfExists('sys/apps/shell')
@ -21,4 +21,4 @@ deleteIfExists('sys/autorun/apps.lua')
deleteIfExists('sys/init/6.tl3.lua') deleteIfExists('sys/init/6.tl3.lua')
-- remove this file -- remove this file
deleteIfExists('sys/autorun/upgraded.lua') -- deleteIfExists('sys/autorun/upgraded.lua')

View File

@ -50,7 +50,10 @@
title = "System", title = "System",
category = "System", category = "System",
icon = "\030 \0307\031f| \010\0307\031f---o\030 \031 \010\030 \009 \0307\031f| ", icon = "\030 \0307\031f| \010\0307\031f---o\030 \031 \010\030 \009 \0307\031f| ",
iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133", iconExt = "22€070†b02‹4Ÿ24\
02—7Ž704ˆ4€€€€\
7ƒ07„1ƒ7‹24ƒƒ",
--iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133",
run = "System.lua", run = "System.lua",
}, },
[ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = { [ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = {

View File

@ -6,10 +6,11 @@ Shortcut keys
* l: Lua application * l: Lua application
* f: Files * f: Files
* e: Edit an application (or right-click) * e: Edit an application (or right-click)
* n: Network
* control-n: Add a new application * control-n: Add a new application
* delete: Delete an application * delete: Delete an application
Adding a new application Adding a new application
======================== ========================
The run entry can be either a disk file or a URL. The run entry can be either a disk file or a URL.
Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Magenta is used for transparency.

View File

@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Peripheral = require('opus.peripheral') local Peripheral = require('opus.peripheral')
_G.device = Peripheral.getList() _G.device = Peripheral.getList()

View File

@ -4,11 +4,8 @@ if fs.native then
return return
end end
_G.requireInjector(_ENV)
local Util = require('opus.util') local Util = require('opus.util')
-- TODO: support getDrive for virtual nodes
fs.native = Util.shallowCopy(fs) fs.native = Util.shallowCopy(fs)
local fstypes = { } local fstypes = { }
@ -23,7 +20,6 @@ for k,fn in pairs(fs) do
end end
function nativefs.list(node, dir) function nativefs.list(node, dir)
local files local files
if fs.native.isDir(dir) then if fs.native.isDir(dir) then
files = fs.native.list(dir) files = fs.native.list(dir)
@ -265,7 +261,6 @@ local function getfstype(fstype)
end end
function fs.mount(path, fstype, ...) function fs.mount(path, fstype, ...)
local vfs = getfstype(fstype) local vfs = getfstype(fstype)
if not vfs then if not vfs then
error('Invalid file system type') error('Invalid file system type')

View File

@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Config = require('opus.config') local Config = require('opus.config')
local device = _G.device local device = _G.device

View File

@ -32,3 +32,23 @@ end
help.setPath(table.concat(helpPaths, ':')) help.setPath(table.concat(helpPaths, ':'))
shell.setPath(table.concat(appPaths, ':')) shell.setPath(table.concat(appPaths, ':'))
local function runDir(directory)
local files = fs.list(directory)
table.sort(files)
for _,file in ipairs(files) do
os.sleep(0)
local result, err = shell.run(directory .. '/' .. file)
if not result and err then
_G.printError('\n' .. err)
end
end
end
for _, package in pairs(Packages:installedSorted()) do
local packageDir = 'packages/' .. package.name .. '/init'
if fs.exists(packageDir) and fs.isDir(packageDir) then
runDir(packageDir)
end
end

View File

@ -1,5 +1,4 @@
_G.requireInjector(_ENV) local Blit = require('opus.ui.blit')
local Config = require('opus.config') local Config = require('opus.config')
local trace = require('opus.trace') local trace = require('opus.trace')
local Util = require('opus.util') local Util = require('opus.util')
@ -47,6 +46,7 @@ local config = {
Config.load('multishell', config) Config.load('multishell', config)
local _colors = parentTerm.isColor() and config.color or config.standard local _colors = parentTerm.isColor() and config.color or config.standard
local palette = parentTerm.isColor() and Blit.colorPalette or Blit.grayscalePalette
local function redrawMenu() local function redrawMenu()
if not tabsDirty then if not tabsDirty then
@ -207,17 +207,11 @@ end)
kernel.hook('multishell_redraw', function() kernel.hook('multishell_redraw', function()
tabsDirty = false tabsDirty = false
local function write(x, text, bg, fg) local blit = Blit(w, {
parentTerm.setBackgroundColor(bg) bg = _colors.tabBarBackgroundColor,
parentTerm.setTextColor(fg) fg = _colors.textColor,
parentTerm.setCursorPos(x, 1) palette = palette,
parentTerm.write(text) })
end
local bg = _colors.tabBarBackgroundColor
parentTerm.setBackgroundColor(bg)
parentTerm.setCursorPos(1, 1)
parentTerm.clearLine()
local currentTab = kernel.getFocused() local currentTab = kernel.getFocused()
@ -254,21 +248,26 @@ kernel.hook('multishell_redraw', function()
tabX = tabX + tab.width tabX = tabX + tab.width
if tab ~= currentTab then if tab ~= currentTab then
local textColor = tab.isDead and _colors.errorColor or _colors.textColor local textColor = tab.isDead and _colors.errorColor or _colors.textColor
write(tab.sx, tab.title:sub(1, tab.width - 1), blit:write(tab.sx, tab.title:sub(1, tab.width - 1),
_colors.backgroundColor, textColor) _colors.backgroundColor, textColor)
end end
end end
end end
if currentTab then if currentTab then
write(currentTab.sx - 1, if currentTab.sx then
' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ', blit:write(currentTab.sx - 1,
_colors.focusBackgroundColor, _colors.focusTextColor) ' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ',
_colors.focusBackgroundColor, _colors.focusTextColor)
end
if not currentTab.noTerminate then if not currentTab.noTerminate then
write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor) blit:write(w, closeInd, nil, _colors.focusTextColor)
end end
end end
parentTerm.setCursorPos(1, 1)
parentTerm.blit(blit.text, blit.fg, blit.bg)
if currentTab and currentTab.window then if currentTab and currentTab.window then
currentTab.window.restoreCursor() currentTab.window.restoreCursor()
end end
@ -334,16 +333,12 @@ kernel.hook('mouse_scroll', function(_, eventData)
end) end)
kernel.hook('kernel_ready', function() kernel.hook('kernel_ready', function()
local env = Util.shallowCopy(shell.getEnv())
_G.requireInjector(env)
overviewId = multishell.openTab({ overviewId = multishell.openTab({
path = config.launcher or 'sys/apps/Overview.lua', path = config.launcher or 'sys/apps/Overview.lua',
isOverview = true, isOverview = true,
noTerminate = true, noTerminate = true,
focused = true, focused = true,
title = '+', title = '+',
env = env,
}) })
multishell.openTab({ multishell.openTab({

View File

@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Array = require('opus.array') local Array = require('opus.array')
local Terminal = require('opus.terminal') local Terminal = require('opus.terminal')
local Util = require('opus.util') local Util = require('opus.util')

View File

@ -1,3 +1,5 @@
local Util = require('opus.util')
local Array = { } local Array = { }
function Array.filter(it, f) function Array.filter(it, f)
@ -14,9 +16,11 @@ function Array.removeByValue(t, e)
for k,v in pairs(t) do for k,v in pairs(t) do
if v == e then if v == e then
table.remove(t, k) table.remove(t, k)
break return e
end end
end end
end end
Array.find = Util.find
return Array return Array

View File

@ -1,7 +1,6 @@
local Util = require('opus.util') local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local shell = _ENV.shell
local Config = { } local Config = { }
@ -25,23 +24,6 @@ function Config.load(fname, data)
return data return data
end end
function Config.loadWithCheck(fname, data)
local filename = 'usr/config/' .. fname
if not fs.exists(filename) then
Config.load(fname, data)
print()
print('The configuration file has been created.')
print('The file name is: ' .. filename)
print()
_G.printError('Press enter to configure')
_G.read()
shell.run('edit ' .. filename)
end
return Config.load(fname, data)
end
function Config.update(fname, data) function Config.update(fname, data)
local filename = 'usr/config/' .. fname local filename = 'usr/config/' .. fname
Util.writeTable(filename, data) Util.writeTable(filename, data)

View File

@ -41,9 +41,9 @@ end
function Entry:updateScroll() function Entry:updateScroll()
local ps = self.scroll local ps = self.scroll
local value = _val(self.value) local len = #_val(self.value)
if self.pos > #value then if self.pos > len then
self.pos = #value self.pos = len
self.scroll = 0 -- ?? self.scroll = 0 -- ??
end end
if self.pos - self.scroll > self.width then if self.pos - self.scroll > self.width then
@ -51,6 +51,11 @@ function Entry:updateScroll()
elseif self.pos < self.scroll then elseif self.pos < self.scroll then
self.scroll = self.pos self.scroll = self.pos
end end
if self.scroll > 0 then
if self.scroll + self.width > len then
self.scroll = len - self.width
end
end
if ps ~= self.scroll then if ps ~= self.scroll then
self.textChanged = true self.textChanged = true
end end
@ -217,6 +222,10 @@ function Entry:paste(ie)
end end
end end
function Entry:forcePaste()
os.queueEvent('clipboard_paste')
end
function Entry:clearLine() function Entry:clearLine()
if #_val(self.value) > 0 then if #_val(self.value) > 0 then
self:reset() self:reset()
@ -363,9 +372,10 @@ local mappings = {
--[ 'control-d' ] = Entry.cutNextWord, --[ 'control-d' ] = Entry.cutNextWord,
[ 'control-x' ] = Entry.cut, [ 'control-x' ] = Entry.cut,
[ 'paste' ] = Entry.paste, [ 'paste' ] = Entry.paste,
-- [ 'control-y' ] = Entry.paste, -- well this won't work... [ 'control-y' ] = Entry.forcePaste, -- well this won't work...
[ 'mouse_doubleclick' ] = Entry.markWord, [ 'mouse_doubleclick' ] = Entry.markWord,
[ 'mouse_tripleclick' ] = Entry.markAll,
[ 'shift-left' ] = Entry.markLeft, [ 'shift-left' ] = Entry.markLeft,
[ 'shift-right' ] = Entry.markRight, [ 'shift-right' ] = Entry.markRight,
[ 'mouse_down' ] = Entry.markAnchor, [ 'mouse_down' ] = Entry.markAnchor,

View File

@ -0,0 +1,21 @@
-- Based on Squid's fuzzy search
-- https://github.com/SquidDev-CC/artist/blob/vnext/artist/lib/match.lua
--
-- not very fuzzy anymore
local SCORE_WEIGHT = 1000
local LEADING_LETTER_PENALTY = -30
local LEADING_LETTER_PENALTY_MAX = -90
local _find = string.find
local _max = math.max
return function(str, pattern)
local start = _find(str, pattern, 1, true)
if start then
-- All letters before the current one are considered leading, so add them to our penalty
return SCORE_WEIGHT
+ _max(LEADING_LETTER_PENALTY * (start - 1), LEADING_LETTER_PENALTY_MAX)
- (#str - #pattern)
end
end

View File

@ -24,9 +24,9 @@ function git.list(repository)
local function getContents() local function getContents()
local dataUrl = string.format(TREE_URL, user, repo, branch) local dataUrl = string.format(TREE_URL, user, repo, branch)
local contents, msg = Util.httpGet(dataUrl,TREE_HEADERS) local contents, msg = Util.httpGet(dataUrl, TREE_HEADERS)
if not contents then if not contents then
error(_sformat('Failed to download %s\n%s', dataUrl, msg), 2) error(string.format('Failed to download %s\n%s', dataUrl, msg), 2)
else else
return json.decode(contents) return json.decode(contents)
end end

View File

@ -65,7 +65,7 @@ function GPS.locate(timeout, debug)
if debug then if debug then
print("Position is "..pos.x..","..pos.y..","..pos.z) print("Position is "..pos.x..","..pos.y..","..pos.z)
end end
return vector.new(pos.x, pos.y, pos.z) return pos and vector.new(pos.x, pos.y, pos.z)
end end
function GPS.isAvailable() function GPS.isAvailable()

View File

@ -50,18 +50,20 @@ function input:toCode(ch, code)
table.insert(result, 'alt') table.insert(result, 'alt')
end end
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or if ch then -- some weird things happen with control/command on mac
code == keys.leftShift or code == keys.rightShift then if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or
if code and modifiers[code] then code == keys.leftShift or code == keys.rightShift then
table.insert(result, 'shift') if code and modifiers[code] then
elseif #ch == 1 then table.insert(result, 'shift')
table.insert(result, ch:upper()) elseif #ch == 1 then
else table.insert(result, ch:upper())
table.insert(result, 'shift') else
table.insert(result, 'shift')
table.insert(result, ch)
end
elseif not code or not modifiers[code] then
table.insert(result, ch) table.insert(result, ch)
end end
elseif not code or not modifiers[code] then
table.insert(result, ch)
end end
return table.concat(result, '-') return table.concat(result, '-')
@ -118,6 +120,7 @@ function input:translate(event, code, p1, p2)
local buttons = { 'mouse_click', 'mouse_rightclick' } local buttons = { 'mouse_click', 'mouse_rightclick' }
self.mch = buttons[code] self.mch = buttons[code]
self.mfired = nil self.mfired = nil
self.anchor = { x = p1, y = p2 }
return { return {
code = input:toCode('mouse_down', 255), code = input:toCode('mouse_down', 255),
button = code, button = code,
@ -132,6 +135,8 @@ function input:translate(event, code, p1, p2)
button = code, button = code,
x = p1, x = p1,
y = p2, y = p2,
dx = p1 - self.anchor.x,
dy = p2 - self.anchor.y,
} }
elseif event == 'mouse_up' then elseif event == 'mouse_up' then
@ -141,18 +146,26 @@ function input:translate(event, code, p1, p2)
p1 == self.x and p2 == self.y and p1 == self.x and p2 == self.y and
(clock - self.timer < .5) then (clock - self.timer < .5) then
self.mch = 'mouse_doubleclick' self.clickCount = self.clickCount + 1
self.timer = nil if self.clickCount == 3 then
self.mch = 'mouse_tripleclick'
self.timer = nil
self.clickCount = 1
else
self.mch = 'mouse_doubleclick'
end
else else
self.timer = os.clock() self.timer = os.clock()
self.x = p1 self.x = p1
self.y = p2 self.y = p2
self.clickCount = 1
end end
self.mfired = input:toCode(self.mch, 255) self.mfired = input:toCode(self.mch, 255)
else else
self.mch = 'mouse_up' self.mch = 'mouse_up'
self.mfired = input:toCode(self.mch, 255) self.mfired = input:toCode(self.mch, 255)
end end
return { return {
code = self.mfired, code = self.mfired,
button = code, button = code,

View File

@ -1,16 +1,19 @@
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local NFT = { } local NFT = { }
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/ -- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
local tColourLookup = { } local hexToColor = { }
for n = 1, 16 do for n = 1, 16 do
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) hexToColor[string.sub("0123456789abcdef", n, n)] = 2 ^ (n - 1)
end end
local colorToHex = Util.transpose(hexToColor)
local function getColourOf(hex) local function getColourOf(hex)
return tColourLookup[hex:byte()] return hexToColor[hex]
end end
function NFT.parse(imageText) function NFT.parse(imageText)
@ -62,8 +65,22 @@ function NFT.parse(imageText)
return image return image
end end
function NFT.load(path) function NFT.transparency(image)
for y = 1, image.height do
for _,key in pairs(Util.keys(image.fg[y])) do
if image.fg[y][key] == colors.magenta then
image.fg[y][key] = nil
end
end
for _,key in pairs(Util.keys(image.bg[y])) do
if image.bg[y][key] == colors.magenta then
image.bg[y][key] = nil
end
end
end
end
function NFT.load(path)
local imageText = Util.readFile(path) local imageText = Util.readFile(path)
if not imageText then if not imageText then
error('Unable to read image file') error('Unable to read image file')
@ -71,4 +88,35 @@ function NFT.load(path)
return NFT.parse(imageText) return NFT.parse(imageText)
end end
function NFT.save(image, filename)
local bgcode, txcode = '\30', '\31'
local output = { }
for y = 1, image.height do
local lastBG, lastFG
if image.text[y] then
for x = 1, #image.text[y] do
local bg = image.bg[y][x] or colors.magenta
if bg ~= lastBG then
lastBG = bg
table.insert(output, bgcode .. colorToHex[bg])
end
local fg = image.fg[y][x] or colors.magenta
if fg ~= lastFG then
lastFG = fg
table.insert(output, txcode .. colorToHex[fg])
end
table.insert(output, image.text[y][x])
end
end
if y < image.height then
table.insert(output, '\n')
end
end
Util.writeFile(filename, table.concat(output))
end
return NFT return NFT

View File

@ -36,61 +36,66 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
local maxScroll = 100 local maxScroll = 100
local cx, cy = 1, 1 local cx, cy = 1, 1
local blink = false local blink = false
local bg, fg = parent.getBackgroundColor(), parent.getTextColor() local _bg, _fg = parent.getBackgroundColor(), parent.getTextColor()
local canvas = Canvas({ win.canvas = Canvas({
x = sx, x = sx,
y = sy, y = sy,
width = w, width = w,
height = h, height = h,
isColor = parent.isColor(), isColor = parent.isColor(),
offy = 0, offy = 0,
bg = _bg,
fg = _fg,
}) })
win.canvas = canvas
local function update() local function update()
if isVisible then if isVisible then
canvas:render(parent) win.canvas:render(parent)
win.setCursorPos(cx, cy) win.setCursorPos(cx, cy)
end end
end end
local function scrollTo(y) local function scrollTo(y)
y = math.max(0, y) y = math.max(0, y)
y = math.min(#canvas.lines - canvas.height, y) y = math.min(#win.canvas.lines - win.canvas.height, y)
if y ~= canvas.offy then if y ~= win.canvas.offy then
canvas.offy = y win.canvas.offy = y
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
function win.write(str) function win.write(str)
str = tostring(str) or '' str = tostring(str) or ''
canvas:write(cx, cy + canvas.offy, str, bg, fg) win.canvas:write(cx, cy + win.canvas.offy, str, win.canvas.bg, win.canvas.fg)
win.setCursorPos(cx + #str, cy) win.setCursorPos(cx + #str, cy)
update() update()
end end
function win.blit(str, fg, bg) function win.blit(str, fg, bg)
canvas:blit(cx, cy + canvas.offy, str, bg, fg) win.canvas:blit(cx, cy + win.canvas.offy, str, bg, fg)
win.setCursorPos(cx + #str, cy) win.setCursorPos(cx + #str, cy)
update() update()
end end
function win.clear() function win.clear()
canvas.offy = 0 win.canvas.offy = 0
for i = #canvas.lines, canvas.height + 1, -1 do for i = #win.canvas.lines, win.canvas.height + 1, -1 do
canvas.lines[i] = nil win.canvas.lines[i] = nil
end end
canvas:clear(bg, fg) win.canvas:clear()
update() update()
end end
function win.getLine(n)
local line = win.canvas.lines[n]
return line.text, line.fg, line.bg
end
function win.clearLine() function win.clearLine()
canvas:clearLine(cy + canvas.offy, bg, fg) win.canvas:clearLine(cy + win.canvas.offy)
win.setCursorPos(cx, cy) win.setCursorPos(cx, cy)
update() update()
end end
@ -102,10 +107,14 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.setCursorPos(x, y) function win.setCursorPos(x, y)
cx, cy = math.floor(x), math.floor(y) cx, cy = math.floor(x), math.floor(y)
if isVisible then if isVisible then
parent.setCursorPos(cx + canvas.x - 1, cy + canvas.y - 1) parent.setCursorPos(cx + win.canvas.x - 1, cy + win.canvas.y - 1)
end end
end end
function win.getCursorBlink()
return blink
end
function win.setCursorBlink(b) function win.setCursorBlink(b)
blink = b blink = b
if isVisible then if isVisible then
@ -114,12 +123,12 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end end
function win.isColor() function win.isColor()
return canvas.isColor return win.canvas.isColor
end end
win.isColour = win.isColor win.isColour = win.isColor
function win.setTextColor(c) function win.setTextColor(c)
fg = c win.canvas.fg = c
end end
win.setTextColour = win.setTextColor win.setTextColour = win.setTextColor
@ -139,38 +148,38 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
win.setPaletteColour = win.setPaletteColor win.setPaletteColour = win.setPaletteColor
function win.setBackgroundColor(c) function win.setBackgroundColor(c)
bg = c win.canvas.bg = c
end end
win.setBackgroundColour = win.setBackgroundColor win.setBackgroundColour = win.setBackgroundColor
function win.getSize() function win.getSize()
return canvas.width, canvas.height return win.canvas.width, win.canvas.height
end end
function win.scroll(n) function win.scroll(n)
n = n or 1 n = n or 1
if n > 0 then if n > 0 then
local lines = #canvas.lines local lines = #win.canvas.lines
for i = 1, n do for i = 1, n do
canvas.lines[lines + i] = { } win.canvas.lines[lines + i] = { }
canvas:clearLine(lines + i, bg, fg) win.canvas:clearLine(lines + i)
end end
while #canvas.lines > maxScroll do while #win.canvas.lines > maxScroll do
table.remove(canvas.lines, 1) table.remove(win.canvas.lines, 1)
end end
scrollTo(#canvas.lines) scrollTo(#win.canvas.lines)
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
function win.getTextColor() function win.getTextColor()
return fg return win.canvas.fg
end end
win.getTextColour = win.getTextColor win.getTextColour = win.getTextColor
function win.getBackgroundColor() function win.getBackgroundColor()
return bg return win.canvas.bg
end end
win.getBackgroundColour = win.getBackgroundColor win.getBackgroundColour = win.getBackgroundColor
@ -178,7 +187,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
if visible ~= isVisible then if visible ~= isVisible then
isVisible = visible isVisible = visible
if isVisible then if isVisible then
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
@ -186,7 +195,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.redraw() function win.redraw()
if isVisible then if isVisible then
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
@ -194,27 +203,27 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.restoreCursor() function win.restoreCursor()
if isVisible then if isVisible then
win.setCursorPos(cx, cy) win.setCursorPos(cx, cy)
win.setTextColor(fg) win.setTextColor(win.canvas.fg)
win.setCursorBlink(blink) win.setCursorBlink(blink)
end end
end end
function win.getPosition() function win.getPosition()
return canvas.x, canvas.y return win.canvas.x, win.canvas.y
end end
function win.reposition(x, y, width, height) function win.reposition(x, y, width, height)
canvas.x, canvas.y = x, y win.canvas.x, win.canvas.y = x, y
canvas:resize(width or canvas.width, height or canvas.height) win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
end end
--[[ Additional methods ]]-- --[[ Additional methods ]]--
function win.scrollDown() function win.scrollDown()
scrollTo(canvas.offy + 1) scrollTo(win.canvas.offy + 1)
end end
function win.scrollUp() function win.scrollUp()
scrollTo(canvas.offy - 1) scrollTo(win.canvas.offy - 1)
end end
function win.scrollTop() function win.scrollTop()
@ -222,7 +231,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end end
function win.scrollBottom() function win.scrollBottom()
scrollTo(#canvas.lines) scrollTo(#win.canvas.lines)
end end
function win.setMaxScroll(ms) function win.setMaxScroll(ms)
@ -230,37 +239,35 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end end
function win.getCanvas() function win.getCanvas()
return canvas return win.canvas
end end
function win.getParent() function win.getParent()
return parent return parent
end end
canvas:clear() win.canvas:clear()
return win return win
end end
-- get windows contents -- get windows contents
function Terminal.getContents(win, parent) function Terminal.getContents(win)
local oblit, oscp = parent.blit, parent.setCursorPos if not win.getLine then
local lines = { } error('window is required')
end
parent.blit = function(text, fg, bg) local lines = { }
lines[#lines + 1] = { local _, h = win.getSize()
for i = 1, h do
local text, fg, bg = win.getLine(i)
lines[i] = {
text = text, text = text,
fg = fg, fg = fg,
bg = bg, bg = bg,
} }
end end
parent.setCursorPos = function() end
win.setVisible(true)
win.redraw()
parent.blit = oblit
parent.setCursorPos = oscp
return lines return lines
end end

View File

@ -1,3 +1,6 @@
local Array = require('opus.array')
local Blit = require('opus.ui.blit')
local Canvas = require('opus.ui.canvas')
local class = require('opus.class') local class = require('opus.class')
local Event = require('opus.event') local Event = require('opus.event')
local Input = require('opus.input') local Input = require('opus.input')
@ -5,7 +8,6 @@ local Transition = require('opus.ui.transition')
local Util = require('opus.util') local Util = require('opus.util')
local _rep = string.rep local _rep = string.rep
local _sub = string.sub
local colors = _G.colors local colors = _G.colors
local device = _G.device local device = _G.device
local fs = _G.fs local fs = _G.fs
@ -32,11 +34,16 @@ local textutils = _G.textutils
]] ]]
--[[-- Top Level Manager --]]-- --[[-- Top Level Manager --]]--
local Manager = class() local UI = { }
function Manager:init() function UI:init()
self.devices = { } self.devices = { }
self.theme = { } self.theme = { }
self.extChars = Util.getVersion() >= 1.76 self.extChars = Util.getVersion() >= 1.76
self.colors = {
primary = colors.green,
secondary = colors.lightGray,
tertiary = colors.gray,
}
local function keyFunction(event, code, held) local function keyFunction(event, code, held)
local ie = Input:translate(event, code, held) local ie = Input:translate(event, code, held)
@ -44,8 +51,7 @@ function Manager:init()
local currentPage = self:getActivePage() local currentPage = self:getActivePage()
if ie and currentPage then if ie and currentPage then
local target = currentPage.focused or currentPage local target = currentPage.focused or currentPage
self:inputEvent(target, target:emit({ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie })
{ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie })
currentPage:sync() currentPage:sync()
end end
end end
@ -53,10 +59,7 @@ function Manager:init()
local function resize(_, side) local function resize(_, side)
local dev = self.devices[side or 'terminal'] local dev = self.devices[side or 'terminal']
if dev and dev.currentPage then if dev and dev.currentPage then
-- the parent doesn't have any children set... dev:resize()
-- that's why we have to resize both the parent and the current page
-- kinda makes sense
dev.currentPage.parent:resize()
dev.currentPage:resize() dev.currentPage:resize()
dev.currentPage:draw() dev.currentPage:draw()
@ -72,17 +75,14 @@ function Manager:init()
monitor_resize = resize, monitor_resize = resize,
mouse_scroll = function(_, direction, x, y) mouse_scroll = function(_, direction, x, y)
local ie = Input:translate('mouse_scroll', direction, x, y)
local currentPage = self:getActivePage() local currentPage = self:getActivePage()
if currentPage then if currentPage then
local event = currentPage:pointToChild(x, y) local event = currentPage:pointToChild(x, y)
local directions = { event.type = ie.code
[ -1 ] = 'up', event.ie = { code = ie.code, x = event.x, y = event.y }
[ 1 ] = 'down' event.element:emit(event)
}
-- 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] })
currentPage:sync() currentPage:sync()
end end
end, end,
@ -92,7 +92,7 @@ function Manager:init()
if dev and dev.currentPage then if dev and dev.currentPage then
Input:translate('mouse_click', 1, x, y) Input:translate('mouse_click', 1, x, y)
local ie = Input:translate('mouse_up', 1, x, y) local ie = Input:translate('mouse_up', 1, x, y)
self:click(dev.currentPage, ie.code, 1, x, y) self:click(dev.currentPage, ie)
end end
end, end,
@ -107,7 +107,7 @@ function Manager:init()
currentPage:setFocus(event.element) currentPage:setFocus(event.element)
currentPage:sync() currentPage:sync()
end end
self:click(currentPage, ie.code, button, x, y) self:click(currentPage, ie)
end end
end end
end, end,
@ -125,7 +125,7 @@ function Manager:init()
elseif ie and currentPage then elseif ie and currentPage then
if not currentPage.parent.device.side then if not currentPage.parent.device.side then
self:click(currentPage, ie.code, button, x, y) self:click(currentPage, ie)
end end
end end
end, end,
@ -135,7 +135,7 @@ function Manager:init()
local currentPage = self:getActivePage() local currentPage = self:getActivePage()
if ie and currentPage then if ie and currentPage then
self:click(currentPage, ie.code, button, x, y) self:click(currentPage, ie)
end end
end, end,
@ -156,7 +156,7 @@ function Manager:init()
end) end)
end end
function Manager:configure(appName, ...) function UI:configure(appName, ...)
local defaults = Util.loadTable('usr/config/' .. appName) or { } local defaults = Util.loadTable('usr/config/' .. appName) or { }
if not defaults.device then if not defaults.device then
defaults.device = { } defaults.device = { }
@ -190,19 +190,15 @@ function Manager:configure(appName, ...)
end end
if defaults.theme then if defaults.theme then
for k,v in pairs(defaults.theme) do Util.deepMerge(self.theme, defaults.theme)
if self[k] and self[k].defaults then
Util.merge(self[k].defaults, v)
end
end
end end
end end
function Manager:disableEffects() function UI:disableEffects()
self.defaultDevice.effectsEnabled = false self.term.effectsEnabled = false
end end
function Manager:loadTheme(filename) function UI:loadTheme(filename)
if fs.exists(filename) then if fs.exists(filename) then
local theme, err = Util.loadTable(filename) local theme, err = Util.loadTable(filename)
if not theme then if not theme then
@ -212,8 +208,20 @@ function Manager:loadTheme(filename)
end end
end end
function Manager:generateTheme(filename) function UI:generateTheme(filename)
local t = { } 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 for k,v in pairs(self) do
if type(v) == 'table' then if type(v) == 'table' then
if v._preload then if v._preload then
@ -226,72 +234,96 @@ function Manager:generateTheme(filename)
if not t[k] then if not t[k] then
t[k] = { } t[k] = { }
end end
for c, n in pairs(colors) do t[k][p] = getName(d)
if n == d then
t[k][p] = 'colors.' .. c
break
end
end
end end
end end
end 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('(")', '')) Util.writeFile(filename, textutils.serialize(t):gsub('(")', ''))
end end
function Manager:emitEvent(event) function UI:emitEvent(event)
local currentPage = self:getActivePage() local currentPage = self:getActivePage()
if currentPage and currentPage.focused then if currentPage and currentPage.focused then
return currentPage.focused:emit(event) return currentPage.focused:emit(event)
end end
end end
function Manager:inputEvent(parent, event) -- deprecate ? function UI:click(target, ie)
return parent and parent:emit(event) local clickEvent
end
function Manager:click(target, code, button, x, y) if ie.code == 'mouse_drag' then
local clickEvent = target:pointToChild(x, y) 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
if code == 'mouse_doubleclick' then local x, y = getPosition(self.lastClicked, ie.x, ie.y)
if self.doubleClickElement ~= clickEvent.element then
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
return return
end end
else else
self.doubleClickElement = clickEvent.element self.lastClicked = clickEvent.element
end end
clickEvent.button = button clickEvent.button = ie.button
clickEvent.type = code clickEvent.type = ie.code
clickEvent.key = code clickEvent.key = ie.code
clickEvent.ie = { code = code, x = clickEvent.x, y = clickEvent.y } clickEvent.ie = { code = ie.code, x = clickEvent.x, y = clickEvent.y }
clickEvent.raw = ie
if clickEvent.element.focus then if clickEvent.element.focus then
target:setFocus(clickEvent.element) target:setFocus(clickEvent.element)
end end
self:inputEvent(clickEvent.element, clickEvent) clickEvent.element:emit(clickEvent)
target:sync() target:sync()
end end
function Manager:setDefaultDevice(dev) function UI:setDefaultDevice(dev)
self.defaultDevice = dev
self.term = dev self.term = dev
end end
function Manager:addPage(name, page) function UI:addPage(name, page)
if not self.pages then if not self.pages then
self.pages = { } self.pages = { }
end end
self.pages[name] = page self.pages[name] = page
end end
function Manager:setPages(pages) function UI:setPages(pages)
self.pages = pages self.pages = pages
end end
function Manager:getPage(pageName) function UI:getPage(pageName)
local page = self.pages[pageName] local page = self.pages[pageName]
if not page then if not page then
@ -301,19 +333,18 @@ function Manager:getPage(pageName)
return page return page
end end
function Manager:getActivePage(page) function UI:getActivePage(page)
if page then if page then
return page.parent.currentPage return page.parent.currentPage
end end
return self.defaultDevice.currentPage return self.term.currentPage
end end
function Manager:setActivePage(page) function UI:setActivePage(page)
page.parent.currentPage = page page.parent.currentPage = page
page.parent.canvas = page.canvas
end end
function Manager:setPage(pageOrName, ...) function UI:setPage(pageOrName, ...)
local page = pageOrName local page = pageOrName
if type(pageOrName) == 'string' then if type(pageOrName) == 'string' then
@ -333,7 +364,6 @@ function Manager:setPage(pageOrName, ...)
page.previousPage = currentPage page.previousPage = currentPage
end end
self:setActivePage(page) self:setActivePage(page)
--page:clear(page.backgroundColor)
page:enable(...) page:enable(...)
page:draw() page:draw()
if page.focused then if page.focused then
@ -344,27 +374,27 @@ function Manager:setPage(pageOrName, ...)
end end
end end
function Manager:getCurrentPage() function UI:getCurrentPage()
return self.defaultDevice.currentPage return self.term.currentPage
end end
function Manager:setPreviousPage() function UI:setPreviousPage()
if self.defaultDevice.currentPage.previousPage then if self.term.currentPage.previousPage then
local previousPage = self.defaultDevice.currentPage.previousPage.previousPage local previousPage = self.term.currentPage.previousPage.previousPage
self:setPage(self.defaultDevice.currentPage.previousPage) self:setPage(self.term.currentPage.previousPage)
self.defaultDevice.currentPage.previousPage = previousPage self.term.currentPage.previousPage = previousPage
end end
end end
function Manager:getDefaults(element, args) function UI:getDefaults(element, args)
local defaults = Util.deepCopy(element.defaults) local defaults = Util.deepCopy(element.defaults)
if args then if args then
Manager:mergeProperties(defaults, args) UI:mergeProperties(defaults, args)
end end
return defaults return defaults
end end
function Manager:mergeProperties(obj, args) function UI:mergeProperties(obj, args)
if args then if args then
for k,v in pairs(args) do for k,v in pairs(args) do
if k == 'accelerators' then if k == 'accelerators' then
@ -380,7 +410,7 @@ function Manager:mergeProperties(obj, args)
end end
end end
function Manager:pullEvents(...) function UI:pullEvents(...)
local s, m = pcall(Event.pullEvents, ...) local s, m = pcall(Event.pullEvents, ...)
self.term:reset() self.term:reset()
if not s and m then if not s and m then
@ -388,21 +418,20 @@ function Manager:pullEvents(...)
end end
end end
Manager.exitPullEvents = Event.exitPullEvents UI.exitPullEvents = Event.exitPullEvents
Manager.quit = Event.exitPullEvents UI.quit = Event.exitPullEvents
Manager.start = Manager.pullEvents UI.start = UI.pullEvents
local UI = Manager() UI:init()
--[[-- Basic drawable area --]]-- --[[-- Basic drawable area --]]--
UI.Window = class() UI.Window = class(Canvas)
UI.Window.uid = 1 UI.Window.uid = 1
UI.Window.docs = { } UI.Window.docs = { }
UI.Window.defaults = { UI.Window.defaults = {
UIElement = 'Window', UIElement = 'Window',
x = 1, x = 1,
y = 1, y = 1,
-- z = 0, -- eventually...
offx = 0, offx = 0,
offy = 0, offy = 0,
cursorX = 1, cursorX = 1,
@ -413,7 +442,9 @@ function UI.Window:init(args)
local defaults = args local defaults = args
local m = getmetatable(self) -- get the class for this instance local m = getmetatable(self) -- get the class for this instance
repeat repeat
defaults = UI:getDefaults(m, defaults) if m ~= Canvas then
defaults = UI:getDefaults(m, defaults)
end
m = m._base m = m._base
until not m until not m
UI:mergeProperties(self, defaults) UI:mergeProperties(self, defaults)
@ -438,6 +469,9 @@ function UI.Window:init(args)
until not m until not m
end 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]]
function UI.Window:postInit() function UI.Window:postInit()
if self.parent then if self.parent then
-- this will cascade down the whole tree of elements starting at the -- this will cascade down the whole tree of elements starting at the
@ -531,47 +565,60 @@ function UI.Window:layout()
if not self.height then if not self.height then
self.height = self.parent.height - self.y + 1 self.height = self.parent.height - self.y + 1
end 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)
end end
-- Called when the window's parent has be assigned -- Called when the window's parent has be assigned
function UI.Window:setParent() function UI.Window:setParent()
self.oh, self.ow = self.height, self.width self.oh, self.ow = self.height, self.width
self.ox, self.oy = self.x, self.y self.ox, self.oy = self.x, self.y
self.oex, self.oey = self.ex, self.ey
self:layout() self:layout()
-- Experimental
-- Inherit properties from the parent container
-- does this need to be in reverse order ?
local m = getmetatable(self) -- get the class for this instance
repeat
if m.inherits then
for k, v in pairs(m.inherits) do
local value = self.parent:getProperty(v)
if value then
self[k] = value
end
end
end
m = m._base
until not m
self:initChildren() self:initChildren()
end end
function UI.Window:resize() function UI.Window:resize()
self.height, self.width = self.oh, self.ow self.height, self.width = self.oh, self.ow
self.x, self.y = self.ox, self.oy self.x, self.y = self.ox, self.oy
self.ex, self.ey = self.oex, self.oey
self:layout() self:layout()
if self.children then if self.children then
for _,child in ipairs(self.children) do for child in self:eachChild() do
child:resize() child:resize()
end end
end end
end 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
UI.Window.docs.add = [[add(TABLE) UI.Window.docs.add = [[add(TABLE)
Add element(s) to a window. Example: Add element(s) to a window. Example:
page:add({ page:add({
@ -584,6 +631,20 @@ function UI.Window:add(children)
self:initChildren() self:initChildren()
end 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
function UI.Window:getCursorPos() function UI.Window:getCursorPos()
return self.cursorX, self.cursorY return self.cursorX, self.cursorY
end end
@ -595,18 +656,20 @@ function UI.Window:setCursorPos(x, y)
end end
function UI.Window:setCursorBlink(blink) function UI.Window:setCursorBlink(blink)
self.parent:setCursorBlink(blink) self.cursorBlink = blink
end end
UI.Window.docs.draw = [[draw(VOID) UI.Window.docs.draw = [[draw(VOID)
Redraws the window in the internal buffer.]] Redraws the window in the internal buffer.]]
function UI.Window:draw() function UI.Window:draw()
self:clear(self.backgroundColor) self:clear()
if self.children then self:drawChildren()
for _,child in pairs(self.children) do end
if child.enabled then
child:draw() function UI.Window:drawChildren()
end for child in self:eachChild() do
if child.enabled then
child:draw()
end end
end end
end end
@ -633,19 +696,38 @@ function UI.Window:sync()
end end
function UI.Window:enable(...) function UI.Window:enable(...)
self.enabled = true if not self.enabled then
if self.children then self.enabled = true
for _,child in pairs(self.children) do if self.transitionHint then
child:enable(...) 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
end end
end end
end end
function UI.Window:disable() function UI.Window:disable()
self.enabled = false if self.enabled then
if self.children then self.enabled = false
for _,child in pairs(self.children) do self.parent:dirty(true)
child:disable()
if self.modal then
self:release(self)
end
for child in self:eachChild() do
if child.enabled then
child:disable()
end
end end
end end
end end
@ -656,13 +738,9 @@ function UI.Window:setTextScale(textScale)
end end
UI.Window.docs.clear = [[clear(opt COLOR bg, opt COLOR fg) UI.Window.docs.clear = [[clear(opt COLOR bg, opt COLOR fg)
Clears the window using the either the passed values or the defaults for that window.]] Clears the window using either the passed values or the defaults for that window.]]
function UI.Window:clear(bg, fg) function UI.Window:clear(bg, fg)
if self.canvas then Canvas.clear(self, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
self.canvas:clear(bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
else
self:clearArea(1 + self.offx, 1 + self.offy, self.width, self.height, bg)
end
end end
function UI.Window:clearLine(y, bg) function UI.Window:clearLine(y, bg)
@ -670,39 +748,28 @@ function UI.Window:clearLine(y, bg)
end end
function UI.Window:clearArea(x, y, width, height, bg) 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)
if width > 0 then if width > 0 then
local filler = _rep(' ', width) local filler = _rep(fillChar, width)
for i = 0, height - 1 do for i = 0, height - 1 do
self:write(x, y + i, filler, bg) self:write(x, y + i, filler, bg, fg)
end end
end end
end end
function UI.Window:write(x, y, text, bg, fg) function UI.Window:write(x, y, text, bg, fg)
bg = bg or self.backgroundColor Canvas.write(self, x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
fg = fg or self.textColor
if self.canvas then
self.canvas:write(x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
else
x = x - self.offx
y = y - self.offy
if y <= self.height and y > 0 then
self.parent:write(
self.x + x - 1, self.y + y - 1, tostring(text), bg, fg)
end
end
end end
function UI.Window:centeredWrite(y, text, bg, fg) function UI.Window:centeredWrite(y, text, bg, fg)
if #text >= self.width then if #text >= self.width then
self:write(1, y, text, bg, fg) self:write(1, y, text, bg, fg)
else else
local space = math.floor((self.width-#text) / 2) local x = math.floor((self.width-#text) / 2) + 1
local filler = _rep(' ', space + 1) self:write(x, y, text, bg, fg)
local str = _sub(filler, 1, space) .. text
str = str .. _sub(filler, self.width - #str + 1)
self:write(1, y, str, bg, fg)
end end
end end
@ -710,89 +777,19 @@ function UI.Window:print(text, bg, fg)
local marginLeft = self.marginLeft or 0 local marginLeft = self.marginLeft or 0
local marginRight = self.marginRight or 0 local marginRight = self.marginRight or 0
local width = self.width - marginLeft - marginRight local width = self.width - marginLeft - marginRight
local cs = {
bg = bg or self:getProperty('backgroundColor'),
fg = fg or self:getProperty('textColor'),
palette = self.palette,
}
local function nextWord(line, cx) local y = (self.marginTop or 0) + 1
local result = { line:find("(%w+)", cx) } for _,line in pairs(Util.split(text)) do
if #result > 1 and result[2] > cx then for _, ln in ipairs(Blit(line, cs):wrap(width)) do
return _sub(line, cx, result[2] + 1) self:blit(marginLeft + 1, y, ln.text, ln.bg, ln.fg)
elseif #result > 0 and result[1] == cx then y = y + 1
result = { line:find("(%w+)", result[2]) }
if #result > 0 then
return _sub(line, cx, result[1] + 1)
end
end
if cx <= #line then
return _sub(line, cx, #line)
end end
end end
local function pieces(f, bg, fg)
local pos = 1
local t = { }
while true do
local s = string.find(f, '\027', pos, true)
if not s then
break
end
if pos < s then
table.insert(t, _sub(f, pos, s - 1))
end
local seq = _sub(f, s)
seq = seq:match("\027%[([%d;]+)m")
local e = { }
for color in string.gmatch(seq, "%d+") do
color = tonumber(color)
if color == 0 then
e.fg = fg
e.bg = bg
elseif color > 20 then
e.bg = 2 ^ (color - 21)
else
e.fg = 2 ^ (color - 1)
end
end
table.insert(t, e)
pos = s + #seq + 3
end
if pos <= #f then
table.insert(t, _sub(f, pos))
end
return t
end
local lines = Util.split(text)
for k,line in pairs(lines) do
local fragments = pieces(line, bg, fg)
for _, fragment in ipairs(fragments) do
local lx = 1
if type(fragment) == 'table' then -- ansi sequence
fg = fragment.fg
bg = fragment.bg
else
while true do
local word = nextWord(fragment, lx)
if not word then
break
end
local w = word
if self.cursorX + #word > width then
self.cursorX = marginLeft + 1
self.cursorY = self.cursorY + 1
w = word:gsub('^ ', '')
end
self:write(self.cursorX, self.cursorY, w, bg, fg)
self.cursorX = self.cursorX + #w
lx = lx + #word
end
end
end
if lines[k + 1] then
self.cursorX = marginLeft + 1
self.cursorY = self.cursorY + 1
end
end
return self.cursorX, self.cursorY
end end
UI.Window.docs.focus = [[focus(VOID) UI.Window.docs.focus = [[focus(VOID)
@ -822,11 +819,11 @@ function UI.Window:release(child)
end end
function UI.Window:pointToChild(x, y) function UI.Window:pointToChild(x, y)
-- TODO: get rid of this offx/y mess and scroll canvas instead
x = x + self.offx - self.x + 1 x = x + self.offx - self.x + 1
y = y + self.offy - self.y + 1 y = y + self.offy - self.y + 1
if self.children then if self.children then
for _,child in pairs(self.children) do for i = #self.children, 1, -1 do
local child = self.children[i]
if child.enabled and not child.inactive and if child.enabled and not child.inactive and
x >= child.x and x < child.x + child.width and x >= child.x and x < child.x + child.width and
y >= child.y and y < child.y + child.height then y >= child.y and y < child.y + child.height then
@ -868,7 +865,7 @@ function UI.Window:getFocusables()
end end
if self.children then if self.children then
getFocusable(self, self.x, self.y) getFocusable(self)
end end
return focusable return focusable
@ -882,36 +879,28 @@ function UI.Window:focusFirst()
end end
end end
function UI.Window:refocus()
local el = self
while el do
local focusables = el:getFocusables()
if focusables[1] then
self:setFocus(focusables[1])
break
end
el = el.parent
end
end
function UI.Window:scrollIntoView() function UI.Window:scrollIntoView()
local parent = self.parent local parent = self.parent
local offx, offy = parent.offx, parent.offy
if self.x <= parent.offx then if self.x <= parent.offx then
parent.offx = math.max(0, self.x - 1) parent.offx = math.max(0, self.x - 1)
parent:draw() if offx ~= parent.offx then
parent:draw()
end
elseif self.x + self.width > parent.width + parent.offx then elseif self.x + self.width > parent.width + parent.offx then
parent.offx = self.x + self.width - parent.width - 1 parent.offx = self.x + self.width - parent.width - 1
parent:draw() if offx ~= parent.offx then
parent:draw()
end
end end
-- TODO: fix -- TODO: fix
local function setOffset(y) local function setOffset(y)
parent.offy = y parent.offy = y
if parent.canvas then if offy ~= parent.offy then
parent.canvas.offy = parent.offy parent:draw()
end end
parent:draw()
end end
if self.y <= parent.offy then if self.y <= parent.offy then
@ -921,45 +910,8 @@ function UI.Window:scrollIntoView()
end end
end end
function UI.Window:getCanvas() function UI.Window:addTransition(effect, args, canvas)
local el = self self.parent:addTransition(effect, args, canvas or self)
repeat
if el.canvas then
return el.canvas
end
el = el.parent
until not el
end
function UI.Window:addLayer(bg, fg)
local canvas = self:getCanvas()
local x, y = self.x, self.y
local parent = self.parent
while parent and not parent.canvas do
x = x + parent.x - 1
y = y + parent.y - 1
parent = parent.parent
end
canvas = canvas:addLayer({
x = x, y = y, height = self.height, width = self.width
}, bg, fg)
canvas:clear(bg or self.backgroundColor, fg or self.textColor)
return canvas
end
function UI.Window:addTransition(effect, args)
if self.parent then
args = args or { }
if not args.x then -- not good
args.x, args.y = self.x, self.y -- getPosition(self)
args.width = self.width
args.height = self.height
end
args.canvas = args.canvas or self.canvas
self.parent:addTransition(effect, args)
end
end end
function UI.Window:emit(event) function UI.Window:emit(event)
@ -991,7 +943,16 @@ function UI.Window:getProperty(property)
end end
function UI.Window:find(uid) function UI.Window:find(uid)
return self.children and Util.find(self.children, 'uid', 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
end end
function UI.Window:eventHandler() function UI.Window:eventHandler()
@ -1010,18 +971,14 @@ UI.Device.defaults = {
function UI.Device:postInit() function UI.Device:postInit()
self.device = self.device or term.current() self.device = self.device or term.current()
--if self.deviceType then
-- self.device = device[self.deviceType]
--end
if not self.device.setTextScale then if not self.device.setTextScale then
self.device.setTextScale = function() end self.device.setTextScale = function() end
end end
self.device.setTextScale(self.textScale) self.device.setTextScale(self.textScale)
self.width, self.height = self.device.getSize() self.width, self.height = self.device.getSize()
self.isColor = self.device.isColor() self.isColor = self.device.isColor()
Canvas.init(self, { isColor = self.isColor })
UI.devices[self.device.side or 'terminal'] = self UI.devices[self.device.side or 'terminal'] = self
end end
@ -1031,8 +988,8 @@ function UI.Device:resize()
self.width, self.height = self.device.getSize() self.width, self.height = self.device.getSize()
self.lines = { } self.lines = { }
-- TODO: resize all pages added to this device -- TODO: resize all pages added to this device
self.canvas:resize(self.width, self.height) Canvas.resize(self, self.width, self.height)
self.canvas:clear(self.backgroundColor, self.textColor) Canvas.clear(self, self.backgroundColor, self.textColor)
end end
function UI.Device:setCursorPos(x, y) function UI.Device:setCursorPos(x, y)
@ -1046,7 +1003,6 @@ end
function UI.Device:setCursorBlink(blink) function UI.Device:setCursorBlink(blink)
self.cursorBlink = blink self.cursorBlink = blink
self.device.setCursorBlink(blink)
end end
function UI.Device:setTextScale(textScale) function UI.Device:setTextScale(textScale)
@ -1061,27 +1017,30 @@ function UI.Device:reset()
self.device.setCursorPos(1, 1) self.device.setCursorPos(1, 1)
end end
function UI.Device:addTransition(effect, args) function UI.Device:addTransition(effect, args, canvas)
if not self.transitions then if not self.transitions then
self.transitions = { } self.transitions = { }
end end
args = args or { }
args.ex = args.x + args.width - 1
args.ey = args.y + args.height - 1
args.canvas = args.canvas or self.canvas
if type(effect) == 'string' then if type(effect) == 'string' then
effect = Transition[effect] effect = Transition[effect] or error('Invalid transition')
if not effect then end
error('Invalid transition')
-- there can be only one
for k,v in pairs(self.transitions) do
if v.canvas == canvas then
table.remove(self.transitions, k)
break
end end
end end
table.insert(self.transitions, { update = effect(args), args = args }) table.insert(self.transitions, { effect = effect, args = args or { }, canvas = canvas })
end end
function UI.Device:runTransitions(transitions, canvas) function UI.Device:runTransitions(transitions)
for _,k in pairs(transitions) do
k.update = k.effect(k.canvas, k.args)
end
while true do while true do
for _,k in ipairs(Util.keys(transitions)) do for _,k in ipairs(Util.keys(transitions)) do
local transition = transitions[k] local transition = transitions[k]
@ -1089,7 +1048,7 @@ function UI.Device:runTransitions(transitions, canvas)
transitions[k] = nil transitions[k] = nil
end end
end end
canvas:render(self.device) self.currentPage:render(self, true)
if Util.empty(transitions) then if Util.empty(transitions) then
break break
end end
@ -1101,17 +1060,19 @@ function UI.Device:sync()
local transitions = self.effectsEnabled and self.transitions local transitions = self.effectsEnabled and self.transitions
self.transitions = nil self.transitions = nil
if self:getCursorBlink() then self.device.setCursorBlink(false)
self.device.setCursorBlink(false)
end
self.canvas:render(self.device)
if transitions then if transitions then
self:runTransitions(transitions, self.canvas) self:runTransitions(transitions)
else
self.currentPage:render(self, true)
end end
if self:getCursorBlink() then if self:getCursorBlink() then
self.device.setCursorPos(self.cursorX, self.cursorY) self.device.setCursorPos(self.cursorX, self.cursorY)
if self.isColor then
self.device.setTextColor(colors.orange)
end
self.device.setCursorBlink(true) self.device.setCursorBlink(true)
end end
end end
@ -1143,7 +1104,7 @@ local function loadComponents()
return self(...) return self(...)
end end
}) })
UI[name]._preload = function(self) UI[name]._preload = function()
return load(name) return load(name)
end end
end end
@ -1152,6 +1113,12 @@ end
loadComponents() loadComponents()
UI:loadTheme('usr/config/ui.theme') UI:loadTheme('usr/config/ui.theme')
Util.merge(UI.Window.defaults, UI.theme.Window) Util.merge(UI.Window.defaults, UI.theme.Window)
UI:setDefaultDevice(UI.Device({ device = term.current() })) 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
return UI return UI

View File

@ -0,0 +1,174 @@
local colors = _G.colors
local _rep = string.rep
local _sub = string.sub
local Blit = { }
Blit.colorPalette = { }
Blit.grayscalePalette = { }
for n = 1, 16 do
Blit.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n)
Blit.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
end
-- default palette
Blit.palette = Blit.colorPalette
function Blit:init(t, args)
if args then
for k,v in pairs(args) do
self[k] = v
end
end
if type(t) == 'string' then
-- create a blit from a string
self.text, self.bg, self.fg = Blit.toblit(t, args or { })
elseif type(t) == 'number' then
-- create a fixed width blit
self.width = t
self.text = _rep(' ', self.width)
self.bg = _rep(self.palette[args.bg], self.width)
self.fg = _rep(self.palette[args.fg], self.width)
else
self.text = t.text
self.bg = t.bg
self.fg = t.fg
end
end
function Blit:write(x, text, bg, fg)
self:insert(x, text,
bg and _rep(self.palette[bg], #text),
fg and _rep(self.palette[fg], #text))
end
function Blit:insert(x, text, bg, fg)
if x <= self.width then
local width = #text
local tx, tex
if x < 1 then
tx = 2 - x
width = width + x - 1
x = 1
end
if x + width - 1 > self.width then
tex = self.width - x + (tx or 1)
width = tex - (tx or 1) + 1
end
if width > 0 then
local function replace(sstr, rstr)
if tx or tex then
rstr = _sub(rstr, tx or 1, tex)
end
if x == 1 and width == self.width then
return rstr
elseif x == 1 then
return rstr .. _sub(sstr, x + width)
elseif x + width > self.width then
return _sub(sstr, 1, x - 1) .. rstr
end
return _sub(sstr, 1, x - 1) .. rstr .. _sub(sstr, x + width)
end
self.text = replace(self.text, text)
if fg then
self.fg = replace(self.fg, fg)
end
if bg then
self.bg = replace(self.bg, bg)
end
end
end
end
function Blit:sub(s, e)
return Blit({
text = self.text:sub(s, e),
bg = self.bg:sub(s, e),
fg = self.fg:sub(s, e),
})
end
function Blit:wrap(max)
local lines = { }
local data = self
repeat
if #data.text <= max then
table.insert(lines, data)
break
elseif data.text:sub(max+1, max+1) == ' ' then
table.insert(lines, data:sub(1, max))
data = data:sub(max + 2)
else
local x = data.text:sub(1, max)
local s = x:match('(.*) ') or x
table.insert(lines, data:sub(1, #s))
data = data:sub(#s + 1)
end
local t = data.text:match('^%s*(.*)')
local spaces = #data.text - #t
if spaces > 0 then
data = data:sub(spaces + 1)
end
until not data.text or #data.text == 0
return lines
end
-- convert a string of text to blit format doing color conversion
-- and processing ansi color sequences
function Blit.toblit(str, cs)
local text, fg, bg = '', '', ''
if not cs.cbg then
-- reset colors
cs.rbg = cs.bg or colors.black
cs.rfg = cs.fg or colors.white
-- current colors
cs.cbg = cs.rbg
cs.cfg = cs.rfg
cs.palette = cs.palette or Blit.palette
end
str = str:gsub('(.-)\027%[([%d;]+)m',
function(k, seq)
text = text .. k
bg = bg .. string.rep(cs.palette[cs.cbg], #k)
fg = fg .. string.rep(cs.palette[cs.cfg], #k)
for color in string.gmatch(seq, "%d+") do
color = tonumber(color)
if color == 0 then
-- reset to default
cs.cfg = cs.rfg
cs.cbg = cs.rbg
elseif color > 20 then
cs.cbg = 2 ^ (color - 21)
else
cs.cfg = 2 ^ (color - 1)
end
end
return k
end)
local k = str:sub(#text + 1)
return text .. k,
bg .. string.rep(cs.palette[cs.cbg], #k),
fg .. string.rep(cs.palette[cs.cfg], #k)
end
return setmetatable(Blit, {
__call = function(_, ...)
local obj = setmetatable({ }, { __index = Blit })
obj:init(...)
return obj
end
})

View File

@ -9,28 +9,34 @@ local colors = _G.colors
local Canvas = class() local Canvas = class()
Canvas.__visualize = false local function genPalette(map)
Canvas.colorPalette = { } local t = { }
Canvas.darkPalette = { } local rcolors = Util.transpose(colors)
Canvas.grayscalePalette = { } for n = 1, 16 do
local pow = 2 ^ (n - 1)
for n = 1, 16 do local ch = _sub(map, n, n)
Canvas.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) t[pow] = ch
Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) t[rcolors[pow]] = ch
Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n) end
return t
end end
Canvas.colorPalette = genPalette('0123456789abcdef')
Canvas.grayscalePalette = genPalette('088888878877787f')
--[[ --[[
A canvas can have more lines than canvas.height in order to scroll A canvas can have more lines than canvas.height in order to scroll
]]
TODO: finish vertical scrolling
]]
function Canvas:init(args) function Canvas:init(args)
self.x = 1 self.bg = colors.black
self.y = 1 self.fg = colors.white
self.layers = { }
Util.merge(self, args) Util.merge(self, args)
self.x = self.x or 1
self.y = self.y or 1
self.ex = self.x + self.width - 1 self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1 self.ey = self.y + self.height - 1
@ -46,16 +52,31 @@ function Canvas:init(args)
for i = 1, self.height do for i = 1, self.height do
self.lines[i] = { } self.lines[i] = { }
end end
self:clear()
end end
function Canvas:move(x, y) function Canvas:move(x, y)
self.x, self.y = x, y self.x, self.y = x, y
self.ex = self.x + self.width - 1 self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1 self.ey = self.y + self.height - 1
if self.parent then
self.parent:dirty(true)
end
end end
function Canvas:resize(w, h) function Canvas:resize(w, h)
for i = #self.lines, h do self:resizeBuffer(w, h)
self.ex = self.x + w - 1
self.ey = self.y + h - 1
self.width = w
self.height = h
end
-- resize the canvas buffer - not the canvas itself
function Canvas:resizeBuffer(w, h)
for i = #self.lines + 1, h do
self.lines[i] = { } self.lines[i] = { }
self:clearLine(i) self:clearLine(i)
end end
@ -66,26 +87,24 @@ function Canvas:resize(w, h)
if w < self.width then if w < self.width then
for i = 1, h do for i = 1, h do
self.lines[i].text = _sub(self.lines[i].text, 1, w) local ln = self.lines[i]
self.lines[i].fg = _sub(self.lines[i].fg, 1, w) ln.text = _sub(ln.text, 1, w)
self.lines[i].bg = _sub(self.lines[i].bg, 1, w) ln.fg = _sub(ln.fg, 1, w)
ln.bg = _sub(ln.bg, 1, w)
end end
elseif w > self.width then elseif w > self.width then
local d = w - self.width local d = w - self.width
local text = _rep(' ', d) local text = _rep(' ', d)
local fg = _rep(self.palette[self.fg or colors.white], d) local fg = _rep(self.palette[self.fg], d)
local bg = _rep(self.palette[self.bg or colors.black], d) local bg = _rep(self.palette[self.bg], d)
for i = 1, h do for i = 1, h do
self.lines[i].text = self.lines[i].text .. text local ln = self.lines[i]
self.lines[i].fg = self.lines[i].fg .. fg ln.text = ln.text .. text
self.lines[i].bg = self.lines[i].bg .. bg ln.fg = ln.fg .. fg
ln.bg = ln.bg .. bg
ln.dirty = true
end end
end end
self.ex = self.x + w - 1
self.ey = self.y + h - 1
self.width = w
self.height = h
end end
function Canvas:copy() function Canvas:copy()
@ -105,30 +124,26 @@ function Canvas:copy()
end end
function Canvas:addLayer(layer) function Canvas:addLayer(layer)
local canvas = Canvas({ layer.parent = self
x = layer.x, if not self.children then
y = layer.y, self.children = { }
width = layer.width, end
height = layer.height, table.insert(self.children, 1, layer)
isColor = self.isColor, return layer
})
canvas.parent = self
table.insert(self.layers, canvas)
return canvas
end end
function Canvas:removeLayer() function Canvas:removeLayer()
for k, layer in pairs(self.parent.layers) do for k, layer in pairs(self.parent.children) do
if layer == self then if layer == self then
self:setVisible(false) self:setVisible(false)
table.remove(self.parent.layers, k) table.remove(self.parent.children, k)
break break
end end
end end
end end
function Canvas:setVisible(visible) function Canvas:setVisible(visible)
self.visible = visible self.visible = visible -- TODO: use self.active = visible
if not visible and self.parent then if not visible and self.parent then
self.parent:dirty() self.parent:dirty()
-- TODO: set parent's lines to dirty for each line in self -- TODO: set parent's lines to dirty for each line in self
@ -137,11 +152,10 @@ end
-- Push a layer to the top -- Push a layer to the top
function Canvas:raise() function Canvas:raise()
if self.parent then if self.parent and self.parent.children then
local layers = self.parent.layers or { } for k, v in pairs(self.parent.children) do
for k, v in pairs(layers) do
if v == self then if v == self then
table.insert(layers, table.remove(layers, k)) table.insert(self.parent.children, table.remove(self.parent.children, k))
break break
end end
end end
@ -161,54 +175,42 @@ end
function Canvas:blit(x, y, text, bg, fg) function Canvas:blit(x, y, text, bg, fg)
if y > 0 and y <= #self.lines and x <= self.width then if y > 0 and y <= #self.lines and x <= self.width then
local width = #text local width = #text
local tx, tex
-- fix ffs
if x < 1 then if x < 1 then
text = _sub(text, 2 - x) tx = 2 - x
if bg then
bg = _sub(bg, 2 - x)
end
if fg then
fg = _sub(fg, 2 - x)
end
width = width + x - 1 width = width + x - 1
x = 1 x = 1
end end
if x + width - 1 > self.width then if x + width - 1 > self.width then
text = _sub(text, 1, self.width - x + 1) tex = self.width - x + (tx or 1)
if bg then width = tex - (tx or 1) + 1
bg = _sub(bg, 1, self.width - x + 1)
end
if fg then
fg = _sub(fg, 1, self.width - x + 1)
end
width = #text
end end
if width > 0 then if width > 0 then
local function replace(sstr, rstr)
local function replace(sstr, pos, rstr) if tx or tex then
if pos == 1 and width == self.width then rstr = _sub(rstr, tx or 1, tex)
return rstr
elseif pos == 1 then
return rstr .. _sub(sstr, pos+width)
elseif pos + width > self.width then
return _sub(sstr, 1, pos-1) .. rstr
end end
return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) if x == 1 and width == self.width then
return rstr
elseif x == 1 then
return rstr .. _sub(sstr, x + width)
elseif x + width > self.width then
return _sub(sstr, 1, x - 1) .. rstr
end
return _sub(sstr, 1, x - 1) .. rstr .. _sub(sstr, x + width)
end end
local line = self.lines[y] local line = self.lines[y]
if line then line.dirty = true
line.dirty = true line.text = replace(line.text, text)
line.text = replace(line.text, x, text, width) if fg then
if fg then line.fg = replace(line.fg, fg)
line.fg = replace(line.fg, x, fg, width) end
end if bg then
if bg then line.bg = replace(line.bg, bg)
line.bg = replace(line.bg, x, bg, width)
end
end end
end end
end end
@ -224,15 +226,15 @@ function Canvas:writeLine(y, text, fg, bg)
end end
function Canvas:clearLine(y, bg, fg) function Canvas:clearLine(y, bg, fg)
fg = _rep(self.palette[fg or colors.white], self.width) fg = _rep(self.palette[fg or self.fg], self.width)
bg = _rep(self.palette[bg or colors.black], self.width) bg = _rep(self.palette[bg or self.bg], self.width)
self:writeLine(y, _rep(' ', self.width), fg, bg) self:writeLine(y, _rep(' ', self.width), fg, bg)
end end
function Canvas:clear(bg, fg) function Canvas:clear(bg, fg)
local text = _rep(' ', self.width) local text = _rep(' ', self.width)
fg = _rep(self.palette[fg or colors.white], self.width) fg = _rep(self.palette[fg or self.fg], self.width)
bg = _rep(self.palette[bg or colors.black], self.width) bg = _rep(self.palette[bg or self.bg], self.width)
for i = 1, #self.lines do for i = 1, #self.lines do
self:writeLine(i, text, fg, bg) self:writeLine(i, text, fg, bg)
end end
@ -246,13 +248,16 @@ function Canvas:isDirty()
end end
end end
function Canvas:dirty() function Canvas:dirty(includingChildren)
for i = 1, #self.lines do if self.lines then
self.lines[i].dirty = true for i = 1, #self.lines do
end self.lines[i].dirty = true
if self.layers then end
for _, canvas in pairs(self.layers) do
canvas:dirty() if includingChildren and self.children then
for _, child in pairs(self.children) do
child:dirty(true)
end
end end
end end
end end
@ -278,115 +283,95 @@ function Canvas:applyPalette(palette)
self.palette = palette self.palette = palette
end end
function Canvas:render(device) -- either render directly to the device
local offset = { x = 0, y = 0 } -- or use another canvas as a backing buffer
local parent = self.parent function Canvas:render(device, doubleBuffer)
while parent do self.regions = Region.new(self.x, self.y, self.ex, self.ey)
offset.x = offset.x + parent.x - 1 self:__renderLayers(device, { x = self.x - 1, y = self.y - 1 }, doubleBuffer)
offset.y = offset.y + parent.y - 1
parent = parent.parent -- doubleBuffering to reduce the amount of
end -- setCursorPos, blits
if #self.layers > 0 then if doubleBuffer then
self:__renderLayers(device, offset) --[[
else local drew = false
self:__blitRect(device, nil, { local bg = _rep(2, device.width)
x = self.x + offset.x, for k,v in pairs(device.lines) do
y = self.y + offset.y if v.dirty then
}) device.device.setCursorPos(device.x, device.y + k - 1)
self:clean() device.device.blit(v.text, v.fg, bg)
drew = true
end
end
if drew then
local c = os.clock()
repeat until os.clock()-c > .1
end
]]
for k,v in pairs(device.lines) do
if v.dirty then
device.device.setCursorPos(device.x, device.y + k - 1)
device.device.blit(v.text, v.fg, v.bg)
v.dirty = false
end
end
end end
end end
-- regions are comprised of absolute values that coorespond to the output device. -- regions are comprised of absolute values that correspond to the output device.
-- canvases have coordinates relative to their parent. -- canvases have coordinates relative to their parent.
-- canvas layer's stacking order is determined by the position within the array. -- canvas layer's stacking order is determined by the position within the array.
-- layers in the beginning of the array are overlayed by layers further down in -- layers in the beginning of the array are overlayed by layers further down in
-- the array. -- the array.
function Canvas:__renderLayers(device, offset) function Canvas:__renderLayers(device, offset, doubleBuffer)
if #self.layers > 0 then if self.children then
self.regions = self.regions or Region.new(self.x + offset.x, self.y + offset.y, self.ex + offset.x, self.ey + offset.y) for i = #self.children, 1, -1 do
local canvas = self.children[i]
for i = 1, #self.layers do if canvas.visible or canvas.enabled then
local canvas = self.layers[i]
if canvas.visible then
-- punch out this area from the parent's canvas
self:__punch(canvas, offset)
-- get the area to render for this layer -- get the area to render for this layer
canvas.regions = Region.new( canvas.regions = Region.new(
canvas.x + offset.x, canvas.x + offset.x - (self.offx or 0),
canvas.y + offset.y, canvas.y + offset.y - (self.offy or 0),
canvas.ex + offset.x, canvas.ex + offset.x - (self.offx or 0),
canvas.ey + offset.y) canvas.ey + offset.y - (self.offy or 0))
-- contain within parent
canvas.regions:andRegion(self.regions)
-- punch out this area from the parent's canvas
self.regions:subRect(
canvas.x + offset.x - (self.offx or 0),
canvas.y + offset.y - (self.offy or 0),
canvas.ex + offset.x - (self.offx or 0),
canvas.ey + offset.y - (self.offy or 0))
-- punch out any layers that overlap this one
for j = i + 1, #self.layers do
if self.layers[j].visible then
canvas:__punch(self.layers[j], offset)
end
end
if #canvas.regions.region > 0 then if #canvas.regions.region > 0 then
canvas:__renderLayers(device, { canvas:__renderLayers(device, {
x = canvas.x + offset.x - 1, x = canvas.x + offset.x - 1 - (self.offx or 0),
y = canvas.y + offset.y - 1, y = canvas.y + offset.y - 1 - (self.offy or 0),
}) }, doubleBuffer)
end end
canvas.regions = nil canvas.regions = nil
end end
end end
self:__blitClipped(device, offset)
self.regions = nil
elseif self.regions and #self.regions.region > 0 then
self:__blitClipped(device, offset)
self.regions = nil
else
self:__blitRect(device, nil, {
x = self.x + offset.x,
y = self.y + offset.y
})
self.regions = nil
end
self:clean()
end
function Canvas:__blitClipped(device, offset)
if self.parent then
-- contain the rendered region in the parent's region
local p = Region.new(1, 1,
self.parent.width + offset.x - self.x + 1,
self.parent.height + offset.y - self.y + 1)
self.regions:andRegion(p)
end end
for _,region in ipairs(self.regions.region) do for _,region in ipairs(self.regions.region) do
self:__blitRect(device, self:__blitRect(device,
{ x = region[1] - offset.x, { x = region[1] - offset.x,
y = region[2] - offset.y, y = region[2] - offset.y,
ex = region[3] - offset.x, ex = region[3] - offset.x,
ey = region[4] - offset.y}, ey = region[4] - offset.y },
{ x = region[1], y = region[2] }) { x = region[1], y = region[2] }, doubleBuffer)
end end
self.regions = nil
self:clean()
end end
function Canvas:__punch(rect, offset) function Canvas:__blitRect(device, src, tgt, doubleBuffer)
self.regions:subRect(
rect.x + offset.x,
rect.y + offset.y,
rect.ex + offset.x,
rect.ey + offset.y)
end
-- performance can probably be improved by using one more buffer tied to the device
function Canvas:__blitRect(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 visualizing updates on the screen -- for visualizing updates on the screen
if Canvas.__visualize then --[[
if Canvas.__visualize or self.visualize then
local drew local drew
local t = _rep(' ', src.ex-src.x + 1) local t = _rep(' ', src.ex-src.x + 1)
local bg = _rep(2, src.ex-src.x + 1) local bg = _rep(2, src.ex-src.x + 1)
@ -399,10 +384,11 @@ function Canvas:__blitRect(device, src, tgt)
end end
end end
if drew then if drew then
local t = os.clock() local c = os.clock()
repeat until os.clock()-t > .2 repeat until os.clock()-c > .03
end end
end end
]]
for i = 0, src.ey - src.y do for i = 0, src.ey - src.y do
local line = self.lines[src.y + i + (self.offy or 0)] local line = self.lines[src.y + i + (self.offy or 0)]
if line and line.dirty then if line and line.dirty then
@ -412,8 +398,13 @@ function Canvas:__blitRect(device, src, tgt)
fg = _sub(fg, src.x, src.ex) fg = _sub(fg, src.x, src.ex)
bg = _sub(bg, src.x, src.ex) bg = _sub(bg, src.x, src.ex)
end end
device.setCursorPos(tgt.x, tgt.y + i) if doubleBuffer then
device.blit(t, fg, bg) Canvas.blit(device, tgt.x, tgt.y + i,
t, bg, fg)
else
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, fg, bg)
end
end end
end end
end end

View File

@ -1,32 +0,0 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.ActiveLayer = class(UI.Window)
UI.ActiveLayer.defaults = {
UIElement = 'ActiveLayer',
}
function UI.ActiveLayer:layout()
UI.Window.layout(self)
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.ActiveLayer:enable(...)
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...)
if self.parent.transitionHint then
self:addTransition(self.parent.transitionHint)
end
self:focusFirst()
end
function UI.ActiveLayer:disable()
if self.canvas then
self.canvas:setVisible(false)
end
UI.Window.disable(self)
end

View File

@ -2,32 +2,30 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.Button = class(UI.Window) UI.Button = class(UI.Window)
UI.Button.defaults = { UI.Button.defaults = {
UIElement = 'Button', UIElement = 'Button',
text = 'button', text = 'button',
backgroundColor = colors.lightGray, backgroundColor = 'lightGray',
backgroundFocusColor = colors.gray, backgroundFocusColor = 'gray',
textFocusColor = colors.white, textFocusColor = 'white',
textInactiveColor = colors.gray, textInactiveColor = 'gray',
textColor = colors.black, textColor = 'black',
centered = true, centered = true,
height = 1, height = 1,
focusIndicator = ' ', focusIndicator = ' ',
event = 'button_press', event = 'button_press',
accelerators = { accelerators = {
space = 'button_activate', [ ' ' ] = 'button_activate',
enter = 'button_activate', enter = 'button_activate',
mouse_click = 'button_activate', mouse_click = 'button_activate',
} }
} }
function UI.Button:setParent() function UI.Button:layout()
if not self.width and not self.ex then if not self.width and not self.ex then
self.width = #self.text + 2 self.width = self.noPadding and #self.text or #self.text + 2
end end
UI.Window.setParent(self) UI.Window.layout(self)
end end
function UI.Button:draw() function UI.Button:draw()
@ -35,13 +33,13 @@ function UI.Button:draw()
local bg = self.backgroundColor local bg = self.backgroundColor
local ind = ' ' local ind = ' '
if self.focused then if self.focused then
bg = self.backgroundFocusColor bg = self:getProperty('backgroundFocusColor')
fg = self.textFocusColor fg = self:getProperty('textFocusColor')
ind = self.focusIndicator ind = self.focusIndicator
elseif self.inactive then elseif self.inactive then
fg = self.textInactiveColor fg = self:getProperty('textInactiveColor')
end end
local text = ind .. self.text .. ' ' local text = self.noPadding and self.text or ind .. self.text .. ' '
if self.centered then if self.centered then
self:clear(bg) self:clear(bg)
self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg)
@ -59,7 +57,7 @@ end
function UI.Button:eventHandler(event) function UI.Button:eventHandler(event)
if event.type == 'button_activate' then if event.type == 'button_activate' then
self:emit({ type = self.event, button = self }) self:emit({ type = self.event, button = self, element = self })
return true return true
end end
return false return false
@ -73,7 +71,7 @@ function UI.Button.example()
}, },
button2 = UI.Button { button2 = UI.Button {
x = 2, y = 4, x = 2, y = 4,
backgroundColor = colors.green, backgroundColor = 'green',
event = 'custom_event', event = 'custom_event',
}, },
button3 = UI.Button { button3 = UI.Button {

View File

@ -1,8 +1,6 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Checkbox = class(UI.Window) UI.Checkbox = class(UI.Window)
UI.Checkbox.defaults = { UI.Checkbox.defaults = {
UIElement = 'Checkbox', UIElement = 'Checkbox',
@ -11,9 +9,9 @@ UI.Checkbox.defaults = {
leftMarker = UI.extChars and '\124' or '[', leftMarker = UI.extChars and '\124' or '[',
rightMarker = UI.extChars and '\124' or ']', rightMarker = UI.extChars and '\124' or ']',
value = false, value = false,
textColor = colors.white, textColor = 'white',
backgroundColor = colors.black, backgroundColor = 'black',
backgroundFocusColor = colors.lightGray, backgroundFocusColor = 'lightGray',
height = 1, height = 1,
width = 3, width = 3,
accelerators = { accelerators = {
@ -21,11 +19,9 @@ UI.Checkbox.defaults = {
mouse_click = 'checkbox_toggle', mouse_click = 'checkbox_toggle',
} }
} }
UI.Checkbox.inherits = { function UI.Checkbox:layout()
labelBackgroundColor = 'backgroundColor',
}
function UI.Checkbox:postInit()
self.width = self.label and #self.label + 4 or 3 self.width = self.label and #self.label + 4 or 3
UI.Window.layout(self)
end end
function UI.Checkbox:draw() function UI.Checkbox:draw()

View File

@ -11,8 +11,8 @@ UI.Chooser.defaults = {
nochoice = 'Select', nochoice = 'Select',
backgroundFocusColor = colors.lightGray, backgroundFocusColor = colors.lightGray,
textInactiveColor = colors.gray, textInactiveColor = colors.gray,
leftIndicator = UI.extChars and '\17' or '<', leftIndicator = UI.extChars and '\171' or '<',
rightIndicator = UI.extChars and '\16' or '>', rightIndicator = UI.extChars and '\187' or '>',
height = 1, height = 1,
accelerators = { accelerators = {
space = 'choice_next', space = 'choice_next',
@ -20,7 +20,7 @@ UI.Chooser.defaults = {
left = 'choice_prev', left = 'choice_prev',
} }
} }
function UI.Chooser:setParent() function UI.Chooser:layout()
if not self.width and not self.ex then if not self.width and not self.ex then
self.width = 1 self.width = 1
for _,v in pairs(self.choices) do for _,v in pairs(self.choices) do
@ -30,7 +30,7 @@ function UI.Chooser:setParent()
end end
self.width = self.width + 4 self.width = self.width + 4
end end
UI.Window.setParent(self) UI.Window.layout(self)
end end
function UI.Chooser:draw() function UI.Chooser:draw()

View File

@ -1,15 +1,11 @@
local Canvas = require('opus.ui.canvas')
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Dialog = class(UI.SlideOut) UI.Dialog = class(UI.SlideOut)
UI.Dialog.defaults = { UI.Dialog.defaults = {
UIElement = 'Dialog', UIElement = 'Dialog',
height = 7, height = 7,
textColor = colors.black, noFill = true,
backgroundColor = colors.white,
okEvent ='dialog_ok', okEvent ='dialog_ok',
cancelEvent = 'dialog_cancel', cancelEvent = 'dialog_cancel',
} }
@ -18,22 +14,36 @@ function UI.Dialog:postInit()
self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title }) self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title })
end end
function UI.Dialog:show(...)
local canvas = self.parent:getCanvas()
self.oldPalette = canvas.palette
canvas:applyPalette(Canvas.darkPalette)
UI.SlideOut.show(self, ...)
end
function UI.Dialog:hide(...)
self.parent:getCanvas().palette = self.oldPalette
UI.SlideOut.hide(self, ...)
self.parent:draw()
end
function UI.Dialog:eventHandler(event) function UI.Dialog:eventHandler(event)
if event.type == 'dialog_cancel' then if event.type == 'dialog_cancel' then
self:hide() self:hide()
end end
return UI.SlideOut.eventHandler(self, event) return UI.SlideOut.eventHandler(self, event)
end end
function UI.Dialog.example()
return UI.Dialog {
title = 'Enter Starting Level',
height = 7,
form = UI.Form {
y = 3, x = 2, height = 4,
event = 'setStartLevel',
cancelEvent = 'slide_hide',
text = UI.Text {
x = 5, y = 1, width = 20,
textColor = 'gray',
},
textEntry = UI.TextEntry {
formKey = 'level',
x = 15, y = 1, width = 7,
},
},
statusBar = UI.StatusBar(),
enable = function(self)
require('opus.event').onTimeout(0, function()
self:show()
self:sync()
end)
end,
}
end

View File

@ -2,12 +2,10 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.DropMenu = class(UI.MenuBar) UI.DropMenu = class(UI.MenuBar)
UI.DropMenu.defaults = { UI.DropMenu.defaults = {
UIElement = 'DropMenu', UIElement = 'DropMenu',
backgroundColor = colors.white, backgroundColor = 'white',
buttonClass = 'DropMenuItem', buttonClass = 'DropMenuItem',
} }
function UI.DropMenu:layout() function UI.DropMenu:layout()
@ -32,42 +30,53 @@ function UI.DropMenu:layout()
self.height = #self.children + 1 self.height = #self.children + 1
self.width = maxWidth + 2 self.width = maxWidth + 2
if not self.canvas then if self.x + self.width > self.parent.width then
self.canvas = self:addLayer() self.x = self.parent.width - self.width + 1
else
self.canvas:resize(self.width, self.height)
end end
self:reposition(self.x, self.y, self.width, self.height)
end end
function UI.DropMenu:enable() function UI.DropMenu:enable()
end local menuBar = self.parent:find(self.menuUid)
local hasActive
function UI.DropMenu:show(x, y) for _,c in pairs(self.children) do
self.x, self.y = x, y if not c.spacer and menuBar then
self.canvas:move(x, y) c.inactive = not menuBar:getActive(c)
self.canvas:setVisible(true) end
if not c.inactive then
hasActive = true
end
end
-- jump through a lot of hoops if all selections are inactive
-- there's gotta be a better way
-- lots of exception code just to handle drop menus
self.focus = not hasActive and function() end
UI.Window.enable(self) UI.Window.enable(self)
if self.focus then
self:setFocus(self)
else
self:focusFirst()
end
self:draw() self:draw()
self:capture(self)
self:focusFirst()
end end
function UI.DropMenu:hide() function UI.DropMenu:disable()
self:disable() UI.Window.disable(self)
self.canvas:setVisible(false) self:remove()
self:release(self)
end end
function UI.DropMenu:eventHandler(event) function UI.DropMenu:eventHandler(event)
if event.type == 'focus_lost' and self.enabled then if event.type == 'focus_lost' and self.enabled then
if not Util.contains(self.children, event.focused) then if not (Util.contains(self.children, event.focused) or event.focused == self) then
self:hide() self:disable()
end end
elseif event.type == 'mouse_out' and self.enabled then elseif event.type == 'mouse_out' and self.enabled then
self:hide() self:disable()
self:refocus() self:setFocus(self.parent:find(self.lastFocus))
else else
return UI.MenuBar.eventHandler(self, event) return UI.MenuBar.eventHandler(self, event)
end end
@ -83,6 +92,15 @@ function UI.DropMenu.example()
{ spacer = true }, { spacer = true },
{ text = 'Quit ^q', event = 'quit' }, { text = 'Quit ^q', event = 'quit' },
} }, } },
{ text = 'Edit', dropdown = {
{ text = 'Copy', event = 'run' },
{ text = 'Paste s', event = 'shell' },
} },
{ text = '\187',
x = -3,
dropdown = {
{ text = 'Associations', event = 'associate' },
} },
} }
} }
end end

View File

@ -1,20 +1,18 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.DropMenuItem = class(UI.Button) UI.DropMenuItem = class(UI.Button)
UI.DropMenuItem.defaults = { UI.DropMenuItem.defaults = {
UIElement = 'DropMenuItem', UIElement = 'DropMenuItem',
textColor = colors.black, textColor = 'black',
backgroundColor = colors.white, backgroundColor = 'white',
textFocusColor = colors.white, textFocusColor = 'white',
textInactiveColor = colors.lightGray, textInactiveColor = 'lightGray',
backgroundFocusColor = colors.lightGray, backgroundFocusColor = 'lightGray',
} }
function UI.DropMenuItem:eventHandler(event) function UI.DropMenuItem:eventHandler(event)
if event.type == 'button_activate' then if event.type == 'button_activate' then
self.parent:hide() self.parent:disable()
end end
return UI.Button.eventHandler(self, event) return UI.Button.eventHandler(self, event)
end end

View File

@ -1,62 +1,63 @@
local class = require('opus.class') local class = require('opus.class')
local Event = require('opus.event')
local Terminal = require('opus.terminal') local Terminal = require('opus.terminal')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Embedded = class(UI.Window) UI.Embedded = class(UI.Window)
UI.Embedded.defaults = { UI.Embedded.defaults = {
UIElement = 'Embedded', UIElement = 'Embedded',
backgroundColor = colors.black, backgroundColor = 'black',
textColor = colors.white, textColor = 'white',
maxScroll = 100, maxScroll = 100,
accelerators = { accelerators = {
up = 'scroll_up', up = 'scroll_up',
down = 'scroll_down', down = 'scroll_down',
} }
} }
function UI.Embedded:setParent()
UI.Window.setParent(self)
self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false)
self.win.setMaxScroll(self.maxScroll)
local canvas = self:getCanvas()
self.win.getCanvas().parent = canvas
table.insert(canvas.layers, self.win.getCanvas())
self.canvas = self.win.getCanvas()
self.win.setCursorPos(1, 1)
self.win.setBackgroundColor(self.backgroundColor)
self.win.setTextColor(self.textColor)
self.win.clear()
end
function UI.Embedded:layout() function UI.Embedded:layout()
UI.Window.layout(self) UI.Window.layout(self)
if self.win then
self.win.reposition(self.x, self.y, self.width, self.height) if not self.win then
local t
function self.render()
if not t then
t = Event.onTimeout(0, function()
t = nil
if self.focused then
self:setCursorPos(self.win.getCursorPos())
end
self:sync()
end)
end
end
self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false)
self.win.canvas = self
self.win.setMaxScroll(self.maxScroll)
self.win.setCursorPos(1, 1)
self.win.setBackgroundColor(self.backgroundColor)
self.win.setTextColor(self.textColor)
self.win.clear()
end end
end end
function UI.Embedded:draw() function UI.Embedded:draw()
self.canvas:dirty() self:dirty()
end
function UI.Embedded:focus()
-- allow scrolling
if self.focused then
self:setCursorBlink(self.win.getCursorBlink())
end
end end
function UI.Embedded:enable() function UI.Embedded:enable()
self.canvas:setVisible(true)
self.canvas:raise()
if self.visible then
-- the window will automatically update on changes
-- the canvas does not need to be rendereed
self.win.setVisible(true)
end
UI.Window.enable(self) UI.Window.enable(self)
self.canvas:dirty() self.win.setVisible(true)
self:dirty()
end end
function UI.Embedded:disable() function UI.Embedded:disable()
self.canvas:setVisible(false)
self.win.setVisible(false) self.win.setVisible(false)
UI.Window.disable(self) UI.Window.disable(self)
end end
@ -71,17 +72,12 @@ function UI.Embedded:eventHandler(event)
end end
end end
function UI.Embedded:focus()
-- allow scrolling
end
function UI.Embedded.example() function UI.Embedded.example()
local Event = require('opus.event')
local Util = require('opus.util') local Util = require('opus.util')
local term = _G.term local term = _G.term
return UI.Embedded { return UI.Embedded {
visible = true, y = 2, x = 2, ex = -2, ey = -2,
enable = function (self) enable = function (self)
UI.Embedded.enable(self) UI.Embedded.enable(self)
Event.addRoutine(function() Event.addRoutine(function()
@ -90,10 +86,11 @@ function UI.Embedded.example()
term.redirect(oterm) term.redirect(oterm)
end) end)
end, end,
eventHandler = function(_, event) eventHandler = function(self, event)
if event.type == 'key' then if event.type == 'key' then
return true return true
end end
return UI.Embedded.eventHandler(self, event)
end end
} }
end end

View File

@ -0,0 +1,118 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local fs = _G.fs
UI.FileSelect = class(UI.Window)
UI.FileSelect.defaults = {
UIElement = 'FileSelect',
}
function UI.FileSelect:postInit()
self.grid = UI.ScrollingGrid {
x = 2, y = 2, ex = -2, ey = -4,
dir = '/',
sortColumn = 'name',
columns = {
{ heading = 'Name', key = 'name' },
{ heading = 'Size', key = 'size', width = 5 }
},
getDisplayValues = function(_, row)
if row.size then
row = Util.shallowCopy(row)
row.size = Util.toBytes(row.size)
end
return row
end,
getRowTextColor = function(_, file)
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end,
sortCompare = function(self, a, b)
if self.sortColumn == 'size' then
return a.size < b.size
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end,
draw = function(self)
local files = fs.listEx(self.dir)
if #self.dir > 0 then
table.insert(files, {
name = '..',
isDir = true,
})
end
self:setValues(files)
self:setIndex(1)
UI.Grid.draw(self)
end,
}
self.path = UI.TextEntry {
x = 2,
y = -2,
ex = -11,
limit = 256,
accelerators = {
enter = 'path_enter',
}
}
self.cancel = UI.Button {
text = 'Cancel',
x = -9,
y = -2,
event = 'select_cancel',
}
end
function UI.FileSelect:draw()
self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray)
self:drawChildren()
end
function UI.FileSelect:enable(path)
self:setPath(path or '')
UI.Window.enable(self)
end
function UI.FileSelect:setPath(path)
self.grid.dir = path
while not fs.isDir(self.grid.dir) do
self.grid.dir = fs.getDir(self.grid.dir)
end
self.path.value = self.grid.dir
end
function UI.FileSelect:eventHandler(event)
if event.type == 'grid_select' then
self.grid.dir = fs.combine(self.grid.dir, event.selected.name)
self.path.value = self.grid.dir
if event.selected.isDir then
self.grid:draw()
self.path:draw()
else
self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self })
end
return true
elseif event.type == 'path_enter' then
if self.path.value then
if fs.isDir(self.path.value) then
self:setPath(self.path.value)
self.grid:draw()
self.path:draw()
else
self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self })
end
end
return true
end
end

View File

@ -0,0 +1,16 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.FlatButton = class(UI.Button)
UI.FlatButton.defaults = {
UIElement = 'FlatButton',
textColor = 'black',
textFocusColor = 'white',
noPadding = true,
}
function UI.FlatButton:setParent()
self.backgroundColor = self.parent:getProperty('backgroundColor')
self.backgroundFocusColor = self.backgroundColor
UI.Button.setParent(self)
end

View File

@ -2,8 +2,6 @@ local class = require('opus.class')
local Sound = require('opus.sound') local Sound = require('opus.sound')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Form = class(UI.Window) UI.Form = class(UI.Window)
UI.Form.defaults = { UI.Form.defaults = {
UIElement = 'Form', UIElement = 'Form',
@ -68,7 +66,7 @@ function UI.Form:createForm()
table.insert(self.children, UI.Text { table.insert(self.children, UI.Text {
x = self.margin, x = self.margin,
y = child.y, y = child.y,
textColor = colors.black, textColor = 'black',
width = #child.formLabel, width = #child.formLabel,
value = child.formLabel, value = child.formLabel,
}) })

View File

@ -2,10 +2,8 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local os = _G.os local os = _G.os
local _rep = string.rep local _rep = string.rep
local _sub = string.sub
local function safeValue(v) local function safeValue(v)
local t = type(v) local t = type(v)
@ -23,18 +21,7 @@ function Writer:init(element, y)
end end
function Writer:write(s, width, align, bg, fg) function Writer:write(s, width, align, bg, fg)
local len = #tostring(s or '') s = Util.widthify(s, width, align)
if len > width then
s = _sub(s, 1, width)
end
local padding = len < width and _rep(' ', width - len)
if padding then
if align == 'right' then
s = padding .. s
else
s = s .. padding
end
end
self.element:write(self.x, self.y, s, bg, fg) self.element:write(self.x, self.y, s, bg, fg)
self.x = self.x + width self.x = self.x + width
end end
@ -56,16 +43,16 @@ UI.Grid.defaults = {
disableHeader = false, disableHeader = false,
headerHeight = 1, headerHeight = 1,
marginRight = 0, marginRight = 0,
textColor = colors.white, textColor = 'white',
textSelectedColor = colors.white, textSelectedColor = 'white',
backgroundColor = colors.black, backgroundColor = 'black',
backgroundSelectedColor = colors.gray, backgroundSelectedColor = 'gray',
headerBackgroundColor = colors.cyan, headerBackgroundColor = 'primary',
headerTextColor = colors.white, headerTextColor = 'white',
headerSortColor = colors.yellow, headerSortColor = 'yellow',
unfocusedTextSelectedColor = colors.white, unfocusedTextSelectedColor = 'white',
unfocusedBackgroundSelectedColor = colors.gray, unfocusedBackgroundSelectedColor = 'gray',
focusIndicator = UI.extChars and '\183' or '>', focusIndicator = UI.extChars and '\26' or '>',
sortIndicator = ' ', sortIndicator = ' ',
inverseSortIndicator = UI.extChars and '\24' or '^', inverseSortIndicator = UI.extChars and '\24' or '^',
values = { }, values = { },
@ -83,8 +70,8 @@ UI.Grid.defaults = {
[ 'control-f' ] = 'scroll_pageDown', [ 'control-f' ] = 'scroll_pageDown',
}, },
} }
function UI.Grid:setParent() function UI.Grid:layout()
UI.Window.setParent(self) UI.Window.layout(self)
for _,c in pairs(self.columns) do for _,c in pairs(self.columns) do
c.cw = c.width c.cw = c.width
@ -522,7 +509,7 @@ function UI.Grid.example()
values = values, values = values,
columns = { columns = {
{ heading = 'key', key = 'key', width = 6, }, { heading = 'key', key = 'key', width = 6, },
{ heading = 'value', key = 'value', textColor = colors.yellow }, { heading = 'value', key = 'value', textColor = 'yellow' },
}, },
}, },
autospace = UI.Grid { autospace = UI.Grid {

View File

@ -1,19 +1,28 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util')
local lookup = '0123456789abcdef'
-- handle files produced by Paint
UI.Image = class(UI.Window) UI.Image = class(UI.Window)
UI.Image.defaults = { UI.Image.defaults = {
UIElement = 'Image', UIElement = 'Image',
event = 'button_press', event = 'button_press',
} }
function UI.Image:setParent() function UI.Image:postInit()
if self.image then if self.filename then
self.image = Util.readLines(self.filename)
end
if self.image and not (self.height or self.ey) then
self.height = #self.image self.height = #self.image
end end
if self.image and not self.width then if self.image and not (self.width or self.ex) then
self.width = #self.image[1] for i = 1, self.height do
self.width = math.max(self.width or 0, #self.image[i])
end
end end
UI.Window.setParent(self)
end end
function UI.Image:draw() function UI.Image:draw()
@ -22,19 +31,22 @@ function UI.Image:draw()
for y = 1, #self.image do for y = 1, #self.image do
local line = self.image[y] local line = self.image[y]
for x = 1, #line do for x = 1, #line do
local ch = line[x] local ch = lookup:find(line:sub(x, x))
if type(ch) == 'number' then if ch then
if ch > 0 then self:write(x, y, ' ', 2 ^ (ch -1))
self:write(x, y, ' ', ch)
end
else
self:write(x, y, ch)
end end
end end
end end
end end
self:drawChildren()
end end
function UI.Image:setImage(image) function UI.Image:setImage(image)
self.image = image self.image = image
end end
function UI.Image.example()
return UI.Image {
filename = 'test.paint',
}
end

View File

@ -13,8 +13,7 @@ function UI.Menu:postInit()
self.pageSize = #self.menuItems self.pageSize = #self.menuItems
end end
function UI.Menu:setParent() function UI.Menu:layout()
UI.Grid.setParent(self)
self.itemWidth = 1 self.itemWidth = 1
for _,v in pairs(self.values) do for _,v in pairs(self.values) do
if #v.prompt > self.itemWidth then if #v.prompt > self.itemWidth then
@ -28,6 +27,7 @@ function UI.Menu:setParent()
else else
self.width = self.itemWidth + 2 self.width = self.itemWidth + 2
end end
UI.Grid.layout(self)
end end
function UI.Menu:center() function UI.Menu:center()

View File

@ -1,28 +1,15 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
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
UI.MenuBar = class(UI.Window) UI.MenuBar = class(UI.Window)
UI.MenuBar.defaults = { UI.MenuBar.defaults = {
UIElement = 'MenuBar', UIElement = 'MenuBar',
buttons = { }, buttons = { },
height = 1, height = 1,
backgroundColor = colors.lightGray, backgroundColor = 'secondary',
textColor = colors.black, textColor = 'black',
spacing = 2, spacing = 2,
lastx = 1, lastx = 1,
showBackButton = false,
buttonClass = 'MenuItem', buttonClass = 'MenuItem',
} }
function UI.MenuBar:postInit() function UI.MenuBar:postInit()
@ -62,10 +49,6 @@ function UI.MenuBar:addButtons(buttons)
else else
table.insert(self.children, button) table.insert(self.children, button)
end end
if button.dropdown then
button.dropmenu = UI.DropMenu { buttons = button.dropdown }
end
end end
end end
if self.parent then if self.parent then
@ -78,23 +61,28 @@ function UI.MenuBar:getActive(menuItem)
end end
function UI.MenuBar:eventHandler(event) function UI.MenuBar:eventHandler(event)
if event.type == 'button_press' and event.button.dropmenu then if event.type == 'button_press' and event.button.dropdown then
if event.button.dropmenu.enabled then local function getPosition(element)
event.button.dropmenu:hide() local x, y = 1, 1
self:refocus() repeat
return true x = element.x + x - 1
else y = element.y + y - 1
local x, y = getPosition(event.button) element = element.parent
if x + event.button.dropmenu.width > self.width then until not element
x = self.width - event.button.dropmenu.width + 1 return x, y
end
for _,c in pairs(event.button.dropmenu.children) do
if not c.spacer then
c.inactive = not self:getActive(c)
end
end
event.button.dropmenu:show(x, y + 1)
end end
local x, y = getPosition(event.button)
local menu = UI.DropMenu {
buttons = event.button.dropdown,
x = x,
y = y + 1,
lastFocus = event.button.uid,
menuUid = self.uid,
}
self.parent:add({ dropmenu = menu })
return true return true
end end
end end
@ -103,7 +91,8 @@ function UI.MenuBar.example()
return UI.MenuBar { return UI.MenuBar {
buttons = { buttons = {
{ text = 'Choice1', event = 'event1' }, { text = 'Choice1', event = 'event1' },
{ text = 'Choice2', event = 'event2' }, { text = 'Choice2', event = 'event2', inactive = true },
{ text = 'Choice3', event = 'event3' },
} }
} }
end end

View File

@ -1,13 +1,9 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors UI.MenuItem = class(UI.FlatButton)
UI.MenuItem = class(UI.Button)
UI.MenuItem.defaults = { UI.MenuItem.defaults = {
UIElement = 'MenuItem', UIElement = 'MenuItem',
textColor = colors.black, noPadding = false,
backgroundColor = colors.lightGray, textInactiveColor = 'gray',
textFocusColor = colors.white,
backgroundFocusColor = colors.lightGray,
} }

View File

@ -0,0 +1,31 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.MiniSlideOut = class(UI.SlideOut)
UI.MiniSlideOut.defaults = {
UIElement = 'MiniSlideOut',
noFill = true,
backgroundColor = 'primary',
height = 1,
}
function UI.MiniSlideOut:postInit()
self.close_button = UI.Button {
x = -1,
backgroundColor = self.backgroundColor,
backgroundFocusColor = self.backgroundColor,
text = 'x',
event = 'slide_hide',
noPadding = true,
}
if self.label then
self.label_text = UI.Text {
x = 2,
value = self.label,
}
end
end
function UI.MiniSlideOut:show(...)
UI.SlideOut.show(self, ...)
self:addTransition('slideLeft', { easing = 'outBounce' })
end

View File

@ -5,17 +5,18 @@ UI.NftImage = class(UI.Window)
UI.NftImage.defaults = { UI.NftImage.defaults = {
UIElement = 'NftImage', UIElement = 'NftImage',
} }
function UI.NftImage:setParent() function UI.NftImage:postInit()
if self.image then if self.image and not (self.ey or self.height) then
self.height = self.image.height self.height = self.image.height
end end
if self.image and not self.width then if self.image and not (self.ex or self.width) then
self.width = self.image.width self.width = self.image.width
end end
UI.Window.setParent(self)
end end
function UI.NftImage:draw() function UI.NftImage:draw()
self:clear()
if self.image then if self.image then
-- due to blittle, the background and foreground transparent -- due to blittle, the background and foreground transparent
-- color is the same as the background color -- color is the same as the background color
@ -25,8 +26,6 @@ function UI.NftImage:draw()
self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg) self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg)
end end
end end
else
self:clear()
end end
end end

View File

@ -4,36 +4,34 @@ local Sound = require('opus.sound')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.Notification = class(UI.Window) UI.Notification = class(UI.Window)
UI.Notification.defaults = { UI.Notification.defaults = {
UIElement = 'Notification', UIElement = 'Notification',
backgroundColor = colors.gray, backgroundColor = 'gray',
closeInd = UI.extChars and '\215' or '*', closeInd = UI.extChars and '\215' or '*',
height = 3, height = 3,
timeout = 3, timeout = 3,
anchor = 'bottom', anchor = 'bottom',
} }
function UI.Notification:draw() function UI.Notification.draw()
end end
function UI.Notification:enable() function UI.Notification.enable()
end end
function UI.Notification:error(value, timeout) function UI.Notification:error(value, timeout)
self.backgroundColor = colors.red self.backgroundColor = 'red'
Sound.play('entity.villager.no', .5) Sound.play('entity.villager.no', .5)
self:display(value, timeout) self:display(value, timeout)
end end
function UI.Notification:info(value, timeout) function UI.Notification:info(value, timeout)
self.backgroundColor = colors.lightGray self.backgroundColor = 'lightGray'
self:display(value, timeout) self:display(value, timeout)
end end
function UI.Notification:success(value, timeout) function UI.Notification:success(value, timeout)
self.backgroundColor = colors.green self.backgroundColor = 'green'
self:display(value, timeout) self:display(value, timeout)
end end
@ -43,32 +41,34 @@ function UI.Notification:cancel()
self.timer = nil self.timer = nil
end end
if self.canvas then self:disable()
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
end
end end
function UI.Notification:display(value, timeout) function UI.Notification:display(value, timeout)
self:cancel()
self.enabled = true
local lines = Util.wordWrap(value, self.width - 3) local lines = Util.wordWrap(value, self.width - 3)
self.enabled = true
self.height = #lines self.height = #lines
if self.anchor == 'bottom' then if self.anchor == 'bottom' then
self.y = self.parent.height - self.height + 1 self.y = self.parent.height - self.height + 1
self.canvas = self:addLayer(self.backgroundColor, self.textColor)
self:addTransition('expandUp', { ticks = self.height }) self:addTransition('expandUp', { ticks = self.height })
else else
self.canvas = self:addLayer(self.backgroundColor, self.textColor)
self.y = 1 self.y = 1
end end
self.canvas:setVisible(true)
self:reposition(self.x, self.y, self.width, self.height)
self:raise()
self:clear() self:clear()
for k,v in pairs(lines) do for k,v in pairs(lines) do
self:write(2, k, v) self:write(2, k, v)
end end
self:write(self.width, 1, self.closeInd)
if self.timer then
Event.off(self.timer)
self.timer = nil
end
timeout = timeout or self.timeout timeout = timeout or self.timeout
if timeout > 0 then if timeout > 0 then
@ -77,7 +77,6 @@ function UI.Notification:display(value, timeout)
self:sync() self:sync()
end) end)
else else
self:write(self.width, 1, self.closeInd)
self:sync() self:sync()
end end
end end
@ -92,7 +91,7 @@ function UI.Notification:eventHandler(event)
end end
function UI.Notification.example() function UI.Notification.example()
return UI.ActiveLayer { return UI.Window {
notify1 = UI.Notification { notify1 = UI.Notification {
anchor = 'top', anchor = 'top',
}, },
@ -111,7 +110,9 @@ function UI.Notification.example()
if event.type == 'test_success' then if event.type == 'test_success' then
self.notify1:success('Example text') self.notify1:success('Example text')
elseif event.type == 'test_error' then elseif event.type == 'test_error' then
self.notify2:error('Example text', 0) self.notify2:error([[Example text test test
test test test test test
test test test]], 0)
end end
end, end,
} }

View File

@ -1,60 +1,31 @@
local Canvas = require('opus.ui.canvas')
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
-- 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
UI.Page = class(UI.Window) UI.Page = class(UI.Window)
UI.Page.defaults = { UI.Page.defaults = {
UIElement = 'Page', UIElement = 'Page',
accelerators = { accelerators = {
down = 'focus_next', down = 'focus_next',
scroll_down = 'focus_next',
enter = 'focus_next', enter = 'focus_next',
tab = 'focus_next', tab = 'focus_next',
['shift-tab' ] = 'focus_prev', ['shift-tab' ] = 'focus_prev',
up = 'focus_prev', up = 'focus_prev',
scroll_up = 'focus_prev',
}, },
backgroundColor = colors.cyan, backgroundColor = 'primary',
textColor = colors.white, textColor = 'white',
} }
function UI.Page:postInit() function UI.Page:postInit()
self.parent = self.parent or UI.defaultDevice self.parent = self.parent or UI.term
self.__target = self self.__target = self
self.canvas = Canvas({
x = 1, y = 1, width = self.parent.width, height = self.parent.height,
isColor = self.parent.isColor,
})
self.canvas:clear(self.backgroundColor, self.textColor)
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()
self.canvas.visible = false
UI.Window.disable(self)
end end
function UI.Page:sync() function UI.Page:sync()
if self.enabled then if self.enabled then
self:checkFocus()
self.parent:setCursorBlink(self.focused and self.focused.cursorBlink)
self.parent:sync() self.parent:sync()
end end
end end
@ -73,22 +44,23 @@ function UI.Page:pointToChild(x, y)
if self.__target == self then if self.__target == self then
return UI.Window.pointToChild(self, x, y) return UI.Window.pointToChild(self, x, y)
end end
x = x + self.offx - self.x + 1
y = y + self.offy - self.y + 1 local function getPosition(element)
--[[ local x, y = 1, 1
-- this is supposed to fix when there are multiple sub canvases repeat
local absX, absY = getPosition(self.__target) x = element.x + x - 1
if self.__target.canvas then y = element.y + y - 1
x = x - (self.__target.canvas.x - self.__target.x) element = element.parent
y = y - (self.__target.canvas.y - self.__target.y) until not element
_syslog({'raw', self.__target.canvas.y, self.__target.y}) return x, y
end end
]]
return self.__target:pointToChild(x, y) local absX, absY = getPosition(self.__target)
return self.__target:pointToChild(x - absX + self.__target.x, y - absY + self.__target.y)
end end
function UI.Page:getFocusables() function UI.Page:getFocusables()
if self.__target == self or self.__target.pageType ~= 'modal' then if self.__target == self or not self.__target.modal then
return UI.Window.getFocusables(self) return UI.Window.getFocusables(self)
end end
return self.__target:getFocusables() return self.__target:getFocusables()
@ -149,12 +121,17 @@ function UI.Page:setFocus(child)
if not child.focused then if not child.focused then
child.focused = true child.focused = true
child:emit({ type = 'focus_change', focused = child }) child:emit({ type = 'focus_change', focused = child })
--self:emit({ type = 'focus_change', focused = child })
end end
child:focus() child:focus()
end end
function UI.Page:checkFocus()
if not self.focused or not self.focused.enabled then
self.__target:focusFirst()
end
end
function UI.Page:eventHandler(event) function UI.Page:eventHandler(event)
if self.focused then if self.focused then
if event.type == 'focus_next' then if event.type == 'focus_next' then

View File

@ -1,39 +1,31 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.ProgressBar = class(UI.Window) UI.ProgressBar = class(UI.Window)
UI.ProgressBar.defaults = { UI.ProgressBar.defaults = {
UIElement = 'ProgressBar', UIElement = 'ProgressBar',
backgroundColor = colors.gray, backgroundColor = 'gray',
height = 1, height = 1,
progressColor = colors.lime, progressColor = 'lime',
progressChar = UI.extChars and '\153' or ' ', progressChar = UI.extChars and '\153' or ' ',
fillChar = ' ', fillChar = ' ',
fillColor = colors.gray, fillColor = 'gray',
textColor = colors.green, textColor = 'green',
value = 0, value = 0,
} }
function UI.ProgressBar:draw() function UI.ProgressBar:draw()
local width = math.ceil(self.value / 100 * self.width) local width = math.ceil(self.value / 100 * self.width)
local filler = string.rep(self.fillChar, self.width) self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor)
local progress = string.rep(self.progressChar, width) self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor)
for i = 1, self.height do
self:write(1, i, filler, nil, self.fillColor)
self:write(1, i, progress, self.progressColor)
end
end end
function UI.ProgressBar.example() function UI.ProgressBar.example()
local Event = require('opus.event')
return UI.ProgressBar { return UI.ProgressBar {
x = 2, ex = -2, y = 2, x = 2, ex = -2, y = 2, height = 2,
focus = function() end, focus = function() end,
enable = function(self) enable = function(self)
Event.onInterval(.25, function() require('opus.event').onInterval(.25, function()
self.value = self.value == 100 and 0 or self.value + 5 self.value = self.value == 100 and 0 or self.value + 5
self:draw() self:draw()
self:sync() self:sync()

View File

@ -0,0 +1,27 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.Question = class(UI.MiniSlideOut)
UI.Question.defaults = {
UIElement = 'Question',
accelerators = {
y = 'question_yes',
n = 'question_no',
}
}
function UI.Question:postInit()
local x = self.label and #self.label + 3 or 1
self.yes_button = UI.Button {
x = x,
text = 'Yes',
backgroundColor = 'primary',
event = 'question_yes',
}
self.no_button = UI.Button {
x = x + 5,
text = 'No',
backgroundColor = 'primary',
event = 'question_no',
}
end

View File

@ -17,7 +17,13 @@ UI.ScrollBar.defaults = {
ey = -1, ey = -1,
} }
function UI.ScrollBar:draw() function UI.ScrollBar:draw()
local view = self.parent:getViewArea() local parent = self.target or self.parent --self:find(self.target)
local view = parent:getViewArea()
self:clear()
-- ...
self:write(1, 1, ' ', view.fill)
if view.totalHeight > view.height then if view.totalHeight > view.height then
local maxScroll = view.totalHeight - view.height local maxScroll = view.totalHeight - view.height
@ -27,7 +33,7 @@ function UI.ScrollBar:draw()
local row = view.y local row = view.y
if not view.static then -- does the container scroll ? if not view.static then -- does the container scroll ?
self.height = view.totalHeight self:reposition(self.x, self.y, self.width, view.totalHeight)
end end
for i = 1, view.height - 2 do for i = 1, view.height - 2 do
@ -56,16 +62,17 @@ end
function UI.ScrollBar:eventHandler(event) function UI.ScrollBar:eventHandler(event)
if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
if event.x == 1 then if event.x == 1 then
local view = self.parent:getViewArea() local parent = self.target or self.parent --self:find(self.target)
local view = parent:getViewArea()
if view.totalHeight > view.height then if view.totalHeight > view.height then
if event.y == view.y then if event.y == view.y then
self:emit({ type = 'scroll_up'}) parent:emit({ type = 'scroll_up'})
elseif event.y == view.y + view.height - 1 then elseif event.y == view.y + view.height - 1 then
self:emit({ type = 'scroll_down'}) parent:emit({ type = 'scroll_down'})
else else
local percent = (event.y - view.y) / (view.height - 2) local percent = (event.y - view.y) / (view.height - 2)
local y = math.floor((view.totalHeight - view.height) * percent) local y = math.floor((view.totalHeight - view.height) * percent)
self:emit({ type = 'scroll_to', offset = y }) parent :emit({ type = 'scroll_to', offset = y })
end end
end end
return true return true

View File

@ -29,6 +29,7 @@ function UI.ScrollingGrid:getViewArea()
height = self.pageSize, -- viewable height height = self.pageSize, -- viewable height
totalHeight = Util.size(self.values), -- total height totalHeight = Util.size(self.values), -- total height
offsetY = self.scrollOffset, -- scroll offset offsetY = self.scrollOffset, -- scroll offset
fill = not self.disableHeader and self.headerBackgroundColor,
} }
end end
@ -57,3 +58,21 @@ function UI.ScrollingGrid:setIndex(index)
end end
UI.Grid.setIndex(self, index) UI.Grid.setIndex(self, index)
end end
function UI.ScrollingGrid.example()
local values = { }
for i = 1, 20 do
table.insert(values, { key = 'key' .. i, value = 'value' .. i })
end
return UI.ScrollingGrid {
values = values,
sortColumn = 'key',
columns = {
{ heading = 'key', key = 'key' },
{ heading = 'value', key = 'value' },
},
accelerators = {
grid_select = 'custom_select',
}
}
end

View File

@ -4,17 +4,9 @@ local UI = require('opus.ui')
UI.SlideOut = class(UI.Window) UI.SlideOut = class(UI.Window)
UI.SlideOut.defaults = { UI.SlideOut.defaults = {
UIElement = 'SlideOut', UIElement = 'SlideOut',
pageType = 'modal', transitionHint = 'expandUp',
modal = true,
} }
function UI.SlideOut:layout()
UI.Window.layout(self)
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.SlideOut:enable() function UI.SlideOut:enable()
end end
@ -27,24 +19,20 @@ function UI.SlideOut:toggle()
end end
function UI.SlideOut:show(...) function UI.SlideOut:show(...)
self:addTransition('expandUp')
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...) UI.Window.enable(self, ...)
self:draw() self:draw()
self:capture(self)
self:focusFirst() self:focusFirst()
end end
function UI.SlideOut:disable()
self.canvas:setVisible(false)
UI.Window.disable(self)
end
function UI.SlideOut:hide() function UI.SlideOut:hide()
self:disable() self:disable()
self:release(self) end
self:refocus()
function UI.SlideOut:draw()
if not self.noFill then
self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), 'black', 'gray')
end
self:drawChildren()
end end
function UI.SlideOut:eventHandler(event) function UI.SlideOut:eventHandler(event)
@ -59,24 +47,27 @@ function UI.SlideOut:eventHandler(event)
end end
function UI.SlideOut.example() function UI.SlideOut.example()
-- for the transistion to work properly, the parent must have a canvas return UI.Window {
return UI.ActiveLayer { y = 3,
y = 2, backgroundColor = 2048,
button = UI.Button { button = UI.Button {
x = 2, y = 5, x = 2, y = 5,
text = 'show', text = 'show',
}, },
slideOut = UI.SlideOut { slideOut = UI.SlideOut {
backgroundColor = _G.colors.yellow, backgroundColor = 16,
y = -4, height = 4, x = 3, ex = -3, y = -7, height = 4, x = 3, ex = -3,
titleBar = UI.TitleBar {
title = 'test',
},
button = UI.Button { button = UI.Button {
x = 2, y = 2, x = 2, y = 2,
text = 'hide', text = 'hide',
--visualize = true,
}, },
}, },
eventHandler = function (self, event) eventHandler = function (self, event)
if event.type == 'button_press' then if event.type == 'button_press' then
self.slideOut.canvas.xxx = true
self.slideOut:toggle() self.slideOut:toggle()
end end
end, end,

View File

@ -2,17 +2,15 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.Slider = class(UI.Window) UI.Slider = class(UI.Window)
UI.Slider.defaults = { UI.Slider.defaults = {
UIElement = 'Slider', UIElement = 'Slider',
height = 1, height = 1,
barChar = UI.extChars and '\140' or '-', barChar = UI.extChars and '\140' or '-',
barColor = colors.gray, barColor = 'gray',
sliderChar = UI.extChars and '\143' or '\124', sliderChar = UI.extChars and '\143' or '\124',
sliderColor = colors.blue, sliderColor = 'blue',
sliderFocusColor = colors.lightBlue, sliderFocusColor = 'lightBlue',
leftBorder = UI.extChars and '\141' or '\124', leftBorder = UI.extChars and '\141' or '\124',
rightBorder = UI.extChars and '\142' or '\124', rightBorder = UI.extChars and '\142' or '\124',
value = 0, value = 0,
@ -57,8 +55,16 @@ end
function UI.Slider:eventHandler(event) function UI.Slider:eventHandler(event)
if event.type == "mouse_down" or event.type == "mouse_drag" then if event.type == "mouse_down" or event.type == "mouse_drag" then
local pos = event.x - 1
if event.type == 'mouse_down' then
self.anchor = event.x - 1
else
pos = self.anchor + event.dx
end
local range = self.max - self.min local range = self.max - self.min
local i = (event.x - 1) / (self.width - 1) local i = pos / (self.width - 1)
self.value = self.min + (i * range) self.value = self.min + (i * range)
self:emit({ type = self.event, value = self.value, element = self }) self:emit({ type = self.event, value = self.value, element = self })
self:draw() self:draw()

View File

@ -3,17 +3,16 @@ local Event = require('opus.event')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.StatusBar = class(UI.Window) UI.StatusBar = class(UI.Window)
UI.StatusBar.defaults = { UI.StatusBar.defaults = {
UIElement = 'StatusBar', UIElement = 'StatusBar',
backgroundColor = colors.lightGray, backgroundColor = 'lightGray',
textColor = colors.gray, textColor = 'gray',
height = 1, height = 1,
ey = -1, ey = -1,
} }
function UI.StatusBar:adjustWidth() function UI.StatusBar:layout()
UI.Window.layout(self)
-- Can only have 1 adjustable width -- Can only have 1 adjustable width
if self.columns then if self.columns then
local w = self.width - #self.columns - 1 local w = self.width - #self.columns - 1
@ -31,16 +30,6 @@ function UI.StatusBar:adjustWidth()
end end
end end
function UI.StatusBar:resize()
UI.Window.resize(self)
self:adjustWidth()
end
function UI.StatusBar:setParent()
UI.Window.setParent(self)
self:adjustWidth()
end
function UI.StatusBar:setStatus(status) function UI.StatusBar:setStatus(status)
if self.values ~= status then if self.values ~= status then
self.values = status self.values = status
@ -63,7 +52,7 @@ end
function UI.StatusBar:timedStatus(status, timeout) function UI.StatusBar:timedStatus(status, timeout)
self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor)
Event.on(timeout or 3, function() Event.onTimeout(timeout or 3, function()
if self.enabled then if self.enabled then
self:draw() self:draw()
self:sync() self:sync()
@ -89,11 +78,13 @@ function UI.StatusBar:draw()
elseif type(self.values) == 'string' then elseif type(self.values) == 'string' then
self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) self:write(1, 1, Util.widthify(' ' .. self.values, self.width))
else else
local s = '' local x = 2
self:clear()
for _,c in ipairs(self.columns) do for _,c in ipairs(self.columns) do
s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) local s = Util.widthify(tostring(self.values[c.key] or ''), c.cw)
self:write(x, 1, s, c.bg, c.fg)
x = x + c.cw + 1
end end
self:write(1, 1, Util.widthify(s, self.width))
end end
end end

View File

@ -1,9 +1,16 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
UI.Tab = class(UI.ActiveLayer) UI.Tab = class(UI.Window)
UI.Tab.defaults = { UI.Tab.defaults = {
UIElement = 'Tab', UIElement = 'Tab',
tabTitle = 'tab', tabTitle = 'tab',
y = 2, y = 2,
} }
function UI.Tab:draw()
if not self.noFill then
self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray)
end
self:drawChildren()
end

View File

@ -6,9 +6,9 @@ UI.TabBar = class(UI.MenuBar)
UI.TabBar.defaults = { UI.TabBar.defaults = {
UIElement = 'TabBar', UIElement = 'TabBar',
buttonClass = 'TabBarMenuItem', buttonClass = 'TabBarMenuItem',
} backgroundColor = 'black',
UI.TabBar.inherits = { selectedBackgroundColor = 'primary',
selectedBackgroundColor = 'backgroundColor', unselectedBackgroundColor = 'tertiary',
} }
function UI.TabBar:enable() function UI.TabBar:enable()
UI.MenuBar.enable(self) UI.MenuBar.enable(self)
@ -32,7 +32,7 @@ function UI.TabBar:eventHandler(event)
self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) self:emit({ type = 'tab_change', current = si, last = pi, tab = selected })
end end
end end
UI.MenuBar.draw(self) self:draw(self)
end end
return UI.MenuBar.eventHandler(self, event) return UI.MenuBar.eventHandler(self, event)
end end

View File

@ -1,27 +1,19 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.TabBarMenuItem = class(UI.Button) UI.TabBarMenuItem = class(UI.Button)
UI.TabBarMenuItem.defaults = { UI.TabBarMenuItem.defaults = {
UIElement = 'TabBarMenuItem', UIElement = 'TabBarMenuItem',
event = 'tab_select', event = 'tab_select',
textColor = colors.black, textInactiveColor = 'lightGray',
selectedBackgroundColor = colors.cyan,
unselectedBackgroundColor = colors.lightGray,
backgroundColor = colors.lightGray,
}
UI.TabBarMenuItem.inherits = {
selectedBackgroundColor = 'selectedBackgroundColor',
} }
function UI.TabBarMenuItem:draw() function UI.TabBarMenuItem:draw()
if self.selected then if self.selected then
self.backgroundColor = self.selectedBackgroundColor self.backgroundColor = self:getProperty('selectedBackgroundColor')
self.backgroundFocusColor = self.selectedBackgroundColor self.backgroundFocusColor = self.backgroundColor
else else
self.backgroundColor = self.unselectedBackgroundColor self.backgroundColor = self:getProperty('unselectedBackgroundColor')
self.backgroundFocusColor = self.unselectedBackgroundColor self.backgroundFocusColor = self.backgroundColor
end end
UI.Button.draw(self) UI.Button.draw(self)
end end

View File

@ -56,12 +56,12 @@ end
function UI.Tabs:enable() function UI.Tabs:enable()
self.enabled = true self.enabled = true
self.transitionHint = nil
self.tabBar:enable() self.tabBar:enable()
local menuItem = Util.find(self.tabBar.children, 'selected', true) local menuItem = Util.find(self.tabBar.children, 'selected', true)
for _,child in pairs(self.children or { }) do for child in self:eachChild() do
child.transitionHint = nil
if child.uid == menuItem.tabUid then if child.uid == menuItem.tabUid then
child:enable() child:enable()
self:emit({ type = 'tab_activate', activated = child }) self:emit({ type = 'tab_activate', activated = child })
@ -74,14 +74,11 @@ end
function UI.Tabs:eventHandler(event) function UI.Tabs:eventHandler(event)
if event.type == 'tab_change' then if event.type == 'tab_change' then
local tab = self:find(event.tab.tabUid) local tab = self:find(event.tab.tabUid)
if event.current > event.last then local hint = event.current > event.last and 'slideLeft' or 'slideRight'
self.transitionHint = 'slideLeft'
else
self.transitionHint = 'slideRight'
end
for _,child in pairs(self.children) do for child in self:eachChild() do
if child.uid == event.tab.tabUid then if child.uid == event.tab.tabUid then
child.transitionHint = hint
child:enable() child:enable()
elseif child.tabTitle then elseif child.tabTitle then
child:disable() child:disable()
@ -89,6 +86,7 @@ function UI.Tabs:eventHandler(event)
end end
self:emit({ type = 'tab_activate', activated = tab }) self:emit({ type = 'tab_activate', activated = tab })
tab:draw() tab:draw()
return true
end end
end end
@ -102,11 +100,26 @@ function UI.Tabs.example()
tab2 = UI.Tab { tab2 = UI.Tab {
index = 2, index = 2,
tabTitle = 'tab2', tabTitle = 'tab2',
button = UI.Button { y = 3 }, subtabs = UI.Tabs {
x = 3, y = 2, ex = -3, ey = -2,
tab1 = UI.Tab {
index = 1,
tabTitle = 'tab4',
entry = UI.TextEntry { y = 3, shadowText = 'text' },
},
tab3 = UI.Tab {
index = 2,
tabTitle = 'tab5',
},
},
}, },
tab3 = UI.Tab { tab3 = UI.Tab {
index = 3, index = 3,
tabTitle = 'tab3', tabTitle = 'tab3',
}, },
enable = function(self)
UI.Tabs.enable(self)
self:setActive(self.tab3, false)
end,
} }
end end

View File

@ -8,11 +8,11 @@ UI.Text.defaults = {
value = '', value = '',
height = 1, height = 1,
} }
function UI.Text:setParent() function UI.Text:layout()
if not self.width and not self.ex then if not self.width and not self.ex then
self.width = #tostring(self.value) self.width = #tostring(self.value)
end end
UI.Window.setParent(self) UI.Window.layout(self)
end end
function UI.Text:draw() function UI.Text:draw()

View File

@ -6,36 +6,44 @@ UI.TextArea.defaults = {
UIElement = 'TextArea', UIElement = 'TextArea',
marginRight = 2, marginRight = 2,
value = '', value = '',
showScrollBar = true,
} }
function UI.TextArea:postInit()
self.scrollBar = UI.ScrollBar()
end
function UI.TextArea:setText(text) function UI.TextArea:setText(text)
self:reset() self:reset()
self.value = text self.value = text
self:draw() self:draw()
end end
function UI.TextArea:focus() function UI.TextArea.focus()
-- allow keyboard scrolling -- allow keyboard scrolling
end end
function UI.TextArea:draw() function UI.TextArea:draw()
self:clear() self:clear()
-- self:setCursorPos(1, 1)
self.cursorX, self.cursorY = 1, 1
self:print(self.value) self:print(self.value)
self:drawChildren()
for _,child in pairs(self.children) do
if child.enabled then
child:draw()
end
end
end end
function UI.TextArea.example() function UI.TextArea.example()
return UI.TextArea { local Ansi = require('opus.ansi')
value = 'sample text\nabc' return UI.Window {
backgroundColor = 2048,
t1 = UI.TextArea {
ey = 3,
value = 'sample text\nabc'
},
t2 = UI.TextArea {
y = 5,
backgroundColor = 'green',
value = string.format([[now %%is the %stime %sfor%s all good men to come to the aid of their country.
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
3
4
5
6
7
8]], Ansi.yellow, Ansi.onred, Ansi.reset),
}
} }
end end

View File

@ -3,7 +3,6 @@ local entry = require('opus.entry')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local _rep = string.rep local _rep = string.rep
local function transform(directive) local function transform(directive)
@ -19,15 +18,16 @@ UI.TextEntry = class(UI.Window)
UI.TextEntry.docs = { } UI.TextEntry.docs = { }
UI.TextEntry.defaults = { UI.TextEntry.defaults = {
UIElement = 'TextEntry', UIElement = 'TextEntry',
--value = '',
shadowText = '', shadowText = '',
focused = false, focused = false,
textColor = colors.white, textColor = 'white',
shadowTextColor = colors.gray, shadowTextColor = 'gray',
backgroundColor = colors.black, -- colors.lightGray, markBackgroundColor = 'gray',
backgroundFocusColor = colors.black, --lightGray, backgroundColor = 'black',
backgroundFocusColor = 'black',
height = 1, height = 1,
limit = 6, limit = 6,
cursorBlink = true,
accelerators = { accelerators = {
[ 'control-c' ] = 'copy', [ 'control-c' ] = 'copy',
} }
@ -74,7 +74,9 @@ function UI.TextEntry:draw()
text = self.shadowText text = self.shadowText
end end
self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) local ss = self.entry.scroll > 0 and '\183' or ' '
self:write(2, 1, Util.widthify(text, self.width - 2) .. ' ', bg, tc)
self:write(1, 1, ss, bg, self.shadowTextColor)
if self.entry.mark.active then if self.entry.mark.active then
local tx = math.max(self.entry.mark.x - self.entry.scroll, 0) local tx = math.max(self.entry.mark.x - self.entry.scroll, 0)
@ -85,7 +87,7 @@ function UI.TextEntry:draw()
end end
if tx ~= tex then if tx ~= tex then
self:write(tx + 2, 1, text:sub(tx + 1, tex), colors.gray, tc) self:write(tx + 2, 1, text:sub(tx + 1, tex), self.markBackgroundColor, tc)
end end
end end
if self.focused then if self.focused then
@ -106,13 +108,12 @@ function UI.TextEntry:updateCursor()
self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1) self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1)
end end
function UI.TextEntry:markAll()
self.entry:markAll()
end
function UI.TextEntry:focus() function UI.TextEntry:focus()
self:draw() self:draw()
if self.focused then
self:setCursorBlink(true)
else
self:setCursorBlink(false)
end
end end
function UI.TextEntry:eventHandler(event) function UI.TextEntry:eventHandler(event)

View File

@ -20,24 +20,15 @@ UI.Throttle.defaults = {
' //) (O ). @ \\-d ) (@ ' ' //) (O ). @ \\-d ) (@ '
} }
} }
function UI.Throttle:setParent() function UI.Throttle:layout()
self.x = math.ceil((self.parent.width - self.width) / 2) self.x = math.ceil((self.parent.width - self.width) / 2)
self.y = math.ceil((self.parent.height - self.height) / 2) self.y = math.ceil((self.parent.height - self.height) / 2)
UI.Window.setParent(self) self:reposition(self.x, self.y, self.width, self.height)
end end
function UI.Throttle:enable() function UI.Throttle:enable()
self.c = os.clock() self.c = os.clock()
self.enabled = false self.ctr = 0
end
function UI.Throttle:disable()
if self.canvas then
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
self.ctr = 0
end
end end
function UI.Throttle:update() function UI.Throttle:update()
@ -46,11 +37,7 @@ function UI.Throttle:update()
os.sleep(0) os.sleep(0)
self.c = os.clock() self.c = os.clock()
self.enabled = true self.enabled = true
if not self.canvas then self:clear(self.borderColor)
self.canvas = self:addLayer(self.backgroundColor, self.borderColor)
self.canvas:setVisible(true)
self:clear(self.borderColor)
end
local image = self.image[self.ctr + 1] local image = self.image[self.ctr + 1]
local width = self.width - 2 local width = self.width - 2
for i = 0, #self.image do for i = 0, #self.image do
@ -63,3 +50,25 @@ function UI.Throttle:update()
self:sync() self:sync()
end end
end end
function UI.Throttle.example()
return UI.Window {
button1 = UI.Button {
x = 2, y = 2,
text = 'Test',
},
throttle = UI.Throttle {
textColor = colors.yellow,
borderColor = colors.green,
},
eventHandler = function (self, event)
if event.type == 'button_press' then
for _ = 1, 40 do
self.throttle:update()
os.sleep(.05)
end
self.throttle:disable()
end
end,
}
end

View File

@ -1,59 +1,20 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local _rep = string.rep
local _sub = string.sub
-- For manipulating text in a fixed width string
local SB = class()
function SB:init(width)
self.width = width
self.buf = _rep(' ', width)
end
function SB:insert(x, str, width)
if x < 1 then
x = self.width + x + 1
end
width = width or #str
if x + width - 1 > self.width then
width = self.width - x
end
if width > 0 then
self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width)
end
end
function SB:fill(x, ch, width)
width = width or self.width - x + 1
self:insert(x, _rep(ch, width))
end
function SB:center(str)
self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str)
end
function SB:get()
return self.buf
end
UI.TitleBar = class(UI.Window) UI.TitleBar = class(UI.Window)
UI.TitleBar.defaults = { UI.TitleBar.defaults = {
UIElement = 'TitleBar', UIElement = 'TitleBar',
height = 1, height = 1,
textColor = colors.white,
backgroundColor = colors.cyan,
title = '', title = '',
frameChar = UI.extChars and '\140' or '-', frameChar = UI.extChars and '\140' or '-',
closeInd = UI.extChars and '\215' or '*', closeInd = UI.extChars and '\215' or '*',
} }
function UI.TitleBar:draw() function UI.TitleBar:draw()
local sb = SB(self.width) self:fillArea(2, 1, self.width - 2, 1, self.frameChar)
sb:fill(2, self.frameChar, sb.width - 3) self:centeredWrite(1, string.format(' %s ', self.title))
sb:center(string.format(' %s ', self.title))
if self.previousPage or self.event then if self.previousPage or self.event then
sb:insert(-1, self.closeInd) self:write(self.width - 1, 1, ' ' .. self.closeInd)
else
sb:insert(-2, self.frameChar)
end end
self:write(1, 1, sb:get())
end end
function UI.TitleBar:eventHandler(event) function UI.TitleBar:eventHandler(event)
@ -69,5 +30,74 @@ function UI.TitleBar:eventHandler(event)
end end
return true return true
end end
elseif event.type == 'mouse_down' then
self.anchor = { x = event.x, y = event.y, ox = self.parent.x, oy = self.parent.y, h = self.parent.height }
elseif event.type == 'mouse_drag' then
if self.expand == 'height' then
local d = event.dy
if self.anchor.h - d > 0 and self.anchor.oy + d > 0 then
self.parent:reposition(self.parent.x, self.anchor.oy + event.dy, self.width, self.anchor.h - d)
end
elseif self.moveable then
local d = event.dy
if self.anchor.oy + d > 0 and self.anchor.oy + d <= self.parent.parent.height then
self.parent:move(self.anchor.ox + event.dx, self.anchor.oy + event.dy)
end
end
end end
end end
function UI.TitleBar.example()
return UI.Window {
win1 = UI.Window {
x = 9, y = 2, ex = -7, ey = -3,
backgroundColor = 'green',
titleBar = UI.TitleBar {
title = 'A really, really, really long title', moveable = true,
},
button1 = UI.Button {
x = 2, y = 3,
text = 'Press',
},
focus = function (self)
self:raise()
end,
},
win2 = UI.Window {
x = 7, y = 3, ex = -9, ey = -2,
backgroundColor = 'orange',
titleBar = UI.TitleBar {
title = 'test', moveable = true,
event = 'none',
},
button1 = UI.Button {
x = 2, y = 3,
text = 'Press',
},
focus = function (self)
self:raise()
end,
},
draw = function(self, isBG)
for i = 1, self.height do
self:write(1, i, self.filler or '')
end
if not isBG then
for _,v in pairs(self.children) do
v:draw()
end
end
end,
enable = function (self)
require('opus.event').onInterval(.5, function()
self.filler = string.rep(string.char(math.random(33, 126)), self.width)
self:draw(true)
self:sync()
end)
UI.Window.enable(self)
end
}
end

View File

@ -1,13 +1,11 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.VerticalMeter = class(UI.Window) UI.VerticalMeter = class(UI.Window)
UI.VerticalMeter.defaults = { UI.VerticalMeter.defaults = {
UIElement = 'VerticalMeter', UIElement = 'VerticalMeter',
backgroundColor = colors.gray, backgroundColor = 'gray',
meterColor = colors.lime, meterColor = 'lime',
width = 1, width = 1,
value = 0, value = 0,
} }
@ -18,12 +16,11 @@ function UI.VerticalMeter:draw()
end end
function UI.VerticalMeter.example() function UI.VerticalMeter.example()
local Event = require('opus.event')
return UI.VerticalMeter { return UI.VerticalMeter {
x = 2, width = 3, y = 2, ey = -2, x = 2, width = 3, y = 2, ey = -2,
focus = function() end, focus = function() end,
enable = function(self) enable = function(self)
Event.onInterval(.25, function() require('opus.event').onInterval(.25, function()
self.value = self.value == 100 and 0 or self.value + 5 self.value = self.value == 100 and 0 or self.value + 5
self:draw() self:draw()
self:sync() self:sync()
@ -31,4 +28,4 @@ function UI.VerticalMeter.example()
return UI.VerticalMeter.enable(self) return UI.VerticalMeter.enable(self)
end end
} }
end end

View File

@ -1,16 +1,15 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Viewport = class(UI.Window) UI.Viewport = class(UI.Window)
UI.Viewport.defaults = { UI.Viewport.defaults = {
UIElement = 'Viewport', UIElement = 'Viewport',
backgroundColor = colors.cyan,
accelerators = { accelerators = {
down = 'scroll_down', down = 'scroll_down',
up = 'scroll_up', up = 'scroll_up',
home = 'scroll_top', home = 'scroll_top',
left = 'scroll_left',
right = 'scroll_right',
[ 'end' ] = 'scroll_bottom', [ 'end' ] = 'scroll_bottom',
pageUp = 'scroll_pageUp', pageUp = 'scroll_pageUp',
[ 'control-b' ] = 'scroll_pageUp', [ 'control-b' ] = 'scroll_pageUp',
@ -18,53 +17,60 @@ UI.Viewport.defaults = {
[ 'control-f' ] = 'scroll_pageDown', [ 'control-f' ] = 'scroll_pageDown',
}, },
} }
function UI.Viewport:layout() function UI.Viewport:postInit()
UI.Window.layout(self) if self.showScrollBar then
if not self.canvas then self.scrollBar = UI.ScrollBar()
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end end
end end
function UI.Viewport:enable() function UI.Viewport:setScrollPosition(offy, offx) -- argh - reverse
UI.Window.enable(self) local oldOffy = self.offy
self.canvas:setVisible(true) self.offy = math.max(offy, 0)
end self.offy = math.min(self.offy, math.max(#self.lines, self.height) - self.height)
if self.offy ~= oldOffy then
function UI.Viewport:disable()
UI.Window.disable(self)
self.canvas:setVisible(false)
end
function UI.Viewport:setScrollPosition(offset)
local oldOffset = self.offy
self.offy = math.max(offset, 0)
self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height)
if self.offy ~= oldOffset then
if self.scrollBar then if self.scrollBar then
self.scrollBar:draw() self.scrollBar:draw()
end end
self.canvas.offy = offset self.offy = offy
self.canvas:dirty() self:dirty(true)
end
local oldOffx = self.offx
self.offx = math.max(offx or 0, 0)
self.offx = math.min(self.offx, math.max(#self.lines[1], self.width) - self.width)
if self.offx ~= oldOffx then
if self.scrollBar then
--self.scrollBar:draw()
end
self.offx = offx or 0
self:dirty(true)
end end
end end
function UI.Viewport:write(x, y, text, bg, tc) function UI.Viewport:blit(x, y, text, bg, fg)
if y > #self.canvas.lines then if y > #self.lines then
for i = #self.canvas.lines, y do self:resizeBuffer(self.width, y)
self.canvas.lines[i + 1] = { } end
self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor) return UI.Window.blit(self, x, y, text, bg, fg)
end end
function UI.Viewport:write(x, y, text, bg, fg)
if y > #self.lines then
self:resizeBuffer(self.width, y)
end
return UI.Window.write(self, x, y, text, bg, fg)
end
function UI.Viewport:setViewHeight(h)
if h > #self.lines then
self:resizeBuffer(self.width, h)
end end
return UI.Window.write(self, x, y, text, bg, tc)
end end
function UI.Viewport:reset() function UI.Viewport:reset()
self.offy = 0 self.offy = 0
self.canvas.offy = 0 for i = self.height + 1, #self.lines do
for i = self.height + 1, #self.canvas.lines do self.lines[i] = nil
self.canvas.lines[i] = nil
end end
end end
@ -72,26 +78,33 @@ function UI.Viewport:getViewArea()
return { return {
y = (self.offy or 0) + 1, y = (self.offy or 0) + 1,
height = self.height, height = self.height,
totalHeight = #self.canvas.lines, totalHeight = #self.lines,
offsetY = self.offy or 0, offsetY = self.offy or 0,
} }
end end
function UI.Viewport:eventHandler(event) function UI.Viewport:eventHandler(event)
if #self.lines <= self.height then
return
end
if event.type == 'scroll_down' then if event.type == 'scroll_down' then
self:setScrollPosition(self.offy + 1) self:setScrollPosition(self.offy + 1, self.offx)
elseif event.type == 'scroll_up' then elseif event.type == 'scroll_up' then
self:setScrollPosition(self.offy - 1) self:setScrollPosition(self.offy - 1, self.offx)
elseif event.type == 'scroll_left' then
self:setScrollPosition(self.offy, self.offx - 1)
elseif event.type == 'scroll_right' then
self:setScrollPosition(self.offy, self.offx + 1)
elseif event.type == 'scroll_top' then elseif event.type == 'scroll_top' then
self:setScrollPosition(0) self:setScrollPosition(0, 0)
elseif event.type == 'scroll_bottom' then elseif event.type == 'scroll_bottom' then
self:setScrollPosition(10000000) self:setScrollPosition(10000000, 0)
elseif event.type == 'scroll_pageUp' then elseif event.type == 'scroll_pageUp' then
self:setScrollPosition(self.offy - self.height) self:setScrollPosition(self.offy - self.height, self.offx)
elseif event.type == 'scroll_pageDown' then elseif event.type == 'scroll_pageDown' then
self:setScrollPosition(self.offy + self.height) self:setScrollPosition(self.offy + self.height, self.offx)
elseif event.type == 'scroll_to' then elseif event.type == 'scroll_to' then
self:setScrollPosition(event.offset) self:setScrollPosition(event.offset, 0)
else else
return false return false
end end

View File

@ -25,9 +25,6 @@ function UI.Wizard:postInit()
} }
Util.merge(self, self.pages) Util.merge(self, self.pages)
--for _, child in pairs(self.pages) do
-- child.ey = -2
--end
end end
function UI.Wizard:add(pages) function UI.Wizard:add(pages)
@ -50,9 +47,8 @@ end
function UI.Wizard:enable(...) function UI.Wizard:enable(...)
self.enabled = true self.enabled = true
self.index = 1 self.index = 1
self.transitionHint = nil
local initial = self:getPage(1) local initial = self:getPage(1)
for _,child in pairs(self.children) do for child in self:eachChild() do
if child == initial or not child.index then if child == initial or not child.index then
child:enable(...) child:enable(...)
else else
@ -93,12 +89,13 @@ function UI.Wizard:eventHandler(event)
elseif event.type == 'enable_view' then elseif event.type == 'enable_view' then
local current = event.next or event.prev local current = event.next or event.prev
if not current then error('property "index" is required on wizard pages') end if not current then error('property "index" is required on wizard pages') end
local hint
if event.current then if event.current then
if event.next then if event.next then
self.transitionHint = 'slideLeft' hint = 'slideLeft'
elseif event.prev then elseif event.prev then
self.transitionHint = 'slideRight' hint = 'slideRight'
end end
event.current:disable() event.current:disable()
end end
@ -117,6 +114,7 @@ function UI.Wizard:eventHandler(event)
self.nextButton.event = 'wizard_complete' self.nextButton.event = 'wizard_complete'
end end
-- a new current view -- a new current view
current.transitionHint = hint
current:enable() current:enable()
current:emit({ type = 'view_enabled', view = current }) current:emit({ type = 'view_enabled', view = current })
self:draw() self:draw()

View File

@ -1,11 +1,8 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors UI.WizardPage = class(UI.Window)
UI.WizardPage = class(UI.ActiveLayer)
UI.WizardPage.defaults = { UI.WizardPage.defaults = {
UIElement = 'WizardPage', UIElement = 'WizardPage',
backgroundColor = colors.cyan,
ey = -2, ey = -2,
} }

View File

@ -2,50 +2,85 @@ local Tween = require('opus.ui.tween')
local Transition = { } local Transition = { }
function Transition.slideLeft(args) function Transition.slideLeft(canvas, args)
local ticks = args.ticks or 10 local ticks = args.ticks or 6
local easing = args.easing or 'outQuint' local easing = args.easing or 'inCirc'
local pos = { x = args.ex } local pos = { x = canvas.ex }
local tween = Tween.new(ticks, pos, { x = args.x }, easing) local tween = Tween.new(ticks, pos, { x = canvas.x }, easing)
args.canvas:move(pos.x, args.canvas.y) canvas:move(pos.x, canvas.y)
return function() return function()
local finished = tween:update(1) local finished = tween:update(1)
args.canvas:move(math.floor(pos.x), args.canvas.y) canvas:move(math.floor(pos.x), canvas.y)
args.canvas:dirty() canvas:dirty(true)
return not finished return not finished
end end
end end
function Transition.slideRight(args) function Transition.slideRight(canvas, args)
local ticks = args.ticks or 10 local ticks = args.ticks or 6
local easing = args.easing or'outQuint' local easing = args.easing or 'inCirc'
local pos = { x = -args.canvas.width } local pos = { x = -canvas.width }
local tween = Tween.new(ticks, pos, { x = 1 }, easing) local tween = Tween.new(ticks, pos, { x = 1 }, easing)
args.canvas:move(pos.x, args.canvas.y) canvas:move(pos.x, canvas.y)
return function() return function()
local finished = tween:update(1) local finished = tween:update(1)
args.canvas:move(math.floor(pos.x), args.canvas.y) canvas:move(math.floor(pos.x), canvas.y)
args.canvas:dirty() canvas:dirty(true)
return not finished return not finished
end end
end end
function Transition.expandUp(args) function Transition.expandUp(canvas, args)
local ticks = args.ticks or 3 local ticks = args.ticks or 3
local easing = args.easing or 'linear' local easing = args.easing or 'linear'
local pos = { y = args.ey + 1 } local pos = { y = canvas.ey + 1 }
local tween = Tween.new(ticks, pos, { y = args.y }, easing) local tween = Tween.new(ticks, pos, { y = canvas.y }, easing)
args.canvas:move(args.x, pos.y) canvas:move(canvas.x, pos.y)
return function() return function()
local finished = tween:update(1) local finished = tween:update(1)
args.canvas:move(args.x, math.floor(pos.y)) canvas:move(canvas.x, math.floor(pos.y))
args.canvas:dirty() canvas.parent:dirty(true)
return not finished
end
end
function Transition.shake(canvas, args)
local ticks = args.ticks or 8
local i = ticks
return function()
i = -i
canvas:move(canvas.x + i, canvas.y)
if i > 0 then
i = i - 2
end
return i ~= 0
end
end
function Transition.shuffle(canvas, args)
local ticks = args.ticks or 4
local easing = args.easing or 'linear'
local t = { }
for _,child in pairs(canvas.children) do
t[child] = Tween.new(ticks, child, { x = child.x, y = child.y }, easing)
child.x = math.random(1, canvas.parent.width)
child.y = math.random(1, canvas.parent.height)
end
return function()
local finished
for child, tween in pairs(t) do
finished = tween:update(1)
child:move(math.floor(child.x), math.floor(child.y))
end
return not finished return not finished
end end
end end

View File

@ -13,6 +13,7 @@ local _unpack = table.unpack
local _bor = bit32.bor local _bor = bit32.bor
local _bxor = bit32.bxor local _bxor = bit32.bxor
local byteArrayMT
byteArrayMT = { byteArrayMT = {
__tostring = function(a) return string.char(_unpack(a)) end, __tostring = function(a) return string.char(_unpack(a)) end,
__index = { __index = {
@ -668,42 +669,31 @@ function Util.trimr(s)
end end
-- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76 -- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
-- word wrapping based on: local function wrap(text, max, lines)
-- https://www.rosettacode.org/wiki/Word_wrap#Lua and local index = 1
-- http://lua-users.org/wiki/StringRecipes repeat
local function paragraphwrap(text, linewidth, res) if #text <= max then
linewidth = linewidth or 75 table.insert(lines, text)
local spaceleft = linewidth text = ''
local line = { } elseif text:sub(max+1, max+1) == ' ' then
table.insert(lines, text:sub(index, max))
for word in text:gmatch("%S+") do text = text:sub(max + 2)
local len = #word + 1
--if colorMode then
-- word:gsub('()@([@%d])', function(pos, c) len = len - 2 end)
--end
if len > spaceleft then
table.insert(res, table.concat(line, ' '))
line = { word }
spaceleft = linewidth - len - 1
else else
table.insert(line, word) local x = text:sub(1, max)
spaceleft = spaceleft - len local s = x:match('(.*) ') or x
text = text:sub(#s + 1)
table.insert(lines, s)
end end
end text = text:match('^%s*(.*)')
until not text or #text == 0
table.insert(res, table.concat(line, ' ')) return lines
return table.concat(res, '\n')
end end
-- end word wrapping
function Util.wordWrap(str, limit) function Util.wordWrap(str, limit)
local longLines = Util.split(str)
local lines = { } local lines = { }
for _,line in ipairs(longLines) do for _,line in ipairs(Util.split(str)) do
paragraphwrap(line, limit, lines) wrap(line, limit, lines)
end end
return lines return lines