diff --git a/.gitignore b/.gitignore index b69bcf7..8852524 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /ignore +.project diff --git a/sys/apps/Files.lua b/sys/apps/Files.lua index b39ddd9..65c806d 100644 --- a/sys/apps/Files.lua +++ b/sys/apps/Files.lua @@ -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() diff --git a/sys/apps/Help.lua b/sys/apps/Help.lua index 3702371..ba530c8 100644 --- a/sys/apps/Help.lua +++ b/sys/apps/Help.lua @@ -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 = { diff --git a/sys/apps/Lua.lua b/sys/apps/Lua.lua index 2de6195..44f2c78 100644 --- a/sys/apps/Lua.lua +++ b/sys/apps/Lua.lua @@ -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() diff --git a/sys/apps/Network.lua b/sys/apps/Network.lua index 6ae002a..1f03fe4 100644 --- a/sys/apps/Network.lua +++ b/sys/apps/Network.lua @@ -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() diff --git a/sys/apps/Overview.lua b/sys/apps/Overview.lua index c93f516..b879fd0 100644 --- a/sys/apps/Overview.lua +++ b/sys/apps/Overview.lua @@ -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() diff --git a/sys/apps/PackageManager.lua b/sys/apps/PackageManager.lua index 3a3dd8b..ec8d9ea 100644 --- a/sys/apps/PackageManager.lua +++ b/sys/apps/PackageManager.lua @@ -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() diff --git a/sys/apps/Sniff.lua b/sys/apps/Sniff.lua index 3b4ca9c..bec9ac0 100644 --- a/sys/apps/Sniff.lua +++ b/sys/apps/Sniff.lua @@ -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() diff --git a/sys/apps/System.lua b/sys/apps/System.lua index b18200d..7005d83 100644 --- a/sys/apps/System.lua +++ b/sys/apps/System.lua @@ -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() diff --git a/sys/apps/Tasks.lua b/sys/apps/Tasks.lua index cde082a..f8c2edd 100644 --- a/sys/apps/Tasks.lua +++ b/sys/apps/Tasks.lua @@ -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() diff --git a/sys/apps/Welcome.lua b/sys/apps/Welcome.lua index 980f27f..1154fbf 100644 --- a/sys/apps/Welcome.lua +++ b/sys/apps/Welcome.lua @@ -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() diff --git a/sys/apps/fileui.lua b/sys/apps/fileui.lua new file mode 100644 index 0000000..1f95f5e --- /dev/null +++ b/sys/apps/fileui.lua @@ -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 diff --git a/sys/apps/inspect.lua b/sys/apps/inspect.lua index 1d4f95b..35a9f5c 100644 --- a/sys/apps/inspect.lua +++ b/sys/apps/inspect.lua @@ -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() diff --git a/sys/apps/network/keygen.lua b/sys/apps/network/keygen.lua index 4a74670..ea2ae40 100644 --- a/sys/apps/network/keygen.lua +++ b/sys/apps/network/keygen.lua @@ -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) diff --git a/sys/apps/shell.lua b/sys/apps/shell.lua index 6124a3d..7036afa 100644 --- a/sys/apps/shell.lua +++ b/sys/apps/shell.lua @@ -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 diff --git a/sys/apps/system/aliases.lua b/sys/apps/system/aliases.lua index 6b04c5c..2d122bb 100644 --- a/sys/apps/system/aliases.lua +++ b/sys/apps/system/aliases.lua @@ -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' }, diff --git a/sys/apps/system/alternate.lua b/sys/apps/system/alternate.lua index 6894428..e903074 100644 --- a/sys/apps/system/alternate.lua +++ b/sys/apps/system/alternate.lua @@ -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 = { } diff --git a/sys/apps/system/cloud.lua b/sys/apps/system/cloud.lua index 66078f6..d837f2e 100644 --- a/sys/apps/system/cloud.lua +++ b/sys/apps/system/cloud.lua @@ -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: diff --git a/sys/apps/system/diskusage.lua b/sys/apps/system/diskusage.lua index 98b8516..c0db595 100644 --- a/sys/apps/system/diskusage.lua +++ b/sys/apps/system/diskusage.lua @@ -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) }, } diff --git a/sys/apps/system/kiosk.lua b/sys/apps/system/kiosk.lua index becee72..2f9510b 100644 --- a/sys/apps/system/kiosk.lua +++ b/sys/apps/system/kiosk.lua @@ -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 +} diff --git a/sys/apps/system/label.lua b/sys/apps/system/label.lua index 0dc77a7..8fe3d1e 100644 --- a/sys/apps/system/label.lua +++ b/sys/apps/system/label.lua @@ -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 diff --git a/sys/apps/system/launcher.lua b/sys/apps/system/launcher.lua index 6bd290c..9c87fe9 100644 --- a/sys/apps/system/launcher.lua +++ b/sys/apps/system/launcher.lua @@ -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', }, } diff --git a/sys/apps/system/network.lua b/sys/apps/system/network.lua index 414c080..b4fd802 100644 --- a/sys/apps/system/network.lua +++ b/sys/apps/system/network.lua @@ -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 +} diff --git a/sys/apps/system/password.lua b/sys/apps/system/password.lua index d822996..8040c76 100644 --- a/sys/apps/system/password.lua +++ b/sys/apps/system/password.lua @@ -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 +} diff --git a/sys/apps/system/path.lua b/sys/apps/system/path.lua index 33249dd..eef9f16 100644 --- a/sys/apps/system/path.lua +++ b/sys/apps/system/path.lua @@ -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, diff --git a/sys/apps/system/settings.lua b/sys/apps/system/settings.lua index 8e071ac..27f1f2b 100644 --- a/sys/apps/system/settings.lua +++ b/sys/apps/system/settings.lua @@ -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, +} diff --git a/sys/apps/system/shell.lua b/sys/apps/system/shell.lua index 13dfda6..8ca32fe 100644 --- a/sys/apps/system/shell.lua +++ b/sys/apps/system/shell.lua @@ -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 diff --git a/sys/apps/system/theme.lua b/sys/apps/system/theme.lua new file mode 100644 index 0000000..b9d4ed4 --- /dev/null +++ b/sys/apps/system/theme.lua @@ -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 +} diff --git a/sys/autorun/clipboard.lua b/sys/autorun/clipboard.lua index 992af47..2a0774c 100644 --- a/sys/autorun/clipboard.lua +++ b/sys/autorun/clipboard.lua @@ -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) diff --git a/sys/autorun/upgraded.lua b/sys/autorun/upgraded.lua index 1cb0c1a..cc1063d 100644 --- a/sys/autorun/upgraded.lua +++ b/sys/autorun/upgraded.lua @@ -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') \ No newline at end of file diff --git a/sys/etc/apps.db b/sys/etc/apps.db index e753fd1..f342709 100644 --- a/sys/etc/apps.db +++ b/sys/etc/apps.db @@ -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" ] = { diff --git a/sys/help/Overview b/sys/help/Overview index d39a4dd..60b8c52 100644 --- a/sys/help/Overview +++ b/sys/help/Overview @@ -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. \ No newline at end of file +Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Magenta is used for transparency. \ No newline at end of file diff --git a/sys/init/1.device.lua b/sys/init/1.device.lua index b61a767..024a08f 100644 --- a/sys/init/1.device.lua +++ b/sys/init/1.device.lua @@ -1,5 +1,3 @@ -_G.requireInjector(_ENV) - local Peripheral = require('opus.peripheral') _G.device = Peripheral.getList() diff --git a/sys/init/2.vfs.lua b/sys/init/2.vfs.lua index 5b3c5f9..69eb387 100644 --- a/sys/init/2.vfs.lua +++ b/sys/init/2.vfs.lua @@ -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') diff --git a/sys/init/5.network.lua b/sys/init/5.network.lua index b02937a..d526b62 100644 --- a/sys/init/5.network.lua +++ b/sys/init/5.network.lua @@ -1,5 +1,3 @@ -_G.requireInjector(_ENV) - local Config = require('opus.config') local device = _G.device diff --git a/sys/init/6.packages.lua b/sys/init/6.packages.lua index 7d06abe..49c55bf 100644 --- a/sys/init/6.packages.lua +++ b/sys/init/6.packages.lua @@ -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 diff --git a/sys/init/7.multishell.lua b/sys/init/7.multishell.lua index d8b722a..1f44a0d 100644 --- a/sys/init/7.multishell.lua +++ b/sys/init/7.multishell.lua @@ -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({ diff --git a/sys/kernel.lua b/sys/kernel.lua index b2ef8a8..546300d 100644 --- a/sys/kernel.lua +++ b/sys/kernel.lua @@ -1,5 +1,3 @@ -_G.requireInjector(_ENV) - local Array = require('opus.array') local Terminal = require('opus.terminal') local Util = require('opus.util') diff --git a/sys/modules/opus/array.lua b/sys/modules/opus/array.lua index f0aa73f..6980a86 100644 --- a/sys/modules/opus/array.lua +++ b/sys/modules/opus/array.lua @@ -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 diff --git a/sys/modules/opus/config.lua b/sys/modules/opus/config.lua index 3518ba2..d41c1c7 100644 --- a/sys/modules/opus/config.lua +++ b/sys/modules/opus/config.lua @@ -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) diff --git a/sys/modules/opus/entry.lua b/sys/modules/opus/entry.lua index aa2397f..0dc3536 100644 --- a/sys/modules/opus/entry.lua +++ b/sys/modules/opus/entry.lua @@ -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, diff --git a/sys/modules/opus/fuzzy.lua b/sys/modules/opus/fuzzy.lua new file mode 100644 index 0000000..c71ac08 --- /dev/null +++ b/sys/modules/opus/fuzzy.lua @@ -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 diff --git a/sys/modules/opus/git.lua b/sys/modules/opus/git.lua index a06cc70..c4a17d7 100644 --- a/sys/modules/opus/git.lua +++ b/sys/modules/opus/git.lua @@ -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 diff --git a/sys/modules/opus/gps.lua b/sys/modules/opus/gps.lua index 3b0ae74..1132b76 100644 --- a/sys/modules/opus/gps.lua +++ b/sys/modules/opus/gps.lua @@ -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() diff --git a/sys/modules/opus/input.lua b/sys/modules/opus/input.lua index 46b58fe..956e0ba 100644 --- a/sys/modules/opus/input.lua +++ b/sys/modules/opus/input.lua @@ -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, diff --git a/sys/modules/opus/nft.lua b/sys/modules/opus/nft.lua index 4fd8d02..a12834d 100644 --- a/sys/modules/opus/nft.lua +++ b/sys/modules/opus/nft.lua @@ -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 diff --git a/sys/modules/opus/terminal.lua b/sys/modules/opus/terminal.lua index cb7df22..7b04a42 100644 --- a/sys/modules/opus/terminal.lua +++ b/sys/modules/opus/terminal.lua @@ -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 diff --git a/sys/modules/opus/ui.lua b/sys/modules/opus/ui.lua index c627005..625128f 100644 --- a/sys/modules/opus/ui.lua +++ b/sys/modules/opus/ui.lua @@ -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 diff --git a/sys/modules/opus/ui/blit.lua b/sys/modules/opus/ui/blit.lua new file mode 100644 index 0000000..a9774d5 --- /dev/null +++ b/sys/modules/opus/ui/blit.lua @@ -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 +}) diff --git a/sys/modules/opus/ui/canvas.lua b/sys/modules/opus/ui/canvas.lua index 6fe12b1..3003981 100644 --- a/sys/modules/opus/ui/canvas.lua +++ b/sys/modules/opus/ui/canvas.lua @@ -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 diff --git a/sys/modules/opus/ui/components/ActiveLayer.lua b/sys/modules/opus/ui/components/ActiveLayer.lua deleted file mode 100644 index dcbb499..0000000 --- a/sys/modules/opus/ui/components/ActiveLayer.lua +++ /dev/null @@ -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 diff --git a/sys/modules/opus/ui/components/Button.lua b/sys/modules/opus/ui/components/Button.lua index 2442614..426d3e0 100644 --- a/sys/modules/opus/ui/components/Button.lua +++ b/sys/modules/opus/ui/components/Button.lua @@ -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 { diff --git a/sys/modules/opus/ui/components/Checkbox.lua b/sys/modules/opus/ui/components/Checkbox.lua index 567833e..0322ad6 100644 --- a/sys/modules/opus/ui/components/Checkbox.lua +++ b/sys/modules/opus/ui/components/Checkbox.lua @@ -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() diff --git a/sys/modules/opus/ui/components/Chooser.lua b/sys/modules/opus/ui/components/Chooser.lua index 32fd3e2..6aa2e4a 100644 --- a/sys/modules/opus/ui/components/Chooser.lua +++ b/sys/modules/opus/ui/components/Chooser.lua @@ -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() diff --git a/sys/modules/opus/ui/components/Dialog.lua b/sys/modules/opus/ui/components/Dialog.lua index dd9de1a..b2d1639 100644 --- a/sys/modules/opus/ui/components/Dialog.lua +++ b/sys/modules/opus/ui/components/Dialog.lua @@ -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 diff --git a/sys/modules/opus/ui/components/DropMenu.lua b/sys/modules/opus/ui/components/DropMenu.lua index 41bdaf9..8e59a91 100644 --- a/sys/modules/opus/ui/components/DropMenu.lua +++ b/sys/modules/opus/ui/components/DropMenu.lua @@ -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 diff --git a/sys/modules/opus/ui/components/DropMenuItem.lua b/sys/modules/opus/ui/components/DropMenuItem.lua index ff28047..e1f2dd8 100644 --- a/sys/modules/opus/ui/components/DropMenuItem.lua +++ b/sys/modules/opus/ui/components/DropMenuItem.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Embedded.lua b/sys/modules/opus/ui/components/Embedded.lua index e15df33..f7996ce 100644 --- a/sys/modules/opus/ui/components/Embedded.lua +++ b/sys/modules/opus/ui/components/Embedded.lua @@ -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 diff --git a/sys/modules/opus/ui/components/FileSelect.lua b/sys/modules/opus/ui/components/FileSelect.lua new file mode 100644 index 0000000..55b86b7 --- /dev/null +++ b/sys/modules/opus/ui/components/FileSelect.lua @@ -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 diff --git a/sys/modules/opus/ui/components/FlatButton.lua b/sys/modules/opus/ui/components/FlatButton.lua new file mode 100644 index 0000000..c9a5ad0 --- /dev/null +++ b/sys/modules/opus/ui/components/FlatButton.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Form.lua b/sys/modules/opus/ui/components/Form.lua index bda9850..92a39a7 100644 --- a/sys/modules/opus/ui/components/Form.lua +++ b/sys/modules/opus/ui/components/Form.lua @@ -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, }) diff --git a/sys/modules/opus/ui/components/Grid.lua b/sys/modules/opus/ui/components/Grid.lua index 1d6e480..80189f5 100644 --- a/sys/modules/opus/ui/components/Grid.lua +++ b/sys/modules/opus/ui/components/Grid.lua @@ -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 { diff --git a/sys/modules/opus/ui/components/Image.lua b/sys/modules/opus/ui/components/Image.lua index 787d813..7d67c35 100644 --- a/sys/modules/opus/ui/components/Image.lua +++ b/sys/modules/opus/ui/components/Image.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Menu.lua b/sys/modules/opus/ui/components/Menu.lua index 0e18682..b5462cf 100644 --- a/sys/modules/opus/ui/components/Menu.lua +++ b/sys/modules/opus/ui/components/Menu.lua @@ -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() diff --git a/sys/modules/opus/ui/components/MenuBar.lua b/sys/modules/opus/ui/components/MenuBar.lua index 1b544cc..7ddf97b 100644 --- a/sys/modules/opus/ui/components/MenuBar.lua +++ b/sys/modules/opus/ui/components/MenuBar.lua @@ -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 diff --git a/sys/modules/opus/ui/components/MenuItem.lua b/sys/modules/opus/ui/components/MenuItem.lua index 61bc0b1..26bcf91 100644 --- a/sys/modules/opus/ui/components/MenuItem.lua +++ b/sys/modules/opus/ui/components/MenuItem.lua @@ -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', } diff --git a/sys/modules/opus/ui/components/MiniSlideOut.lua b/sys/modules/opus/ui/components/MiniSlideOut.lua new file mode 100644 index 0000000..4d68a0e --- /dev/null +++ b/sys/modules/opus/ui/components/MiniSlideOut.lua @@ -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 diff --git a/sys/modules/opus/ui/components/NftImage.lua b/sys/modules/opus/ui/components/NftImage.lua index 4d295d7..6c5158f 100644 --- a/sys/modules/opus/ui/components/NftImage.lua +++ b/sys/modules/opus/ui/components/NftImage.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Notification.lua b/sys/modules/opus/ui/components/Notification.lua index 701733e..2d52872 100644 --- a/sys/modules/opus/ui/components/Notification.lua +++ b/sys/modules/opus/ui/components/Notification.lua @@ -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, } diff --git a/sys/modules/opus/ui/components/Page.lua b/sys/modules/opus/ui/components/Page.lua index 7bc8e70..9daed63 100644 --- a/sys/modules/opus/ui/components/Page.lua +++ b/sys/modules/opus/ui/components/Page.lua @@ -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 diff --git a/sys/modules/opus/ui/components/ProgressBar.lua b/sys/modules/opus/ui/components/ProgressBar.lua index a066952..c769449 100644 --- a/sys/modules/opus/ui/components/ProgressBar.lua +++ b/sys/modules/opus/ui/components/ProgressBar.lua @@ -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() diff --git a/sys/modules/opus/ui/components/Question.lua b/sys/modules/opus/ui/components/Question.lua new file mode 100644 index 0000000..3be0d52 --- /dev/null +++ b/sys/modules/opus/ui/components/Question.lua @@ -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 diff --git a/sys/modules/opus/ui/components/ScrollBar.lua b/sys/modules/opus/ui/components/ScrollBar.lua index d4bef52..8f8b6ab 100644 --- a/sys/modules/opus/ui/components/ScrollBar.lua +++ b/sys/modules/opus/ui/components/ScrollBar.lua @@ -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 diff --git a/sys/modules/opus/ui/components/ScrollingGrid.lua b/sys/modules/opus/ui/components/ScrollingGrid.lua index 06dd5b8..864d7ad 100644 --- a/sys/modules/opus/ui/components/ScrollingGrid.lua +++ b/sys/modules/opus/ui/components/ScrollingGrid.lua @@ -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 \ No newline at end of file diff --git a/sys/modules/opus/ui/components/SlideOut.lua b/sys/modules/opus/ui/components/SlideOut.lua index 479be4b..efafd68 100644 --- a/sys/modules/opus/ui/components/SlideOut.lua +++ b/sys/modules/opus/ui/components/SlideOut.lua @@ -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, diff --git a/sys/modules/opus/ui/components/Slider.lua b/sys/modules/opus/ui/components/Slider.lua index 9a58cdb..620877e 100644 --- a/sys/modules/opus/ui/components/Slider.lua +++ b/sys/modules/opus/ui/components/Slider.lua @@ -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() diff --git a/sys/modules/opus/ui/components/StatusBar.lua b/sys/modules/opus/ui/components/StatusBar.lua index 96a1943..1b2d2e0 100644 --- a/sys/modules/opus/ui/components/StatusBar.lua +++ b/sys/modules/opus/ui/components/StatusBar.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Tab.lua b/sys/modules/opus/ui/components/Tab.lua index 1e31de6..d2e591b 100644 --- a/sys/modules/opus/ui/components/Tab.lua +++ b/sys/modules/opus/ui/components/Tab.lua @@ -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 diff --git a/sys/modules/opus/ui/components/TabBar.lua b/sys/modules/opus/ui/components/TabBar.lua index e684cae..eb81b3a 100644 --- a/sys/modules/opus/ui/components/TabBar.lua +++ b/sys/modules/opus/ui/components/TabBar.lua @@ -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 diff --git a/sys/modules/opus/ui/components/TabBarMenuItem.lua b/sys/modules/opus/ui/components/TabBarMenuItem.lua index f9f549b..0ade0b0 100644 --- a/sys/modules/opus/ui/components/TabBarMenuItem.lua +++ b/sys/modules/opus/ui/components/TabBarMenuItem.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Tabs.lua b/sys/modules/opus/ui/components/Tabs.lua index 2b0c824..8c2a692 100644 --- a/sys/modules/opus/ui/components/Tabs.lua +++ b/sys/modules/opus/ui/components/Tabs.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Text.lua b/sys/modules/opus/ui/components/Text.lua index 0250a48..5bf3799 100644 --- a/sys/modules/opus/ui/components/Text.lua +++ b/sys/modules/opus/ui/components/Text.lua @@ -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() diff --git a/sys/modules/opus/ui/components/TextArea.lua b/sys/modules/opus/ui/components/TextArea.lua index 2b8c342..3b821c7 100644 --- a/sys/modules/opus/ui/components/TextArea.lua +++ b/sys/modules/opus/ui/components/TextArea.lua @@ -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 \ No newline at end of file diff --git a/sys/modules/opus/ui/components/TextEntry.lua b/sys/modules/opus/ui/components/TextEntry.lua index 929cb3c..9fd61a5 100644 --- a/sys/modules/opus/ui/components/TextEntry.lua +++ b/sys/modules/opus/ui/components/TextEntry.lua @@ -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) diff --git a/sys/modules/opus/ui/components/Throttle.lua b/sys/modules/opus/ui/components/Throttle.lua index 0466486..d83662e 100644 --- a/sys/modules/opus/ui/components/Throttle.lua +++ b/sys/modules/opus/ui/components/Throttle.lua @@ -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 diff --git a/sys/modules/opus/ui/components/TitleBar.lua b/sys/modules/opus/ui/components/TitleBar.lua index ce7fdd4..743103a 100644 --- a/sys/modules/opus/ui/components/TitleBar.lua +++ b/sys/modules/opus/ui/components/TitleBar.lua @@ -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 diff --git a/sys/modules/opus/ui/components/VerticalMeter.lua b/sys/modules/opus/ui/components/VerticalMeter.lua index 051d740..8129990 100644 --- a/sys/modules/opus/ui/components/VerticalMeter.lua +++ b/sys/modules/opus/ui/components/VerticalMeter.lua @@ -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() @@ -31,4 +28,4 @@ function UI.VerticalMeter.example() return UI.VerticalMeter.enable(self) end } -end \ No newline at end of file +end diff --git a/sys/modules/opus/ui/components/Viewport.lua b/sys/modules/opus/ui/components/Viewport.lua index 35cd0eb..9389db5 100644 --- a/sys/modules/opus/ui/components/Viewport.lua +++ b/sys/modules/opus/ui/components/Viewport.lua @@ -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 diff --git a/sys/modules/opus/ui/components/Wizard.lua b/sys/modules/opus/ui/components/Wizard.lua index 549669e..0fcab17 100644 --- a/sys/modules/opus/ui/components/Wizard.lua +++ b/sys/modules/opus/ui/components/Wizard.lua @@ -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() diff --git a/sys/modules/opus/ui/components/WizardPage.lua b/sys/modules/opus/ui/components/WizardPage.lua index cb2c2de..da895ef 100644 --- a/sys/modules/opus/ui/components/WizardPage.lua +++ b/sys/modules/opus/ui/components/WizardPage.lua @@ -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, } diff --git a/sys/modules/opus/ui/transition.lua b/sys/modules/opus/ui/transition.lua index 4448760..24d07ee 100644 --- a/sys/modules/opus/ui/transition.lua +++ b/sys/modules/opus/ui/transition.lua @@ -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 diff --git a/sys/modules/opus/util.lua b/sys/modules/opus/util.lua index 383420e..8cd7286 100644 --- a/sys/modules/opus/util.lua +++ b/sys/modules/opus/util.lua @@ -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