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
.project

View File

@ -82,16 +82,60 @@ local Browser = UI.Page {
},
sortColumn = 'name',
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 {
columns = {
{ key = 'status' },
{ 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 { },
associations = UI.SlideOut {
backgroundColor = colors.cyan,
menuBar = UI.MenuBar {
buttons = {
{ text = 'Save', event = 'save' },
@ -99,7 +143,7 @@ local Browser = UI.Page {
},
},
grid = UI.ScrollingGrid {
x = 2, ex = -6, y = 3, ey = -5,
x = 2, ex = -6, y = 3, ey = -8,
columns = {
{ heading = 'Extension', key = 'name' },
{ heading = 'Program', key = 'value' },
@ -114,8 +158,11 @@ local Browser = UI.Page {
x = -4, y = 6,
text = '-', event = 'remove_entry', help = 'Remove',
},
[1] = UI.Window {
x = 2, y = -6, ex = -6, ey = -3,
},
form = UI.Form {
x = 3, y = -3, ey = -2,
x = 3, y = -5, ex = -7, ey = -3,
margin = 1,
manualControls = true,
[1] = UI.TextEntry {
@ -137,9 +184,7 @@ local Browser = UI.Page {
text = 'Add', event = 'add_association',
},
},
statusBar = UI.StatusBar {
backgroundColor = colors.cyan,
},
statusBar = UI.StatusBar { },
},
accelerators = {
[ 'control-q' ] = 'quit',
@ -175,51 +220,6 @@ function Browser.menuBar:getActive(menuItem)
return true
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, ...)
self.notification:info(string.format(status, ...))
end
@ -255,7 +255,6 @@ function Browser:getDirectory(directory)
end
function Browser:updateDirectory(dir)
dir.size = 0
dir.totalSize = 0
Util.clear(dir.files)
@ -344,7 +343,7 @@ function Browser:eventHandler(event)
local file = self.grid:getSelected()
if event.type == 'quit' then
Event.exitPullEvents()
UI:quit()
elseif event.type == 'edit' and file then
self:run('edit', file.name)
@ -432,28 +431,25 @@ function Browser:eventHandler(event)
elseif event.type == 'delete' then
if self:hasMarked() then
local width = self.statusBar:getColumnWidth('status')
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)
self.question:show()
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
if self:hasMarked() then
@ -549,6 +545,4 @@ local args = Util.parse(...)
Browser:setDir(args[1] or shell.dir())
UI:setPage(Browser)
Event.pullEvents()
UI.term:reset()
UI:start()

View File

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

View File

@ -58,11 +58,16 @@ local page = UI.Page {
},
[2] = UI.Tab {
tabTitle = 'Output',
backgroundColor = 'black',
output = UI.Embedded {
visible = true,
y = 2,
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 pos = 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()
elseif event.type == 'device' then
@ -196,7 +201,6 @@ function page:eventHandler(event)
command = nil
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw()
end
return true
@ -243,7 +247,6 @@ function page:setResult(result)
end
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw()
end
@ -373,7 +376,7 @@ function page:executeStatement(statement)
end
if _exit then
UI:exitPullEvents()
UI:quit()
end
end
@ -382,7 +385,8 @@ if args[1] then
command = 'args[1]'
sandboxEnv.args = args
page:setResult(args[1])
page:setPrompt(command)
end
UI:setPage(page)
UI:pullEvents()
UI:start()

View File

@ -4,7 +4,6 @@ local Socket = require('opus.socket')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local device = _G.device
local network = _G.network
local os = _G.os
@ -56,6 +55,31 @@ local page = UI.Page {
columns = gridColumns,
sortColumn = 'label',
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 {
titleBar = UI.TitleBar {
@ -72,17 +96,22 @@ local page = UI.Page {
sortColumn = 'port',
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 {
backgroundColor = colors.cyan,
x = 5, ex = -5, height = 8, y = -8,
titleBar = UI.TitleBar {
title = 'Network Help',
event = 'slide_hide',
},
text = UI.TextArea {
x = 2, y = 2,
backgroundColor = colors.cyan,
x = 1, y = 2,
marginLeft = 1,
value = [[
In order to connect to another computer:
@ -127,13 +156,6 @@ local function sendCommand(host, command)
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()
local transport = network:getTransport()
@ -230,7 +252,7 @@ function page:eventHandler(event)
Config.update('network', config)
elseif event.type == 'quit' then
Event.exitPullEvents()
UI:quit()
end
UI.Page.eventHandler(self, event)
end
@ -243,33 +265,6 @@ function page.menuBar:getActive(menuItem)
return menuItem.noCheck or not not t
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()
page.grid:update()
page.grid:draw()
@ -295,4 +290,4 @@ if not device.wireless_modem then
end
UI:setPage(page)
UI:pullEvents()
UI:start()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -131,7 +131,7 @@ function page:eventHandler(event)
shell.openForegroundTab('PackageManager')
elseif event.type == 'wizard_complete' or event.type == 'cancel' then
UI.exitPullEvents()
UI:quit()
else
return UI.Page.eventHandler(self, event)
@ -140,4 +140,4 @@ function page:eventHandler(event)
end
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)
if event.type == 'focus_change' and isRelevant(event.focused) then
focused = event.focused
@ -144,7 +150,6 @@ page = UI.Page {
})
end
self.tabs.properties.grid:setValues(t)
self.tabs.properties.grid:update()
self.tabs.properties.grid:draw()
t = { }
@ -156,7 +161,6 @@ page = UI.Page {
end
end
self.tabs.methodsTab.grid:setValues(t)
self.tabs.methodsTab.grid:update()
self.tabs.methodsTab.grid:draw()
elseif event.type == 'edit_property' then
@ -168,6 +172,19 @@ page = UI.Page {
elseif event.type == 'editor_apply' then
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
return UI.Page.eventHandler(self, event)
@ -175,4 +192,4 @@ page = UI.Page {
}
UI:setPage(page)
UI:pullEvents()
UI:start()

View File

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

View File

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

View File

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

View File

@ -2,8 +2,6 @@ local Array = require('opus.array')
local Config = require('opus.config')
local UI = require('opus.ui')
local colors = _G.colors
local tab = UI.Tab {
tabTitle = 'Preferred',
description = 'Select preferred applications',
@ -22,20 +20,19 @@ local tab = UI.Tab {
disableHeader = true,
columns = {
{ 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 {
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()
local app = self.apps:getSelected().name
local choices = { }

View File

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

View File

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

View File

@ -4,11 +4,11 @@ local colors = _G.colors
local peripheral = _G.peripheral
local settings = _G.settings
local tab = UI.Tab {
return peripheral.find('monitor') and UI.Tab {
tabTitle = 'Kiosk',
description = 'Kiosk options',
form = UI.Form {
x = 2, ex = -2,
x = 2, y = 2, ex = -2, ey = 5,
manualControls = true,
monitor = UI.Chooser {
formLabel = 'Monitor', formKey = 'monitor',
@ -22,41 +22,36 @@ local tab = UI.Tab {
},
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()
local choices = { }
peripheral.find('monitor', function(side)
table.insert(choices, { name = side, value = side })
end)
peripheral.find('monitor', function(side)
table.insert(choices, { name = side, value = side })
end)
self.form.monitor.choices = choices
self.form.monitor.value = settings.get('kiosk.monitor')
self.form.monitor.choices = choices
self.form.monitor.value = settings.get('kiosk.monitor')
self.form.textScale.value = settings.get('kiosk.textscale')
self.form.textScale.value = settings.get('kiosk.textscale')
UI.Tab.enable(self)
end
function tab:eventHandler(event)
if event.type == 'choice_change' then
if self.form.monitor.value then
settings.set('kiosk.monitor', self.form.monitor.value)
UI.Tab.enable(self)
end,
eventHandler = function(self, event)
if event.type == 'choice_change' then
if self.form.monitor.value then
settings.set('kiosk.monitor', self.form.monitor.value)
end
if self.form.textScale.value then
settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end
if self.form.textScale.value then
settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
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 os = _G.os
local labelTab = UI.Tab {
return UI.Tab {
tabTitle = 'Label',
description = 'Set the computer label',
labelText = UI.Text {
x = 3, y = 2,
x = 3, y = 3,
value = 'Label'
},
label = UI.TextEntry {
x = 9, y = 2, ex = -4,
x = 9, y = 3, ex = -4,
limit = 32,
value = os.getComputerLabel(),
accelerators = {
enter = 'update_label',
},
},
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
grid = UI.ScrollingGrid {
y = 3,
x = 2, y = 5, ex = -2, ey = -2,
values = {
{ name = '', value = '' },
{ name = 'CC version', value = Util.getVersion() },
@ -30,20 +33,18 @@ local labelTab = UI.Tab {
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) },
},
disableHeader = true,
inactive = true,
columns = {
{ 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 {
tabTitle = 'Launcher',
description = 'Set the application launcher',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 5,
},
launcherLabel = UI.Text {
x = 3, y = 2,
x = 3, y = 3,
value = 'Launcher',
},
launcher = UI.Chooser {
x = 13, y = 2, width = 12,
x = 13, y = 3, width = 12,
choices = {
{ name = 'Overview', value = 'sys/apps/Overview.lua' },
{ name = 'Shell', value = 'sys/apps/ShellLauncher.lua' },
@ -22,18 +25,20 @@ local tab = UI.Tab {
},
},
custom = UI.TextEntry {
x = 13, ex = -3, y = 3,
x = 13, ex = -3, y = 4,
limit = 128,
shadowText = 'File name',
},
button = UI.Button {
x = 3, y = 5,
text = 'Update',
x = -8, ex = -2, y = -2,
text = 'Apply',
event = 'update',
},
labelText = UI.TextArea {
x = 3, ex = -3, y = 7,
x = 2, ex = -2, y = 6, ey = -4,
backgroundColor = colors.black,
textColor = colors.yellow,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Choose an application launcher',
},
}

View File

@ -2,59 +2,61 @@ local Ansi = require('opus.ansi')
local Config = require('opus.config')
local UI = require('opus.ui')
local colors = _G.colors
local device = _G.device
local tab = UI.Tab {
return UI.Tab {
tabTitle = 'Network',
description = 'Networking options',
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(
[[%sSet the primary modem used for wireless communications.%s
Reboot to take effect.]], Ansi.yellow, Ansi.reset)
},
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
label = UI.Text {
x = 3, y = 2,
x = 3, y = 3,
value = 'Modem',
},
modem = UI.Chooser {
x = 10, ex = -3, y = 2,
x = 10, ex = -3, y = 3,
nochoice = 'auto',
},
}
enable = function(self)
local width = 7
local choices = {
{ name = 'auto', value = 'auto' },
{ name = 'disable', value = 'none' },
}
function tab:enable()
local width = 7
local choices = {
{ name = 'auto', value = 'auto' },
{ name = 'disable', value = 'none' },
}
for k,v in pairs(device) do
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
table.insert(choices, { name = k, value = v.name })
width = math.max(width, #k)
end
end
for k,v in pairs(device) do
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
table.insert(choices, { name = k, value = v.name })
width = math.max(width, #k)
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,
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
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 UI = require('opus.ui')
local colors = _G.colors
local passwordTab = UI.Tab {
return UI.Tab {
tabTitle = 'Password',
description = 'Wireless network password',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
newPass = UI.TextEntry {
x = 3, ex = -3, y = 3,
limit = 32,
@ -17,28 +18,28 @@ local passwordTab = UI.Tab {
},
},
button = UI.Button {
x = 3, y = 5,
text = 'Update',
x = -8, ex = -2, y = -2,
text = 'Apply',
event = 'update_password',
},
info = UI.TextArea {
x = 3, ex = -3, y = 7,
textColor = colors.yellow,
x = 2, ex = -2, y = 5, ey = -4,
backgroundColor = 'black',
textColor = 'yellow',
inactive = true,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Add a password to enable other computers to connect to this one.',
}
}
function passwordTab:eventHandler(event)
if event.type == 'update_password' then
if not self.newPass.value or #self.newPass.value == 0 then
self:emit({ type = 'error_message', message = 'Invalid password' })
},
eventHandler = function(self, event)
if event.type == 'update_password' then
if not self.newPass.value or #self.newPass.value == 0 then
self:emit({ type = 'error_message', message = 'Invalid password' })
else
Security.updatePassword(SHA.compute(self.newPass.value))
self:emit({ type = 'success_message', message = 'Password updated' })
else
Security.updatePassword(SHA.compute(self.newPass.value))
self:emit({ type = 'success_message', message = 'Password updated' })
end
return true
end
return true
end
end
return passwordTab
}

View File

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

View File

@ -2,48 +2,94 @@ local UI = require('opus.ui')
local settings = _G.settings
if settings then
local settingsTab = UI.Tab {
tabTitle = 'Settings',
description = 'Computercraft configurable settings',
grid = UI.Grid {
y = 2,
autospace = true,
sortColumn = 'name',
columns = {
{ heading = 'Setting', key = 'name' },
{ heading = 'Value', key = 'value' },
},
},
}
local transform = {
string = tostring,
number = tonumber,
}
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 = { }
for _,v in pairs(settings.getNames()) do
local value = settings.get(v)
if not value then
value = false
end
table.insert(values, {
name = v,
value = value,
value = settings.get(v) or false,
})
end
self.grid:setValues(values)
self.grid:setIndex(1)
end,
enable = function(self)
self:reload()
UI.Tab.enable(self)
end
function settingsTab:eventHandler(event)
end,
eventHandler = function(self, event)
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
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
else
self.editor:show(event.selected)
end
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
return true
end
end
return settingsTab
end
end,
}

View File

@ -28,7 +28,7 @@ local defaults = {
local _colors = config.color or Util.shallowCopy(defaults)
local allSettings = { }
for k, v in pairs(defaults) do
for k in pairs(defaults) do
table.insert(allSettings, { name = k })
end
@ -38,29 +38,34 @@ if not _colors.backgroundColor then
_colors.fileColor = colors.white
end
local tab = UI.Tab {
return UI.Tab {
tabTitle = 'Shell',
description = 'Shell options',
grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 3, ex = -16,
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 = -3,
y = 2, ey = -10, x = -14, ex = -2,
disableHeader = true,
columns = { { key = 'name' } },
values = allColors,
sortColumn = 'name',
},
directoryLabel = UI.Text {
x = 2, y = -2,
value = 'Display directory',
getRowTextColor = function(self, row)
local selected = self.parent.grid1:getSelected()
if _colors[selected.name] == row.value then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
},
directory = UI.Checkbox {
x = 20, y = -2,
x = 2, y = -2,
labelBackgroundColor = colors.black,
label = 'Directory',
value = config.displayDirectory
},
reset = UI.Button {
@ -74,69 +79,57 @@ local tab = UI.Tab {
event = 'update',
},
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]
end)
keyboard.addHotkey('shift-paste', function()
local function queuePaste()
local data = keyboard.clipboard
if type(data) == 'table' then
@ -20,4 +20,7 @@ keyboard.addHotkey('shift-paste', function()
if data then
os.queueEvent('paste', data)
end
end)
end
kernel.hook('clipboard_paste', queuePaste)
keyboard.addHotkey('shift-paste', queuePaste)

View File

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

View File

@ -50,7 +50,10 @@
title = "System",
category = "System",
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",
},
[ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = {

View File

@ -6,10 +6,11 @@ Shortcut keys
* l: Lua application
* f: Files
* e: Edit an application (or right-click)
* n: Network
* control-n: Add a new application
* delete: Delete an application
Adding a new application
========================
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')
_G.device = Peripheral.getList()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
local Util = require('opus.util')
local fs = _G.fs
local shell = _ENV.shell
local Config = { }
@ -25,23 +24,6 @@ function Config.load(fname, data)
return data
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)
local filename = 'usr/config/' .. fname
Util.writeTable(filename, data)

View File

@ -41,9 +41,9 @@ end
function Entry:updateScroll()
local ps = self.scroll
local value = _val(self.value)
if self.pos > #value then
self.pos = #value
local len = #_val(self.value)
if self.pos > len then
self.pos = len
self.scroll = 0 -- ??
end
if self.pos - self.scroll > self.width then
@ -51,6 +51,11 @@ function Entry:updateScroll()
elseif self.pos < self.scroll then
self.scroll = self.pos
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
self.textChanged = true
end
@ -217,6 +222,10 @@ function Entry:paste(ie)
end
end
function Entry:forcePaste()
os.queueEvent('clipboard_paste')
end
function Entry:clearLine()
if #_val(self.value) > 0 then
self:reset()
@ -363,9 +372,10 @@ local mappings = {
--[ 'control-d' ] = Entry.cutNextWord,
[ 'control-x' ] = Entry.cut,
[ '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_tripleclick' ] = Entry.markAll,
[ 'shift-left' ] = Entry.markLeft,
[ 'shift-right' ] = Entry.markRight,
[ '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 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
error(_sformat('Failed to download %s\n%s', dataUrl, msg), 2)
error(string.format('Failed to download %s\n%s', dataUrl, msg), 2)
else
return json.decode(contents)
end

View File

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

View File

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

View File

@ -1,16 +1,19 @@
local Util = require('opus.util')
local colors = _G.colors
local NFT = { }
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
local tColourLookup = { }
local hexToColor = { }
for n = 1, 16 do
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
hexToColor[string.sub("0123456789abcdef", n, n)] = 2 ^ (n - 1)
end
local colorToHex = Util.transpose(hexToColor)
local function getColourOf(hex)
return tColourLookup[hex:byte()]
return hexToColor[hex]
end
function NFT.parse(imageText)
@ -62,8 +65,22 @@ function NFT.parse(imageText)
return image
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)
if not imageText then
error('Unable to read image file')
@ -71,4 +88,35 @@ function NFT.load(path)
return NFT.parse(imageText)
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

View File

@ -36,61 +36,66 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
local maxScroll = 100
local cx, cy = 1, 1
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,
y = sy,
width = w,
height = h,
isColor = parent.isColor(),
offy = 0,
bg = _bg,
fg = _fg,
})
win.canvas = canvas
local function update()
if isVisible then
canvas:render(parent)
win.canvas:render(parent)
win.setCursorPos(cx, cy)
end
end
local function scrollTo(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
canvas.offy = y
canvas:dirty()
if y ~= win.canvas.offy then
win.canvas.offy = y
win.canvas:dirty()
update()
end
end
function win.write(str)
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)
update()
end
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)
update()
end
function win.clear()
canvas.offy = 0
for i = #canvas.lines, canvas.height + 1, -1 do
canvas.lines[i] = nil
win.canvas.offy = 0
for i = #win.canvas.lines, win.canvas.height + 1, -1 do
win.canvas.lines[i] = nil
end
canvas:clear(bg, fg)
win.canvas:clear()
update()
end
function win.getLine(n)
local line = win.canvas.lines[n]
return line.text, line.fg, line.bg
end
function win.clearLine()
canvas:clearLine(cy + canvas.offy, bg, fg)
win.canvas:clearLine(cy + win.canvas.offy)
win.setCursorPos(cx, cy)
update()
end
@ -102,10 +107,14 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.setCursorPos(x, y)
cx, cy = math.floor(x), math.floor(y)
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
function win.getCursorBlink()
return blink
end
function win.setCursorBlink(b)
blink = b
if isVisible then
@ -114,12 +123,12 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end
function win.isColor()
return canvas.isColor
return win.canvas.isColor
end
win.isColour = win.isColor
function win.setTextColor(c)
fg = c
win.canvas.fg = c
end
win.setTextColour = win.setTextColor
@ -139,38 +148,38 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
win.setPaletteColour = win.setPaletteColor
function win.setBackgroundColor(c)
bg = c
win.canvas.bg = c
end
win.setBackgroundColour = win.setBackgroundColor
function win.getSize()
return canvas.width, canvas.height
return win.canvas.width, win.canvas.height
end
function win.scroll(n)
n = n or 1
if n > 0 then
local lines = #canvas.lines
local lines = #win.canvas.lines
for i = 1, n do
canvas.lines[lines + i] = { }
canvas:clearLine(lines + i, bg, fg)
win.canvas.lines[lines + i] = { }
win.canvas:clearLine(lines + i)
end
while #canvas.lines > maxScroll do
table.remove(canvas.lines, 1)
while #win.canvas.lines > maxScroll do
table.remove(win.canvas.lines, 1)
end
scrollTo(#canvas.lines)
canvas:dirty()
scrollTo(#win.canvas.lines)
win.canvas:dirty()
update()
end
end
function win.getTextColor()
return fg
return win.canvas.fg
end
win.getTextColour = win.getTextColor
function win.getBackgroundColor()
return bg
return win.canvas.bg
end
win.getBackgroundColour = win.getBackgroundColor
@ -178,7 +187,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
if visible ~= isVisible then
isVisible = visible
if isVisible then
canvas:dirty()
win.canvas:dirty()
update()
end
end
@ -186,7 +195,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.redraw()
if isVisible then
canvas:dirty()
win.canvas:dirty()
update()
end
end
@ -194,27 +203,27 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.restoreCursor()
if isVisible then
win.setCursorPos(cx, cy)
win.setTextColor(fg)
win.setTextColor(win.canvas.fg)
win.setCursorBlink(blink)
end
end
function win.getPosition()
return canvas.x, canvas.y
return win.canvas.x, win.canvas.y
end
function win.reposition(x, y, width, height)
canvas.x, canvas.y = x, y
canvas:resize(width or canvas.width, height or canvas.height)
win.canvas.x, win.canvas.y = x, y
win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
end
--[[ Additional methods ]]--
function win.scrollDown()
scrollTo(canvas.offy + 1)
scrollTo(win.canvas.offy + 1)
end
function win.scrollUp()
scrollTo(canvas.offy - 1)
scrollTo(win.canvas.offy - 1)
end
function win.scrollTop()
@ -222,7 +231,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end
function win.scrollBottom()
scrollTo(#canvas.lines)
scrollTo(#win.canvas.lines)
end
function win.setMaxScroll(ms)
@ -230,37 +239,35 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end
function win.getCanvas()
return canvas
return win.canvas
end
function win.getParent()
return parent
end
canvas:clear()
win.canvas:clear()
return win
end
-- get windows contents
function Terminal.getContents(win, parent)
local oblit, oscp = parent.blit, parent.setCursorPos
local lines = { }
function Terminal.getContents(win)
if not win.getLine then
error('window is required')
end
parent.blit = function(text, fg, bg)
lines[#lines + 1] = {
local lines = { }
local _, h = win.getSize()
for i = 1, h do
local text, fg, bg = win.getLine(i)
lines[i] = {
text = text,
fg = fg,
bg = bg,
}
end
parent.setCursorPos = function() end
win.setVisible(true)
win.redraw()
parent.blit = oblit
parent.setCursorPos = oscp
return lines
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 Event = require('opus.event')
local Input = require('opus.input')
@ -5,7 +8,6 @@ local Transition = require('opus.ui.transition')
local Util = require('opus.util')
local _rep = string.rep
local _sub = string.sub
local colors = _G.colors
local device = _G.device
local fs = _G.fs
@ -32,11 +34,16 @@ local textutils = _G.textutils
]]
--[[-- Top Level Manager --]]--
local Manager = class()
function Manager:init()
local UI = { }
function UI:init()
self.devices = { }
self.theme = { }
self.extChars = Util.getVersion() >= 1.76
self.colors = {
primary = colors.green,
secondary = colors.lightGray,
tertiary = colors.gray,
}
local function keyFunction(event, code, held)
local ie = Input:translate(event, code, held)
@ -44,8 +51,7 @@ function Manager:init()
local currentPage = self:getActivePage()
if ie and currentPage then
local target = currentPage.focused or currentPage
self:inputEvent(target,
{ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie })
target:emit({ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie })
currentPage:sync()
end
end
@ -53,10 +59,7 @@ function Manager:init()
local function resize(_, side)
local dev = self.devices[side or 'terminal']
if dev and dev.currentPage then
-- the parent doesn't have any children set...
-- that's why we have to resize both the parent and the current page
-- kinda makes sense
dev.currentPage.parent:resize()
dev:resize()
dev.currentPage:resize()
dev.currentPage:draw()
@ -72,17 +75,14 @@ function Manager:init()
monitor_resize = resize,
mouse_scroll = function(_, direction, x, y)
local ie = Input:translate('mouse_scroll', direction, x, y)
local currentPage = self:getActivePage()
if currentPage then
local event = currentPage:pointToChild(x, y)
local directions = {
[ -1 ] = 'up',
[ 1 ] = 'down'
}
-- revisit - should send out scroll_up and scroll_down events
-- let the element convert them to up / down
self:inputEvent(event.element,
{ type = 'key', key = directions[direction] })
event.type = ie.code
event.ie = { code = ie.code, x = event.x, y = event.y }
event.element:emit(event)
currentPage:sync()
end
end,
@ -92,7 +92,7 @@ function Manager:init()
if dev and dev.currentPage then
Input:translate('mouse_click', 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,
@ -107,7 +107,7 @@ function Manager:init()
currentPage:setFocus(event.element)
currentPage:sync()
end
self:click(currentPage, ie.code, button, x, y)
self:click(currentPage, ie)
end
end
end,
@ -125,7 +125,7 @@ function Manager:init()
elseif ie and currentPage then
if not currentPage.parent.device.side then
self:click(currentPage, ie.code, button, x, y)
self:click(currentPage, ie)
end
end
end,
@ -135,7 +135,7 @@ function Manager:init()
local currentPage = self:getActivePage()
if ie and currentPage then
self:click(currentPage, ie.code, button, x, y)
self:click(currentPage, ie)
end
end,
@ -156,7 +156,7 @@ function Manager:init()
end)
end
function Manager:configure(appName, ...)
function UI:configure(appName, ...)
local defaults = Util.loadTable('usr/config/' .. appName) or { }
if not defaults.device then
defaults.device = { }
@ -190,19 +190,15 @@ function Manager:configure(appName, ...)
end
if defaults.theme then
for k,v in pairs(defaults.theme) do
if self[k] and self[k].defaults then
Util.merge(self[k].defaults, v)
end
end
Util.deepMerge(self.theme, defaults.theme)
end
end
function Manager:disableEffects()
self.defaultDevice.effectsEnabled = false
function UI:disableEffects()
self.term.effectsEnabled = false
end
function Manager:loadTheme(filename)
function UI:loadTheme(filename)
if fs.exists(filename) then
local theme, err = Util.loadTable(filename)
if not theme then
@ -212,8 +208,20 @@ function Manager:loadTheme(filename)
end
end
function Manager:generateTheme(filename)
function UI:generateTheme(filename)
local t = { }
local function getName(d)
if type(d) == 'string' then
return string.format("'%s'", d)
end
for c, n in pairs(colors) do
if n == d then
return 'colors.' .. c
end
end
end
for k,v in pairs(self) do
if type(v) == 'table' then
if v._preload then
@ -226,72 +234,96 @@ function Manager:generateTheme(filename)
if not t[k] then
t[k] = { }
end
for c, n in pairs(colors) do
if n == d then
t[k][p] = 'colors.' .. c
break
end
end
t[k][p] = getName(d)
end
end
end
end
end
t.colors = {
primary = getName(self.colors.primary),
secondary = getName(self.colors.secondary),
tertiary = getName(self.colors.tertiary),
}
Util.writeFile(filename, textutils.serialize(t):gsub('(")', ''))
end
function Manager:emitEvent(event)
function UI:emitEvent(event)
local currentPage = self:getActivePage()
if currentPage and currentPage.focused then
return currentPage.focused:emit(event)
end
end
function Manager:inputEvent(parent, event) -- deprecate ?
return parent and parent:emit(event)
end
function UI:click(target, ie)
local clickEvent
function Manager:click(target, code, button, x, y)
local clickEvent = target:pointToChild(x, y)
if ie.code == 'mouse_drag' then
local function getPosition(element, x, y)
repeat
x = x - element.x + 1
y = y - element.y + 1
element = element.parent
until not element
return x, y
end
if code == 'mouse_doubleclick' then
if self.doubleClickElement ~= clickEvent.element then
local x, y = getPosition(self.lastClicked, ie.x, ie.y)
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
end
else
self.doubleClickElement = clickEvent.element
self.lastClicked = clickEvent.element
end
clickEvent.button = button
clickEvent.type = code
clickEvent.key = code
clickEvent.ie = { code = code, x = clickEvent.x, y = clickEvent.y }
clickEvent.button = ie.button
clickEvent.type = ie.code
clickEvent.key = ie.code
clickEvent.ie = { code = ie.code, x = clickEvent.x, y = clickEvent.y }
clickEvent.raw = ie
if clickEvent.element.focus then
target:setFocus(clickEvent.element)
end
self:inputEvent(clickEvent.element, clickEvent)
clickEvent.element:emit(clickEvent)
target:sync()
end
function Manager:setDefaultDevice(dev)
self.defaultDevice = dev
function UI:setDefaultDevice(dev)
self.term = dev
end
function Manager:addPage(name, page)
function UI:addPage(name, page)
if not self.pages then
self.pages = { }
end
self.pages[name] = page
end
function Manager:setPages(pages)
function UI:setPages(pages)
self.pages = pages
end
function Manager:getPage(pageName)
function UI:getPage(pageName)
local page = self.pages[pageName]
if not page then
@ -301,19 +333,18 @@ function Manager:getPage(pageName)
return page
end
function Manager:getActivePage(page)
function UI:getActivePage(page)
if page then
return page.parent.currentPage
end
return self.defaultDevice.currentPage
return self.term.currentPage
end
function Manager:setActivePage(page)
function UI:setActivePage(page)
page.parent.currentPage = page
page.parent.canvas = page.canvas
end
function Manager:setPage(pageOrName, ...)
function UI:setPage(pageOrName, ...)
local page = pageOrName
if type(pageOrName) == 'string' then
@ -333,7 +364,6 @@ function Manager:setPage(pageOrName, ...)
page.previousPage = currentPage
end
self:setActivePage(page)
--page:clear(page.backgroundColor)
page:enable(...)
page:draw()
if page.focused then
@ -344,27 +374,27 @@ function Manager:setPage(pageOrName, ...)
end
end
function Manager:getCurrentPage()
return self.defaultDevice.currentPage
function UI:getCurrentPage()
return self.term.currentPage
end
function Manager:setPreviousPage()
if self.defaultDevice.currentPage.previousPage then
local previousPage = self.defaultDevice.currentPage.previousPage.previousPage
self:setPage(self.defaultDevice.currentPage.previousPage)
self.defaultDevice.currentPage.previousPage = previousPage
function UI:setPreviousPage()
if self.term.currentPage.previousPage then
local previousPage = self.term.currentPage.previousPage.previousPage
self:setPage(self.term.currentPage.previousPage)
self.term.currentPage.previousPage = previousPage
end
end
function Manager:getDefaults(element, args)
function UI:getDefaults(element, args)
local defaults = Util.deepCopy(element.defaults)
if args then
Manager:mergeProperties(defaults, args)
UI:mergeProperties(defaults, args)
end
return defaults
end
function Manager:mergeProperties(obj, args)
function UI:mergeProperties(obj, args)
if args then
for k,v in pairs(args) do
if k == 'accelerators' then
@ -380,7 +410,7 @@ function Manager:mergeProperties(obj, args)
end
end
function Manager:pullEvents(...)
function UI:pullEvents(...)
local s, m = pcall(Event.pullEvents, ...)
self.term:reset()
if not s and m then
@ -388,21 +418,20 @@ function Manager:pullEvents(...)
end
end
Manager.exitPullEvents = Event.exitPullEvents
Manager.quit = Event.exitPullEvents
Manager.start = Manager.pullEvents
UI.exitPullEvents = Event.exitPullEvents
UI.quit = Event.exitPullEvents
UI.start = UI.pullEvents
local UI = Manager()
UI:init()
--[[-- Basic drawable area --]]--
UI.Window = class()
UI.Window = class(Canvas)
UI.Window.uid = 1
UI.Window.docs = { }
UI.Window.defaults = {
UIElement = 'Window',
x = 1,
y = 1,
-- z = 0, -- eventually...
offx = 0,
offy = 0,
cursorX = 1,
@ -413,7 +442,9 @@ function UI.Window:init(args)
local defaults = args
local m = getmetatable(self) -- get the class for this instance
repeat
defaults = UI:getDefaults(m, defaults)
if m ~= Canvas then
defaults = UI:getDefaults(m, defaults)
end
m = m._base
until not m
UI:mergeProperties(self, defaults)
@ -438,6 +469,9 @@ function UI.Window:init(args)
until not m
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()
if self.parent then
-- 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
self.height = self.parent.height - self.y + 1
end
self.width = math.max(self.width, 1)
self.height = math.max(self.height, 1)
self:reposition(self.x, self.y, self.width, self.height)
end
-- Called when the window's parent has be assigned
function UI.Window:setParent()
self.oh, self.ow = self.height, self.width
self.ox, self.oy = self.x, self.y
self.oex, self.oey = self.ex, self.ey
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()
end
function UI.Window:resize()
self.height, self.width = self.oh, self.ow
self.x, self.y = self.ox, self.oy
self.ex, self.ey = self.oex, self.oey
self:layout()
if self.children then
for _,child in ipairs(self.children) do
for child in self:eachChild() do
child:resize()
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)
Add element(s) to a window. Example:
page:add({
@ -584,6 +631,20 @@ function UI.Window:add(children)
self:initChildren()
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()
return self.cursorX, self.cursorY
end
@ -595,18 +656,20 @@ function UI.Window:setCursorPos(x, y)
end
function UI.Window:setCursorBlink(blink)
self.parent:setCursorBlink(blink)
self.cursorBlink = blink
end
UI.Window.docs.draw = [[draw(VOID)
Redraws the window in the internal buffer.]]
function UI.Window:draw()
self:clear(self.backgroundColor)
if self.children then
for _,child in pairs(self.children) do
if child.enabled then
child:draw()
end
self:clear()
self:drawChildren()
end
function UI.Window:drawChildren()
for child in self:eachChild() do
if child.enabled then
child:draw()
end
end
end
@ -633,19 +696,38 @@ function UI.Window:sync()
end
function UI.Window:enable(...)
self.enabled = true
if self.children then
for _,child in pairs(self.children) do
child:enable(...)
if not self.enabled then
self.enabled = true
if self.transitionHint then
self:addTransition(self.transitionHint)
end
if self.modal then
self:raise()
self:capture(self)
end
for child in self:eachChild() do
if not child.enabled then
child:enable(...)
end
end
end
end
function UI.Window:disable()
self.enabled = false
if self.children then
for _,child in pairs(self.children) do
child:disable()
if self.enabled then
self.enabled = false
self.parent:dirty(true)
if self.modal then
self:release(self)
end
for child in self:eachChild() do
if child.enabled then
child:disable()
end
end
end
end
@ -656,13 +738,9 @@ function UI.Window:setTextScale(textScale)
end
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)
if self.canvas then
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
Canvas.clear(self, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
end
function UI.Window:clearLine(y, bg)
@ -670,39 +748,28 @@ function UI.Window:clearLine(y, bg)
end
function UI.Window:clearArea(x, y, width, height, bg)
self:fillArea(x, y, width, height, ' ', bg)
end
function UI.Window:fillArea(x, y, width, height, fillChar, bg, fg)
if width > 0 then
local filler = _rep(' ', width)
local filler = _rep(fillChar, width)
for i = 0, height - 1 do
self:write(x, y + i, filler, bg)
self:write(x, y + i, filler, bg, fg)
end
end
end
function UI.Window:write(x, y, text, bg, fg)
bg = bg or self.backgroundColor
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
Canvas.write(self, x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
end
function UI.Window:centeredWrite(y, text, bg, fg)
if #text >= self.width then
self:write(1, y, text, bg, fg)
else
local space = math.floor((self.width-#text) / 2)
local filler = _rep(' ', space + 1)
local str = _sub(filler, 1, space) .. text
str = str .. _sub(filler, self.width - #str + 1)
self:write(1, y, str, bg, fg)
local x = math.floor((self.width-#text) / 2) + 1
self:write(x, y, text, bg, fg)
end
end
@ -710,89 +777,19 @@ function UI.Window:print(text, bg, fg)
local marginLeft = self.marginLeft or 0
local marginRight = self.marginRight or 0
local width = self.width - marginLeft - marginRight
local cs = {
bg = bg or self:getProperty('backgroundColor'),
fg = fg or self:getProperty('textColor'),
palette = self.palette,
}
local function nextWord(line, cx)
local result = { line:find("(%w+)", cx) }
if #result > 1 and result[2] > cx then
return _sub(line, cx, result[2] + 1)
elseif #result > 0 and result[1] == cx then
result = { line:find("(%w+)", result[2]) }
if #result > 0 then
return _sub(line, cx, result[1] + 1)
end
end
if cx <= #line then
return _sub(line, cx, #line)
local y = (self.marginTop or 0) + 1
for _,line in pairs(Util.split(text)) do
for _, ln in ipairs(Blit(line, cs):wrap(width)) do
self:blit(marginLeft + 1, y, ln.text, ln.bg, ln.fg)
y = y + 1
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
UI.Window.docs.focus = [[focus(VOID)
@ -822,11 +819,11 @@ function UI.Window:release(child)
end
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
y = y + self.offy - self.y + 1
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
x >= child.x and x < child.x + child.width and
y >= child.y and y < child.y + child.height then
@ -868,7 +865,7 @@ function UI.Window:getFocusables()
end
if self.children then
getFocusable(self, self.x, self.y)
getFocusable(self)
end
return focusable
@ -882,36 +879,28 @@ function UI.Window:focusFirst()
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()
local parent = self.parent
local offx, offy = parent.offx, parent.offy
if self.x <= parent.offx then
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
parent.offx = self.x + self.width - parent.width - 1
parent:draw()
if offx ~= parent.offx then
parent:draw()
end
end
-- TODO: fix
local function setOffset(y)
parent.offy = y
if parent.canvas then
parent.canvas.offy = parent.offy
if offy ~= parent.offy then
parent:draw()
end
parent:draw()
end
if self.y <= parent.offy then
@ -921,45 +910,8 @@ function UI.Window:scrollIntoView()
end
end
function UI.Window:getCanvas()
local el = 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
function UI.Window:addTransition(effect, args, canvas)
self.parent:addTransition(effect, args, canvas or self)
end
function UI.Window:emit(event)
@ -991,7 +943,16 @@ function UI.Window:getProperty(property)
end
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
function UI.Window:eventHandler()
@ -1010,18 +971,14 @@ UI.Device.defaults = {
function UI.Device:postInit()
self.device = self.device or term.current()
--if self.deviceType then
-- self.device = device[self.deviceType]
--end
if not self.device.setTextScale then
self.device.setTextScale = function() end
end
self.device.setTextScale(self.textScale)
self.width, self.height = self.device.getSize()
self.isColor = self.device.isColor()
Canvas.init(self, { isColor = self.isColor })
UI.devices[self.device.side or 'terminal'] = self
end
@ -1031,8 +988,8 @@ function UI.Device:resize()
self.width, self.height = self.device.getSize()
self.lines = { }
-- TODO: resize all pages added to this device
self.canvas:resize(self.width, self.height)
self.canvas:clear(self.backgroundColor, self.textColor)
Canvas.resize(self, self.width, self.height)
Canvas.clear(self, self.backgroundColor, self.textColor)
end
function UI.Device:setCursorPos(x, y)
@ -1046,7 +1003,6 @@ end
function UI.Device:setCursorBlink(blink)
self.cursorBlink = blink
self.device.setCursorBlink(blink)
end
function UI.Device:setTextScale(textScale)
@ -1061,27 +1017,30 @@ function UI.Device:reset()
self.device.setCursorPos(1, 1)
end
function UI.Device:addTransition(effect, args)
function UI.Device:addTransition(effect, args, canvas)
if not self.transitions then
self.transitions = { }
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
effect = Transition[effect]
if not effect then
error('Invalid transition')
effect = Transition[effect] or error('Invalid transition')
end
-- there can be only one
for k,v in pairs(self.transitions) do
if v.canvas == canvas then
table.remove(self.transitions, k)
break
end
end
table.insert(self.transitions, { update = effect(args), args = args })
table.insert(self.transitions, { effect = effect, args = args or { }, canvas = canvas })
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
for _,k in ipairs(Util.keys(transitions)) do
local transition = transitions[k]
@ -1089,7 +1048,7 @@ function UI.Device:runTransitions(transitions, canvas)
transitions[k] = nil
end
end
canvas:render(self.device)
self.currentPage:render(self, true)
if Util.empty(transitions) then
break
end
@ -1101,17 +1060,19 @@ function UI.Device:sync()
local transitions = self.effectsEnabled and self.transitions
self.transitions = nil
if self:getCursorBlink() then
self.device.setCursorBlink(false)
end
self.device.setCursorBlink(false)
self.canvas:render(self.device)
if transitions then
self:runTransitions(transitions, self.canvas)
self:runTransitions(transitions)
else
self.currentPage:render(self, true)
end
if self:getCursorBlink() then
self.device.setCursorPos(self.cursorX, self.cursorY)
if self.isColor then
self.device.setTextColor(colors.orange)
end
self.device.setCursorBlink(true)
end
end
@ -1143,7 +1104,7 @@ local function loadComponents()
return self(...)
end
})
UI[name]._preload = function(self)
UI[name]._preload = function()
return load(name)
end
end
@ -1152,6 +1113,12 @@ end
loadComponents()
UI:loadTheme('usr/config/ui.theme')
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

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

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

View File

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

View File

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

View File

@ -1,15 +1,11 @@
local Canvas = require('opus.ui.canvas')
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.Dialog = class(UI.SlideOut)
UI.Dialog.defaults = {
UIElement = 'Dialog',
height = 7,
textColor = colors.black,
backgroundColor = colors.white,
noFill = true,
okEvent ='dialog_ok',
cancelEvent = 'dialog_cancel',
}
@ -18,22 +14,36 @@ function UI.Dialog:postInit()
self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title })
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)
if event.type == 'dialog_cancel' then
self:hide()
end
return UI.SlideOut.eventHandler(self, event)
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 Util = require('opus.util')
local colors = _G.colors
UI.DropMenu = class(UI.MenuBar)
UI.DropMenu.defaults = {
UIElement = 'DropMenu',
backgroundColor = colors.white,
backgroundColor = 'white',
buttonClass = 'DropMenuItem',
}
function UI.DropMenu:layout()
@ -32,42 +30,53 @@ function UI.DropMenu:layout()
self.height = #self.children + 1
self.width = maxWidth + 2
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
if self.x + self.width > self.parent.width then
self.x = self.parent.width - self.width + 1
end
self:reposition(self.x, self.y, self.width, self.height)
end
function UI.DropMenu:enable()
end
local menuBar = self.parent:find(self.menuUid)
local hasActive
function UI.DropMenu:show(x, y)
self.x, self.y = x, y
self.canvas:move(x, y)
self.canvas:setVisible(true)
for _,c in pairs(self.children) do
if not c.spacer and menuBar then
c.inactive = not menuBar:getActive(c)
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)
if self.focus then
self:setFocus(self)
else
self:focusFirst()
end
self:draw()
self:capture(self)
self:focusFirst()
end
function UI.DropMenu:hide()
self:disable()
self.canvas:setVisible(false)
self:release(self)
function UI.DropMenu:disable()
UI.Window.disable(self)
self:remove()
end
function UI.DropMenu:eventHandler(event)
if event.type == 'focus_lost' and self.enabled then
if not Util.contains(self.children, event.focused) then
self:hide()
if not (Util.contains(self.children, event.focused) or event.focused == self) then
self:disable()
end
elseif event.type == 'mouse_out' and self.enabled then
self:hide()
self:refocus()
self:disable()
self:setFocus(self.parent:find(self.lastFocus))
else
return UI.MenuBar.eventHandler(self, event)
end
@ -83,6 +92,15 @@ function UI.DropMenu.example()
{ spacer = true },
{ 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

View File

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

View File

@ -1,62 +1,63 @@
local class = require('opus.class')
local Event = require('opus.event')
local Terminal = require('opus.terminal')
local UI = require('opus.ui')
local colors = _G.colors
UI.Embedded = class(UI.Window)
UI.Embedded.defaults = {
UIElement = 'Embedded',
backgroundColor = colors.black,
textColor = colors.white,
backgroundColor = 'black',
textColor = 'white',
maxScroll = 100,
accelerators = {
up = 'scroll_up',
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()
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
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
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)
self.canvas:dirty()
self.win.setVisible(true)
self:dirty()
end
function UI.Embedded:disable()
self.canvas:setVisible(false)
self.win.setVisible(false)
UI.Window.disable(self)
end
@ -71,17 +72,12 @@ function UI.Embedded:eventHandler(event)
end
end
function UI.Embedded:focus()
-- allow scrolling
end
function UI.Embedded.example()
local Event = require('opus.event')
local Util = require('opus.util')
local term = _G.term
return UI.Embedded {
visible = true,
y = 2, x = 2, ex = -2, ey = -2,
enable = function (self)
UI.Embedded.enable(self)
Event.addRoutine(function()
@ -90,10 +86,11 @@ function UI.Embedded.example()
term.redirect(oterm)
end)
end,
eventHandler = function(_, event)
eventHandler = function(self, event)
if event.type == 'key' then
return true
end
return UI.Embedded.eventHandler(self, event)
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 UI = require('opus.ui')
local colors = _G.colors
UI.Form = class(UI.Window)
UI.Form.defaults = {
UIElement = 'Form',
@ -68,7 +66,7 @@ function UI.Form:createForm()
table.insert(self.children, UI.Text {
x = self.margin,
y = child.y,
textColor = colors.black,
textColor = 'black',
width = #child.formLabel,
value = child.formLabel,
})

View File

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

View File

@ -1,19 +1,28 @@
local class = require('opus.class')
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.defaults = {
UIElement = 'Image',
event = 'button_press',
}
function UI.Image:setParent()
if self.image then
function UI.Image:postInit()
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
end
if self.image and not self.width then
self.width = #self.image[1]
if self.image and not (self.width or self.ex) then
for i = 1, self.height do
self.width = math.max(self.width or 0, #self.image[i])
end
end
UI.Window.setParent(self)
end
function UI.Image:draw()
@ -22,19 +31,22 @@ function UI.Image:draw()
for y = 1, #self.image do
local line = self.image[y]
for x = 1, #line do
local ch = line[x]
if type(ch) == 'number' then
if ch > 0 then
self:write(x, y, ' ', ch)
end
else
self:write(x, y, ch)
local ch = lookup:find(line:sub(x, x))
if ch then
self:write(x, y, ' ', 2 ^ (ch -1))
end
end
end
end
self:drawChildren()
end
function UI.Image:setImage(image)
self.image = image
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
end
function UI.Menu:setParent()
UI.Grid.setParent(self)
function UI.Menu:layout()
self.itemWidth = 1
for _,v in pairs(self.values) do
if #v.prompt > self.itemWidth then
@ -28,6 +27,7 @@ function UI.Menu:setParent()
else
self.width = self.itemWidth + 2
end
UI.Grid.layout(self)
end
function UI.Menu:center()

View File

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

View File

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

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 = {
UIElement = 'NftImage',
}
function UI.NftImage:setParent()
if self.image then
function UI.NftImage:postInit()
if self.image and not (self.ey or self.height) then
self.height = self.image.height
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
end
UI.Window.setParent(self)
end
function UI.NftImage:draw()
self:clear()
if self.image then
-- due to blittle, the background and foreground transparent
-- 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)
end
end
else
self:clear()
end
end

View File

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

View File

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

View File

@ -1,39 +1,31 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.ProgressBar = class(UI.Window)
UI.ProgressBar.defaults = {
UIElement = 'ProgressBar',
backgroundColor = colors.gray,
backgroundColor = 'gray',
height = 1,
progressColor = colors.lime,
progressColor = 'lime',
progressChar = UI.extChars and '\153' or ' ',
fillChar = ' ',
fillColor = colors.gray,
textColor = colors.green,
fillColor = 'gray',
textColor = 'green',
value = 0,
}
function UI.ProgressBar:draw()
local width = math.ceil(self.value / 100 * self.width)
local filler = string.rep(self.fillChar, self.width)
local progress = string.rep(self.progressChar, width)
for i = 1, self.height do
self:write(1, i, filler, nil, self.fillColor)
self:write(1, i, progress, self.progressColor)
end
self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor)
self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor)
end
function UI.ProgressBar.example()
local Event = require('opus.event')
return UI.ProgressBar {
x = 2, ex = -2, y = 2,
x = 2, ex = -2, y = 2, height = 2,
focus = function() end,
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:draw()
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,
}
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
local maxScroll = view.totalHeight - view.height
@ -27,7 +33,7 @@ function UI.ScrollBar:draw()
local row = view.y
if not view.static then -- does the container scroll ?
self.height = view.totalHeight
self:reposition(self.x, self.y, self.width, view.totalHeight)
end
for i = 1, view.height - 2 do
@ -56,16 +62,17 @@ end
function UI.ScrollBar:eventHandler(event)
if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' 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 event.y == view.y then
self:emit({ type = 'scroll_up'})
parent:emit({ type = 'scroll_up'})
elseif event.y == view.y + view.height - 1 then
self:emit({ type = 'scroll_down'})
parent:emit({ type = 'scroll_down'})
else
local percent = (event.y - view.y) / (view.height - 2)
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
return true

View File

@ -29,6 +29,7 @@ function UI.ScrollingGrid:getViewArea()
height = self.pageSize, -- viewable height
totalHeight = Util.size(self.values), -- total height
offsetY = self.scrollOffset, -- scroll offset
fill = not self.disableHeader and self.headerBackgroundColor,
}
end
@ -57,3 +58,21 @@ function UI.ScrollingGrid:setIndex(index)
end
UI.Grid.setIndex(self, index)
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.defaults = {
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()
end
@ -27,24 +19,20 @@ function UI.SlideOut:toggle()
end
function UI.SlideOut:show(...)
self:addTransition('expandUp')
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...)
self:draw()
self:capture(self)
self:focusFirst()
end
function UI.SlideOut:disable()
self.canvas:setVisible(false)
UI.Window.disable(self)
end
function UI.SlideOut:hide()
self:disable()
self:release(self)
self:refocus()
end
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
function UI.SlideOut:eventHandler(event)
@ -59,24 +47,27 @@ function UI.SlideOut:eventHandler(event)
end
function UI.SlideOut.example()
-- for the transistion to work properly, the parent must have a canvas
return UI.ActiveLayer {
y = 2,
return UI.Window {
y = 3,
backgroundColor = 2048,
button = UI.Button {
x = 2, y = 5,
text = 'show',
},
slideOut = UI.SlideOut {
backgroundColor = _G.colors.yellow,
y = -4, height = 4, x = 3, ex = -3,
backgroundColor = 16,
y = -7, height = 4, x = 3, ex = -3,
titleBar = UI.TitleBar {
title = 'test',
},
button = UI.Button {
x = 2, y = 2,
text = 'hide',
--visualize = true,
},
},
eventHandler = function (self, event)
if event.type == 'button_press' then
self.slideOut.canvas.xxx = true
self.slideOut:toggle()
end
end,

View File

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

View File

@ -3,17 +3,16 @@ local Event = require('opus.event')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.StatusBar = class(UI.Window)
UI.StatusBar.defaults = {
UIElement = 'StatusBar',
backgroundColor = colors.lightGray,
textColor = colors.gray,
backgroundColor = 'lightGray',
textColor = 'gray',
height = 1,
ey = -1,
}
function UI.StatusBar:adjustWidth()
function UI.StatusBar:layout()
UI.Window.layout(self)
-- Can only have 1 adjustable width
if self.columns then
local w = self.width - #self.columns - 1
@ -31,16 +30,6 @@ function UI.StatusBar:adjustWidth()
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)
if self.values ~= status then
self.values = status
@ -63,7 +52,7 @@ end
function UI.StatusBar:timedStatus(status, timeout)
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
self:draw()
self:sync()
@ -89,11 +78,13 @@ function UI.StatusBar:draw()
elseif type(self.values) == 'string' then
self:write(1, 1, Util.widthify(' ' .. self.values, self.width))
else
local s = ''
local x = 2
self:clear()
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
self:write(1, 1, Util.widthify(s, self.width))
end
end

View File

@ -1,9 +1,16 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.Tab = class(UI.ActiveLayer)
UI.Tab = class(UI.Window)
UI.Tab.defaults = {
UIElement = 'Tab',
tabTitle = 'tab',
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 = {
UIElement = 'TabBar',
buttonClass = 'TabBarMenuItem',
}
UI.TabBar.inherits = {
selectedBackgroundColor = 'backgroundColor',
backgroundColor = 'black',
selectedBackgroundColor = 'primary',
unselectedBackgroundColor = 'tertiary',
}
function UI.TabBar:enable()
UI.MenuBar.enable(self)
@ -32,7 +32,7 @@ function UI.TabBar:eventHandler(event)
self:emit({ type = 'tab_change', current = si, last = pi, tab = selected })
end
end
UI.MenuBar.draw(self)
self:draw(self)
end
return UI.MenuBar.eventHandler(self, event)
end

View File

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

View File

@ -56,12 +56,12 @@ end
function UI.Tabs:enable()
self.enabled = true
self.transitionHint = nil
self.tabBar:enable()
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
child:enable()
self:emit({ type = 'tab_activate', activated = child })
@ -74,14 +74,11 @@ end
function UI.Tabs:eventHandler(event)
if event.type == 'tab_change' then
local tab = self:find(event.tab.tabUid)
if event.current > event.last then
self.transitionHint = 'slideLeft'
else
self.transitionHint = 'slideRight'
end
local hint = event.current > event.last and 'slideLeft' or 'slideRight'
for _,child in pairs(self.children) do
for child in self:eachChild() do
if child.uid == event.tab.tabUid then
child.transitionHint = hint
child:enable()
elseif child.tabTitle then
child:disable()
@ -89,6 +86,7 @@ function UI.Tabs:eventHandler(event)
end
self:emit({ type = 'tab_activate', activated = tab })
tab:draw()
return true
end
end
@ -102,11 +100,26 @@ function UI.Tabs.example()
tab2 = UI.Tab {
index = 2,
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 {
index = 3,
tabTitle = 'tab3',
},
enable = function(self)
UI.Tabs.enable(self)
self:setActive(self.tab3, false)
end,
}
end

View File

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

View File

@ -6,36 +6,44 @@ UI.TextArea.defaults = {
UIElement = 'TextArea',
marginRight = 2,
value = '',
showScrollBar = true,
}
function UI.TextArea:postInit()
self.scrollBar = UI.ScrollBar()
end
function UI.TextArea:setText(text)
self:reset()
self.value = text
self:draw()
end
function UI.TextArea:focus()
function UI.TextArea.focus()
-- allow keyboard scrolling
end
function UI.TextArea:draw()
self:clear()
-- self:setCursorPos(1, 1)
self.cursorX, self.cursorY = 1, 1
self:print(self.value)
for _,child in pairs(self.children) do
if child.enabled then
child:draw()
end
end
self:drawChildren()
end
function UI.TextArea.example()
return UI.TextArea {
value = 'sample text\nabc'
local Ansi = require('opus.ansi')
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

View File

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

View File

@ -20,24 +20,15 @@ UI.Throttle.defaults = {
' //) (O ). @ \\-d ) (@ '
}
}
function UI.Throttle:setParent()
function UI.Throttle:layout()
self.x = math.ceil((self.parent.width - self.width) / 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
function UI.Throttle:enable()
self.c = os.clock()
self.enabled = false
end
function UI.Throttle:disable()
if self.canvas then
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
self.ctr = 0
end
self.ctr = 0
end
function UI.Throttle:update()
@ -46,11 +37,7 @@ function UI.Throttle:update()
os.sleep(0)
self.c = os.clock()
self.enabled = true
if not self.canvas then
self.canvas = self:addLayer(self.backgroundColor, self.borderColor)
self.canvas:setVisible(true)
self:clear(self.borderColor)
end
self:clear(self.borderColor)
local image = self.image[self.ctr + 1]
local width = self.width - 2
for i = 0, #self.image do
@ -63,3 +50,25 @@ function UI.Throttle:update()
self:sync()
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 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.defaults = {
UIElement = 'TitleBar',
height = 1,
textColor = colors.white,
backgroundColor = colors.cyan,
title = '',
frameChar = UI.extChars and '\140' or '-',
closeInd = UI.extChars and '\215' or '*',
}
function UI.TitleBar:draw()
local sb = SB(self.width)
sb:fill(2, self.frameChar, sb.width - 3)
sb:center(string.format(' %s ', self.title))
self:fillArea(2, 1, self.width - 2, 1, self.frameChar)
self:centeredWrite(1, string.format(' %s ', self.title))
if self.previousPage or self.event then
sb:insert(-1, self.closeInd)
else
sb:insert(-2, self.frameChar)
self:write(self.width - 1, 1, ' ' .. self.closeInd)
end
self:write(1, 1, sb:get())
end
function UI.TitleBar:eventHandler(event)
@ -69,5 +30,74 @@ function UI.TitleBar:eventHandler(event)
end
return true
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
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 UI = require('opus.ui')
local colors = _G.colors
UI.VerticalMeter = class(UI.Window)
UI.VerticalMeter.defaults = {
UIElement = 'VerticalMeter',
backgroundColor = colors.gray,
meterColor = colors.lime,
backgroundColor = 'gray',
meterColor = 'lime',
width = 1,
value = 0,
}
@ -18,12 +16,11 @@ function UI.VerticalMeter:draw()
end
function UI.VerticalMeter.example()
local Event = require('opus.event')
return UI.VerticalMeter {
x = 2, width = 3, y = 2, ey = -2,
focus = function() end,
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:draw()
self:sync()

View File

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

View File

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

View File

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

View File

@ -2,50 +2,85 @@ local Tween = require('opus.ui.tween')
local Transition = { }
function Transition.slideLeft(args)
local ticks = args.ticks or 10
local easing = args.easing or 'outQuint'
local pos = { x = args.ex }
local tween = Tween.new(ticks, pos, { x = args.x }, easing)
function Transition.slideLeft(canvas, args)
local ticks = args.ticks or 6
local easing = args.easing or 'inCirc'
local pos = { x = canvas.ex }
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()
local finished = tween:update(1)
args.canvas:move(math.floor(pos.x), args.canvas.y)
args.canvas:dirty()
canvas:move(math.floor(pos.x), canvas.y)
canvas:dirty(true)
return not finished
end
end
function Transition.slideRight(args)
local ticks = args.ticks or 10
local easing = args.easing or'outQuint'
local pos = { x = -args.canvas.width }
function Transition.slideRight(canvas, args)
local ticks = args.ticks or 6
local easing = args.easing or 'inCirc'
local pos = { x = -canvas.width }
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()
local finished = tween:update(1)
args.canvas:move(math.floor(pos.x), args.canvas.y)
args.canvas:dirty()
canvas:move(math.floor(pos.x), canvas.y)
canvas:dirty(true)
return not finished
end
end
function Transition.expandUp(args)
function Transition.expandUp(canvas, args)
local ticks = args.ticks or 3
local easing = args.easing or 'linear'
local pos = { y = args.ey + 1 }
local tween = Tween.new(ticks, pos, { y = args.y }, easing)
local pos = { y = canvas.ey + 1 }
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()
local finished = tween:update(1)
args.canvas:move(args.x, math.floor(pos.y))
args.canvas:dirty()
canvas:move(canvas.x, math.floor(pos.y))
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
end
end

View File

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