From 98ec840db1c41c0e8db9467a8a9800dc004d3396 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 7 Oct 2017 23:03:18 -0400 Subject: [PATCH] UI improvements --- sys/apis/class.lua | 2 +- sys/apis/ui.lua | 283 ++++++++++++++++++++++++----------------- sys/apis/ui/canvas.lua | 15 ++- sys/apis/util.lua | 2 +- sys/apps/Files.lua | 25 ++-- sys/apps/Help.lua | 73 +++++++---- sys/apps/Overview.lua | 4 +- sys/etc/ext.theme | 2 +- 8 files changed, 240 insertions(+), 166 deletions(-) diff --git a/sys/apis/class.lua b/sys/apis/class.lua index 87f29e5..ced9020 100644 --- a/sys/apis/class.lua +++ b/sys/apis/class.lua @@ -36,7 +36,7 @@ return function(base) c.is_a = function(self, klass) local m = getmetatable(self) - while m do + while m do if m == klass then return true end m = m._base end diff --git a/sys/apis/ui.lua b/sys/apis/ui.lua index 0532bc2..e8a96c6 100644 --- a/sys/apis/ui.lua +++ b/sys/apis/ui.lua @@ -4,8 +4,10 @@ local Event = require('event') local Transition = require('ui.transition') local Util = require('util') -local _rep = string.rep -local _sub = string.sub +local _rep = string.rep +local _sub = string.sub +local colors = _G.colors +local keys = _G.keys local function safeValue(v) local t = type(v) @@ -147,7 +149,8 @@ function Manager:init() singleThread('char', function(ch) control = false if self.currentPage then - self:inputEvent(self.currentPage.focused, { type = 'key', key = ch }) + local target = self.currentPage.focused or self.currentPage + self:inputEvent(target, { type = 'key', key = ch }) self.currentPage:sync() end end) @@ -180,8 +183,9 @@ function Manager:init() -- as char events if ch and #ch > 1 and (code < 2 or code > 11) then if self.currentPage then - self:inputEvent(self.currentPage.focused, - { type = 'key', key = ch, element = self.currentPage.focused }) + local target = self.currentPage.focused or self.currentPage + self:inputEvent(target, + { type = 'key', key = ch, element = target }) self.currentPage:sync() end end @@ -264,10 +268,12 @@ end function Manager:inputEvent(parent, event) while parent do - local acc = parent.accelerators[event.key] - if acc then - if parent:emit({ type = acc, element = parent }) then - return true + if parent.accelerators then + local acc = parent.accelerators[event.key] + if acc then + if parent:emit({ type = acc, element = parent }) then + return true + end end end if parent.eventHandler then @@ -515,7 +521,7 @@ UI.Window.defaults = { offy = 0, cursorX = 1, cursorY = 1, - accelerators = { }, + -- accelerators = { }, } function UI.Window:init(args) local defaults = UI:getDefaults(UI.Window, args) @@ -727,8 +733,10 @@ function UI.Window:centeredWrite(y, text, bg, fg) end end -function UI.Window:print(text, bg, fg, indent) - indent = indent or 1 +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 function nextWord(line, cx) local result = { line:find("(%w+)", cx) } @@ -749,7 +757,7 @@ function UI.Window:print(text, bg, fg, indent) local pos = 1 local t = { } while true do - local s = f:find('\027', pos) + local s = string.find(f, '\027', pos, true) if not s then break end @@ -773,7 +781,7 @@ function UI.Window:print(text, bg, fg, indent) table.insert(t, e) pos = s + #seq + 3 end - if pos < #f then + if pos <= #f then table.insert(t, _sub(f, pos)) end return t @@ -794,8 +802,8 @@ function UI.Window:print(text, bg, fg, indent) break end local w = word - if self.cursorX + #word > self.width then - self.cursorX = indent + if self.cursorX + #word > width then + self.cursorX = marginLeft + 1 self.cursorY = self.cursorY + 1 w = word:gsub(' ', '') end @@ -806,7 +814,7 @@ function UI.Window:print(text, bg, fg, indent) end end if lines[k + 1] then - self.cursorX = indent + self.cursorX = marginLeft + 1 self.cursorY = self.cursorY + 1 end end @@ -1246,6 +1254,7 @@ UI.Grid.defaults = { index = 1, inverseSort = false, disableHeader = false, + marginRight = 0, textColor = colors.white, textSelectedColor = colors.white, backgroundColor = colors.black, @@ -1311,7 +1320,7 @@ end function UI.Grid:adjustWidth() local t = { } -- cols without width - local w = self.width - #self.columns - 1 -- width remaing + local w = self.width - #self.columns - 1 - self.marginRight -- width remaing for _,c in pairs(self.columns) do if c.width then @@ -1668,109 +1677,59 @@ end UI.ScrollingGrid = class(UI.Grid) UI.ScrollingGrid.defaults = { UIElement = 'ScrollingGrid', - scrollOffset = 1, - lineChar = '|', - sliderChar = '#', - upArrowChar = '^', - downArrowChar = 'v', - scrollbarColor = colors.lightGray, + scrollOffset = 0, + marginRight = 1, } function UI.ScrollingGrid:init(args) UI.Grid.init(self, UI:getDefaults(UI.ScrollingGrid, args)) + self.scrollBar = UI.ScrollBar() end function UI.ScrollingGrid:drawRows() UI.Grid.drawRows(self) - self:drawScrollbar() + self.scrollBar:draw() end -function UI.ScrollingGrid:drawScrollbar() - local ts = Util.size(self.values) - if ts > self.pageSize then - local maxScroll = ts - self.pageSize - local percent = (self.scrollOffset - 1) / maxScroll - local sliderSize = self.pageSize / ts * (self.pageSize - 2) - local row = 2 - - if self.disableHeader then - row = 1 - end - - local x = self.width - for i = 1, self.pageSize - 2 do - self:write(x, row + i, self.lineChar, nil, self.scrollbarColor) - end - - local y = Util.round((self.pageSize - 2 - sliderSize) * percent) - for i = 1, Util.round(sliderSize) do - self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor) - end - - local color = self.scrollbarColor - if self.scrollOffset > 1 then - color = colors.white - end - self:write(x, 2, self.upArrowChar, nil, color) - - color = self.scrollbarColor - if self.scrollOffset + self.pageSize - 1 < Util.size(self.values) then - color = colors.white - end - self:write(x, self.pageSize + 1, self.downArrowChar, nil, color) +function UI.ScrollingGrid:getViewArea() + local y = 1 + if not self.disableHeader then + y = 2 end + return { + static = true, -- the container doesn't scroll + y = y, -- scrollbar Y + height = self.pageSize, -- viewable height + totalHeight = Util.size(self.values), -- total height + offsetY = self.scrollOffset, -- scroll offset + } end function UI.ScrollingGrid:getStartRow() local ts = Util.size(self.values) if ts < self.pageSize then - self.scrollOffset = 1 + self.scrollOffset = 0 end - return self.scrollOffset + return self.scrollOffset + 1 end function UI.ScrollingGrid:setIndex(index) - if index < self.scrollOffset then - self.scrollOffset = index - elseif index - (self.scrollOffset - 1) > self.pageSize then - self.scrollOffset = index - self.pageSize + 1 + if index < self.scrollOffset + 1 then + self.scrollOffset = index - 1 + elseif index - self.scrollOffset > self.pageSize then + self.scrollOffset = index - self.pageSize end - if self.scrollOffset < 1 then - self.scrollOffset = 1 + if self.scrollOffset < 0 then + self.scrollOffset = 0 else local ts = Util.size(self.values) - if self.pageSize + self.scrollOffset > ts then - self.scrollOffset = math.max(1, ts - self.pageSize + 1) + if self.pageSize + self.scrollOffset + 1 > ts then + self.scrollOffset = math.max(0, ts - self.pageSize) end end UI.Grid.setIndex(self, index) end -function UI.ScrollingGrid:eventHandler(event) - - if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then - if event.x == self.width then - local ts = Util.size(self.values) - if ts > self.pageSize then - local row = 2 - if self.disableHeader then - row = 1 - end - if event.y == row then - self:setIndex(self.scrollOffset - 1) - elseif event.y == self.height then - self:setIndex(self.scrollOffset + self.pageSize) - -- else - -- ... percentage ... - end - return true - end - end - end - - return UI.Grid.eventHandler(self, event) -end - --[[-- Menu --]]-- UI.Menu = class(UI.Grid) UI.Menu.defaults = { @@ -1832,10 +1791,10 @@ function UI.Menu:eventHandler(event) return UI.Grid.eventHandler(self, event) end ---[[-- ViewportWindow --]]-- -UI.ViewportWindow = class(UI.Window) -UI.ViewportWindow.defaults = { - UIElement = 'ViewportWindow', +--[[-- Viewport --]]-- +UI.Viewport = class(UI.Window) +UI.Viewport.defaults = { + UIElement = 'Viewport', backgroundColor = colors.cyan, accelerators = { down = 'scroll_down', @@ -1848,18 +1807,20 @@ UI.ViewportWindow.defaults = { [ 'control-f' ] = 'scroll_pageDown', }, } -function UI.ViewportWindow:init(args) - local defaults = UI:getDefaults(UI.ViewportWindow, args) +function UI.Viewport:init(args) + local defaults = UI:getDefaults(UI.Viewport, args) UI.Window.init(self, defaults) end -function UI.ViewportWindow:setScrollPosition(offset) +function UI.Viewport:setScrollPosition(offset) local oldOffset = self.offy self.offy = math.max(offset, 0) local max = self.ymax or self.height if self.children then for _, child in ipairs(self.children) do - max = math.max(child.y + child.height - 1, max) + if child ~= self.scrollBar then -- hack ! + max = math.max(child.y + child.height - 1, max) + end end end self.offy = math.min(self.offy, math.max(max, self.height) - self.height) @@ -1868,11 +1829,20 @@ function UI.ViewportWindow:setScrollPosition(offset) end end -function UI.ViewportWindow:reset() +function UI.Viewport:reset() self.offy = 0 end -function UI.ViewportWindow:eventHandler(event) +function UI.Viewport:getViewArea() + return { + y = (self.offy or 0) + 1, + height = self.height, + totalHeight = self.ymax, + offsetY = self.offy or 0, + } +end + +function UI.Viewport:eventHandler(event) if event.type == 'scroll_down' then self:setScrollPosition(self.offy + 1) @@ -2042,8 +2012,7 @@ UI.MenuItem.defaults = { } function UI.MenuItem:init(args) - local defaults = UI:getDefaults(UI.MenuItem, args) - UI.Button.init(self, defaults) + UI.Button.init(self, UI:getDefaults(UI.MenuItem, args)) end --[[-- MenuBar --]]-- @@ -2144,8 +2113,7 @@ UI.DropMenuItem.defaults = { } function UI.DropMenuItem:init(args) - local defaults = UI:getDefaults(UI.DropMenuItem, args) - UI.Button.init(self, defaults) + UI.Button.init(self, UI:getDefaults(UI.DropMenuItem, args)) end --[[-- DropMenu --]]-- @@ -2466,9 +2434,9 @@ UI.Throttle.defaults = { ctr = 0, image = { ' //) (O )~@ &~&-( ?Q ', - ' //) (O )- @ \-( ?) && ', - ' //) (O ), @ \-(?) && ', - ' //) (O ). @ \-d ) (@ ' + ' //) (O )- @ \\-( ?) && ', + ' //) (O ), @ \\-(?) && ', + ' //) (O ). @ \\-d ) (@ ' } } @@ -2949,22 +2917,100 @@ function UI.Text:setParent() end function UI.Text:draw() - local value = self.value or '' - self:write(1, 1, Util.widthify(value, self.width), self.backgroundColor) + self:write(1, 1, Util.widthify(self.value or '', self.width), self.backgroundColor) +end + +--[[-- ScrollBar --]]-- +UI.ScrollBar = class(UI.Window) +UI.ScrollBar.defaults = { + UIElement = 'ScrollBar', + lineChar = '|', + sliderChar = '#', + upArrowChar = '^', + downArrowChar = 'v', + scrollbarColor = colors.lightGray, + value = '', + width = 1, + x = -1, + ey = -1, +} +function UI.ScrollBar:init(args) + UI.Window.init(self, UI:getDefaults(UI.ScrollBar, args)) +end + +function UI.ScrollBar:draw() + local parent = self.parent + local view = parent:getViewArea() + + if view.totalHeight > view.height then + local maxScroll = view.totalHeight - view.height + local percent = view.offsetY / maxScroll + local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2))) + local x = self.width + + local row = view.y + if not view.static then -- does the container scroll ? + self.y = row -- if so, move the scrollbar onscreen + row = 1 + end + + for i = 1, view.height - 2 do + self:write(x, row + i, self.lineChar, nil, self.scrollbarColor) + end + + local y = Util.round((view.height - 2 - sliderSize) * percent) + for i = 1, sliderSize do + self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor) + end + + local color = self.scrollbarColor + if view.offsetY > 0 then + color = colors.white + end + self:write(x, row, self.upArrowChar, nil, color) + + color = self.scrollbarColor + if view.offsetY + view.height < view.totalHeight then + color = colors.white + end + self:write(x, row + view.height - 1, self.downArrowChar, nil, color) + end +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() + if view.totalHeight > view.height then + if event.y == view.y then + self:emit({ type = 'scroll_up'}) + elseif event.y == self.height then + self:emit({ type = 'scroll_down'}) + -- else + -- ... percentage ... + end + end + return true + end + end end --[[-- TextArea --]]-- -UI.TextArea = class(UI.Window) +UI.TextArea = class(UI.Viewport) UI.TextArea.defaults = { UIElement = 'TextArea', + marginRight = 2, value = '', } function UI.TextArea:init(args) - local defaults = UI:getDefaults(UI.TextArea, args) - UI.Window.init(self, defaults) + UI.Viewport.init(self, UI:getDefaults(UI.TextArea, args)) + self.scrollBar = UI.ScrollBar() end function UI.TextArea:setText(text) + self.offy = 0 + self.ymax = nil self.value = text self:draw() end @@ -2973,6 +3019,13 @@ function UI.TextArea:draw() self:clear() self:setCursorPos(1, 1) self:print(self.value) + self.ymax = self.cursorY + 1 + + for _,child in pairs(self.children) do + if child.enabled then + child:draw() + end + end end --[[-- Form --]]-- diff --git a/sys/apis/ui/canvas.lua b/sys/apis/ui/canvas.lua index fd69f74..c7d3c9f 100644 --- a/sys/apis/ui/canvas.lua +++ b/sys/apis/ui/canvas.lua @@ -5,6 +5,7 @@ local Util = require('util') local _rep = string.rep local _sub = string.sub local _gsub = string.gsub +local colors = _G.colors local Canvas = class() @@ -171,7 +172,7 @@ function Canvas:writeBlit(x, y, text, bg, fg) end return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) end - + local line = self.lines[y] line.dirty = true line.text = replace(line.text, x, text, width) @@ -217,7 +218,7 @@ function Canvas:blitClipped(device) self:blit(device, { x = region[1] - self.x + 1, y = region[2] - self.y + 1, - ex = region[3]- self.x + 1, + ex = region[3]- self.x + 1, ey = region[4] - self.y + 1 }, { x = region[1], y = region[2] }) end @@ -251,7 +252,7 @@ function Canvas:dirty() end function Canvas:clean() - for y, line in pairs(self.lines) do + for _, line in pairs(self.lines) do line.dirty = false end end @@ -314,13 +315,13 @@ function Canvas:applyPalette(palette) self.palette = palette end -function Canvas.convertWindow(win, parent, x, y) +function Canvas.convertWindow(win, parent, wx, wy) local w, h = win.getSize() win.canvas = Canvas({ - x = x, - y = y, + x = wx, + y = wy, width = w, height = h, isColor = win.isColor(), @@ -331,7 +332,7 @@ function Canvas.convertWindow(win, parent, x, y) end function win.clearLine() - local x, y = win.getCursorPos() + local _, y = win.getCursorPos() win.canvas:write(1, y, _rep(' ', win.canvas.width), diff --git a/sys/apis/util.lua b/sys/apis/util.lua index 1093a53..4e51540 100644 --- a/sys/apis/util.lua +++ b/sys/apis/util.lua @@ -426,7 +426,7 @@ function Util.split(str, pattern) pattern = pattern or "(.-)\n" local t = {} local function helper(line) table.insert(t, line) return "" end - helper((str:gsub(pattern, helper))) + helper((str:gsub(pattern, helper))) return t end diff --git a/sys/apps/Files.lua b/sys/apps/Files.lua index 05ede9b..dd052a6 100644 --- a/sys/apps/Files.lua +++ b/sys/apps/Files.lua @@ -5,6 +5,8 @@ local Event = require('event') local UI = require('ui') local Util = require('util') +local colors = _G.colors + multishell.setTitle(multishell.getCurrent(), 'Files') UI:configure('Files', ...) @@ -20,7 +22,7 @@ local marked = { } local directories = { } local cutMode = false -function formatSize(size) +local function formatSize(size) if size >= 1000000 then return string.format('%dM', math.floor(size/1000000, 2)) elseif size >= 1000 then @@ -58,16 +60,16 @@ local Browser = UI.Page { }, }, grid = UI.ScrollingGrid { - columns = { + columns = { { heading = 'Name', key = 'name' }, { key = 'flags', width = 2 }, - { heading = 'Size', key = 'fsize', width = 6 }, + { heading = 'Size', key = 'fsize', width = 5 }, }, sortColumn = 'name', y = 2, ey = -2, }, statusBar = UI.StatusBar { - columns = { + columns = { { key = 'status' }, { key = 'totalSize', width = 6 }, }, @@ -84,6 +86,7 @@ local Browser = UI.Page { d = 'delete', delete = 'delete', [ 'control-h' ] = 'toggle_hidden', + [ 'control-s' ] = 'toggle_dirSize', [ 'control-x' ] = 'cut', [ 'control-c' ] = 'copy', paste = 'paste', @@ -117,7 +120,7 @@ function Browser.grid:sortCompare(a, b) return a.isDir end -function Browser.grid:getRowTextColor(file, selected) +function Browser.grid:getRowTextColor(file) if file.marked then return colors.green end @@ -130,13 +133,6 @@ function Browser.grid:getRowTextColor(file, selected) return colors.white end -function Browser.grid:getRowBackgroundColorX(file, selected) - if selected then - return colors.gray - end - return self.backgroundColor -end - function Browser.grid:eventHandler(event) if event.type == 'copy' then -- let copy be handled by parent return false @@ -169,7 +165,6 @@ function Browser:unmarkAll() end function Browser:getDirectory(directory) - local s, dir = pcall(function() local dir = directories[directory] @@ -350,7 +345,7 @@ function Browser:eventHandler(event) self.statusBar:sync() local _, ch = os.pullEvent('char') if ch == 'y' or ch == 'Y' then - for k,m in pairs(marked) do + for _,m in pairs(marked) do pcall(function() fs.delete(m.fullName) end) @@ -377,7 +372,7 @@ function Browser:eventHandler(event) end elseif event.type == 'paste' then - for k,m in pairs(copied) do + for _,m in pairs(copied) do local s, m = pcall(function() if cutMode then fs.move(m.fullName, fs.combine(self.dir.name, m.name)) diff --git a/sys/apps/Help.lua b/sys/apps/Help.lua index 25928b9..023b062 100644 --- a/sys/apps/Help.lua +++ b/sys/apps/Help.lua @@ -1,14 +1,19 @@ requireInjector(getfenv(1)) -local Event = require('event') local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local help = _G.help multishell.setTitle(multishell.getCurrent(), 'Help') UI:configure('Help', ...) -local files = { } -for _,f in pairs(help.topics()) do - table.insert(files, { name = f }) +local topics = { } +for _,topic in pairs(help.topics()) do + if help.lookup(topic) then + table.insert(topics, { name = topic }) + end end local page = UI.Page { @@ -22,9 +27,9 @@ local page = UI.Page { }, grid = UI.ScrollingGrid { y = 4, - values = files, + values = topics, columns = { - { heading = 'Name', key = 'name' }, + { heading = 'Topic', key = 'name' }, }, sortColumn = 'name', }, @@ -34,36 +39,56 @@ local page = UI.Page { }, } -local function showHelp(name) - UI.term:reset() - shell.run('help ' .. name) - print('Press enter to return') - repeat - os.pullEvent('key') - local _, k = os.pullEvent('key_up') - until k == keys.enter +local topicPage = UI.Page { + backgroundColor = colors.black, + titleBar = UI.TitleBar { + title = 'text', + previousPage = true, + }, + helpText = UI.TextArea { + backgroundColor = colors.black, + x = 2, ex = -1, y = 3, ey = -2, + }, + accelerators = { + q = 'back', + backspace = 'back', + }, +} + +function topicPage.helpText:focus() + -- let the help text get focused so we consume key strokes +end + +function topicPage:eventHandler(event) + if event.type == 'back' then + UI:setPreviousPage() + end + return UI.Page.eventHandler(self, event) end function page:eventHandler(event) if event.type == 'quit' then - Event.exitPullEvents() + UI:exitPullEvents() elseif event.type == 'grid_select' then if self.grid:getSelected() then - showHelp(self.grid:getSelected().name) - self:setFocus(self.filter) - self:draw() + local name = self.grid:getSelected().name + local f = help.lookup(name) + + topicPage.titleBar.title = name + topicPage.helpText:setText(Util.readFile(f)) + + UI:setPage(topicPage) end elseif event.type == 'text_change' then - local text = event.text - if #text == 0 then - self.grid.values = files + if #event.text == 0 then + self.grid.values = topics else self.grid.values = { } - for _,f in pairs(files) do - if string.find(f.name, text) then + for _,f in pairs(topics) do + if string.find(f.name, event.text) then table.insert(self.grid.values, f) end end @@ -72,7 +97,7 @@ function page:eventHandler(event) self.grid:setIndex(1) self.grid:draw() else - UI.Page.eventHandler(self, event) + return UI.Page.eventHandler(self, event) end end diff --git a/sys/apps/Overview.lua b/sys/apps/Overview.lua index 965e13c..099cd71 100644 --- a/sys/apps/Overview.lua +++ b/sys/apps/Overview.lua @@ -148,7 +148,7 @@ local page = UI.Page { tabBar = UI.VerticalTabBar { buttons = buttons, }, - container = UI.ViewportWindow { + container = UI.Viewport { x = cx, y = cy, }, @@ -324,7 +324,7 @@ function page.container:setCategory(categoryName) end function page.container:draw() - UI.ViewportWindow.draw(self) + UI.Viewport.draw(self) end function page:refresh() diff --git a/sys/etc/ext.theme b/sys/etc/ext.theme index f4fcbb8..927a1c1 100644 --- a/sys/etc/ext.theme +++ b/sys/etc/ext.theme @@ -1,5 +1,5 @@ { - ScrollingGrid = { + ScrollBar = { lineChar = '|', sliderChar = '\127', upArrowChar = '\30',