ui overhaul
This commit is contained in:
492
sys/apis/ui/components/Grid.lua
Normal file
492
sys/apis/ui/components/Grid.lua
Normal file
@@ -0,0 +1,492 @@
|
||||
local class = require('class')
|
||||
local UI = require('ui')
|
||||
local Util = require('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)
|
||||
if t == 'string' or t == 'number' then
|
||||
return v
|
||||
end
|
||||
return tostring(v)
|
||||
end
|
||||
|
||||
local Writer = class()
|
||||
function Writer:init(element, y)
|
||||
self.element = element
|
||||
self.y = y
|
||||
self.x = 1
|
||||
end
|
||||
|
||||
function Writer:write(s, width, justify, 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 justify == 'right' then
|
||||
s = padding .. s
|
||||
else
|
||||
s = s .. padding
|
||||
end
|
||||
end
|
||||
self.element:write(self.x, self.y, s, bg, fg)
|
||||
self.x = self.x + width
|
||||
end
|
||||
|
||||
function Writer:finish(bg)
|
||||
if self.x <= self.element.width then
|
||||
self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg)
|
||||
end
|
||||
self.x = 1
|
||||
self.y = self.y + 1
|
||||
end
|
||||
|
||||
--[[-- Grid --]]--
|
||||
UI.Grid = class(UI.Window)
|
||||
UI.Grid.defaults = {
|
||||
UIElement = 'Grid',
|
||||
index = 1,
|
||||
inverseSort = false,
|
||||
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 = '>',
|
||||
sortIndicator = ' ',
|
||||
inverseSortIndicator = '^',
|
||||
values = { },
|
||||
columns = { },
|
||||
accelerators = {
|
||||
enter = 'key_enter',
|
||||
[ 'control-c' ] = 'copy',
|
||||
down = 'scroll_down',
|
||||
up = 'scroll_up',
|
||||
home = 'scroll_top',
|
||||
[ 'end' ] = 'scroll_bottom',
|
||||
pageUp = 'scroll_pageUp',
|
||||
[ 'control-b' ] = 'scroll_pageUp',
|
||||
pageDown = 'scroll_pageDown',
|
||||
[ 'control-f' ] = 'scroll_pageDown',
|
||||
},
|
||||
}
|
||||
function UI.Grid:setParent()
|
||||
UI.Window.setParent(self)
|
||||
|
||||
for _,c in pairs(self.columns) do
|
||||
c.cw = c.width
|
||||
if not c.heading then
|
||||
c.heading = ''
|
||||
end
|
||||
end
|
||||
|
||||
self:update()
|
||||
|
||||
if not self.pageSize then
|
||||
if self.disableHeader then
|
||||
self.pageSize = self.height
|
||||
else
|
||||
self.pageSize = self.height - self.headerHeight
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UI.Grid:resize()
|
||||
UI.Window.resize(self)
|
||||
|
||||
if self.disableHeader then
|
||||
self.pageSize = self.height
|
||||
else
|
||||
self.pageSize = self.height - self.headerHeight
|
||||
end
|
||||
self:adjustWidth()
|
||||
end
|
||||
|
||||
function UI.Grid:adjustWidth()
|
||||
local t = { } -- cols without width
|
||||
local w = self.width - #self.columns - 1 - self.marginRight -- width remaining
|
||||
|
||||
for _,c in pairs(self.columns) do
|
||||
if c.width then
|
||||
c.cw = c.width
|
||||
w = w - c.cw
|
||||
else
|
||||
table.insert(t, c)
|
||||
end
|
||||
end
|
||||
|
||||
if #t == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if #t == 1 then
|
||||
t[1].cw = #(t[1].heading or '')
|
||||
t[1].cw = math.max(t[1].cw, w)
|
||||
return
|
||||
end
|
||||
|
||||
if not self.autospace then
|
||||
for k,c in ipairs(t) do
|
||||
c.cw = math.floor(w / (#t - k + 1))
|
||||
w = w - c.cw
|
||||
end
|
||||
|
||||
else
|
||||
for _,c in ipairs(t) do
|
||||
c.cw = #(c.heading or '')
|
||||
w = w - c.cw
|
||||
end
|
||||
-- adjust the size to the length of the value
|
||||
for key,row in pairs(self.values) do
|
||||
if w <= 0 then
|
||||
break
|
||||
end
|
||||
row = self:getDisplayValues(row, key)
|
||||
for _,col in pairs(t) do
|
||||
local value = row[col.key]
|
||||
if value then
|
||||
value = tostring(value)
|
||||
if #value > col.cw then
|
||||
w = w + col.cw
|
||||
col.cw = math.min(#value, w)
|
||||
w = w - col.cw
|
||||
if w <= 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- last column does not get padding (right alignment)
|
||||
if not self.columns[#self.columns].width then
|
||||
Util.removeByValue(t, self.columns[#self.columns])
|
||||
end
|
||||
|
||||
-- got some extra room - add some padding
|
||||
if w > 0 then
|
||||
for k,c in ipairs(t) do
|
||||
local padding = math.floor(w / (#t - k + 1))
|
||||
c.cw = c.cw + padding
|
||||
w = w - padding
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UI.Grid:setPageSize(pageSize)
|
||||
self.pageSize = pageSize
|
||||
end
|
||||
|
||||
function UI.Grid:getValues()
|
||||
return self.values
|
||||
end
|
||||
|
||||
function UI.Grid:setValues(t)
|
||||
self.values = t
|
||||
self:update()
|
||||
end
|
||||
|
||||
function UI.Grid:setInverseSort(inverseSort)
|
||||
self.inverseSort = inverseSort
|
||||
self:update()
|
||||
self:setIndex(self.index)
|
||||
end
|
||||
|
||||
function UI.Grid:setSortColumn(column)
|
||||
self.sortColumn = column
|
||||
end
|
||||
|
||||
function UI.Grid:getDisplayValues(row, key)
|
||||
return row
|
||||
end
|
||||
|
||||
function UI.Grid:getSelected()
|
||||
if self.sorted then
|
||||
return self.values[self.sorted[self.index]], self.sorted[self.index]
|
||||
end
|
||||
end
|
||||
|
||||
function UI.Grid:setSelected(name, value)
|
||||
if self.sorted then
|
||||
for k,v in pairs(self.sorted) do
|
||||
if self.values[v][name] == value then
|
||||
self:setIndex(k)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
self:setIndex(1)
|
||||
end
|
||||
|
||||
function UI.Grid:focus()
|
||||
self:drawRows()
|
||||
end
|
||||
|
||||
function UI.Grid:draw()
|
||||
if not self.disableHeader then
|
||||
self:drawHeadings()
|
||||
end
|
||||
|
||||
if self.index <= 0 then
|
||||
self:setIndex(1)
|
||||
elseif self.index > #self.sorted then
|
||||
self:setIndex(#self.sorted)
|
||||
end
|
||||
self:drawRows()
|
||||
end
|
||||
|
||||
-- Something about the displayed table has changed
|
||||
-- resort the table
|
||||
function UI.Grid:update()
|
||||
local function sort(a, b)
|
||||
if not a[self.sortColumn] then
|
||||
return false
|
||||
elseif not b[self.sortColumn] then
|
||||
return true
|
||||
end
|
||||
return self:sortCompare(a, b)
|
||||
end
|
||||
|
||||
local function inverseSort(a, b)
|
||||
return not sort(a, b)
|
||||
end
|
||||
|
||||
local order
|
||||
if self.sortColumn then
|
||||
order = sort
|
||||
if self.inverseSort then
|
||||
order = inverseSort
|
||||
end
|
||||
end
|
||||
|
||||
self.sorted = Util.keys(self.values)
|
||||
if order then
|
||||
table.sort(self.sorted, function(a,b)
|
||||
return order(self.values[a], self.values[b])
|
||||
end)
|
||||
end
|
||||
|
||||
self:adjustWidth()
|
||||
end
|
||||
|
||||
function UI.Grid:drawHeadings()
|
||||
if self.headerHeight > 1 then
|
||||
self:clear(self.headerBackgroundColor)
|
||||
end
|
||||
local sb = Writer(self, math.ceil(self.headerHeight / 2))
|
||||
for _,col in ipairs(self.columns) do
|
||||
local ind = ' '
|
||||
local color = self.headerTextColor
|
||||
if col.key == self.sortColumn then
|
||||
if self.inverseSort then
|
||||
ind = self.inverseSortIndicator
|
||||
else
|
||||
ind = self.sortIndicator
|
||||
end
|
||||
color = self.headerSortColor
|
||||
end
|
||||
sb:write(ind .. col.heading,
|
||||
col.cw + 1,
|
||||
col.justify,
|
||||
self.headerBackgroundColor,
|
||||
color)
|
||||
end
|
||||
sb:finish(self.headerBackgroundColor)
|
||||
end
|
||||
|
||||
function UI.Grid:sortCompare(a, b)
|
||||
a = safeValue(a[self.sortColumn])
|
||||
b = safeValue(b[self.sortColumn])
|
||||
if type(a) == type(b) then
|
||||
return a < b
|
||||
end
|
||||
return tostring(a) < tostring(b)
|
||||
end
|
||||
|
||||
function UI.Grid:drawRows()
|
||||
local startRow = math.max(1, self:getStartRow())
|
||||
|
||||
local sb = Writer(self, self.disableHeader and 1 or self.headerHeight + 1)
|
||||
|
||||
local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted)
|
||||
for index = startRow, lastRow do
|
||||
|
||||
local key = self.sorted[index]
|
||||
local rawRow = self.values[key]
|
||||
local row = self:getDisplayValues(rawRow, key)
|
||||
|
||||
local ind = ' '
|
||||
if self.focused and index == self.index and not self.inactive then
|
||||
ind = self.focusIndicator
|
||||
end
|
||||
|
||||
local selected = index == self.index and not self.inactive
|
||||
local bg = self:getRowBackgroundColor(rawRow, selected)
|
||||
local fg = self:getRowTextColor(rawRow, selected)
|
||||
|
||||
for _,col in pairs(self.columns) do
|
||||
sb:write(ind .. safeValue(row[col.key] or ''),
|
||||
col.cw + 1,
|
||||
col.justify,
|
||||
bg,
|
||||
fg)
|
||||
ind = ' '
|
||||
end
|
||||
sb:finish(bg)
|
||||
end
|
||||
|
||||
if sb.y <= self.height then
|
||||
self:clearArea(1, sb.y, self.width, self.height - sb.y + 1)
|
||||
end
|
||||
end
|
||||
|
||||
function UI.Grid:getRowTextColor(row, selected)
|
||||
if selected then
|
||||
if self.focused then
|
||||
return self.textSelectedColor
|
||||
end
|
||||
return self.unfocusedTextSelectedColor
|
||||
end
|
||||
return self.textColor
|
||||
end
|
||||
|
||||
function UI.Grid:getRowBackgroundColor(row, selected)
|
||||
if selected then
|
||||
if self.focused then
|
||||
return self.backgroundSelectedColor
|
||||
end
|
||||
return self.unfocusedBackgroundSelectedColor
|
||||
end
|
||||
return self.backgroundColor
|
||||
end
|
||||
|
||||
function UI.Grid:getIndex()
|
||||
return self.index
|
||||
end
|
||||
|
||||
function UI.Grid:setIndex(index)
|
||||
index = math.max(1, index)
|
||||
self.index = math.min(index, #self.sorted)
|
||||
|
||||
local selected = self:getSelected()
|
||||
if selected ~= self.selected then
|
||||
self:drawRows()
|
||||
self.selected = selected
|
||||
if selected then
|
||||
self:emit({ type = 'grid_focus_row', selected = selected, element = self })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UI.Grid:getStartRow()
|
||||
return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1
|
||||
end
|
||||
|
||||
function UI.Grid:getPage()
|
||||
return math.floor(self.index / self.pageSize) + 1
|
||||
end
|
||||
|
||||
function UI.Grid:getPageCount()
|
||||
local tableSize = Util.size(self.values)
|
||||
local pc = math.floor(tableSize / self.pageSize)
|
||||
if tableSize % self.pageSize > 0 then
|
||||
pc = pc + 1
|
||||
end
|
||||
return pc
|
||||
end
|
||||
|
||||
function UI.Grid:nextPage()
|
||||
self:setPage(self:getPage() + 1)
|
||||
end
|
||||
|
||||
function UI.Grid:previousPage()
|
||||
self:setPage(self:getPage() - 1)
|
||||
end
|
||||
|
||||
function UI.Grid:setPage(pageNo)
|
||||
-- 1 based paging
|
||||
self:setIndex((pageNo-1) * self.pageSize + 1)
|
||||
end
|
||||
|
||||
function UI.Grid:eventHandler(event)
|
||||
if event.type == 'mouse_click' or
|
||||
event.type == 'mouse_rightclick' or
|
||||
event.type == 'mouse_doubleclick' then
|
||||
if not self.disableHeader then
|
||||
if event.y <= self.headerHeight then
|
||||
local col = 2
|
||||
for _,c in ipairs(self.columns) do
|
||||
if event.x < col + c.cw then
|
||||
self:emit({
|
||||
type = 'grid_sort',
|
||||
sortColumn = c.key,
|
||||
inverseSort = self.sortColumn == c.key and not self.inverseSort,
|
||||
element = self,
|
||||
})
|
||||
break
|
||||
end
|
||||
col = col + c.cw + 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
local row = self:getStartRow() + event.y - 1
|
||||
if not self.disableHeader then
|
||||
row = row - self.headerHeight
|
||||
end
|
||||
if row > 0 and row <= Util.size(self.values) then
|
||||
self:setIndex(row)
|
||||
if event.type == 'mouse_doubleclick' then
|
||||
self:emit({ type = 'key_enter' })
|
||||
elseif event.type == 'mouse_rightclick' then
|
||||
self:emit({ type = 'grid_select_right', selected = self.selected, element = self })
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
|
||||
elseif event.type == 'grid_sort' then
|
||||
self.sortColumn = event.sortColumn
|
||||
self:setInverseSort(event.inverseSort)
|
||||
self:draw()
|
||||
elseif event.type == 'scroll_down' then
|
||||
self:setIndex(self.index + 1)
|
||||
elseif event.type == 'scroll_up' then
|
||||
self:setIndex(self.index - 1)
|
||||
elseif event.type == 'scroll_top' then
|
||||
self:setIndex(1)
|
||||
elseif event.type == 'scroll_bottom' then
|
||||
self:setIndex(Util.size(self.values))
|
||||
elseif event.type == 'scroll_pageUp' then
|
||||
self:setIndex(self.index - self.pageSize)
|
||||
elseif event.type == 'scroll_pageDown' then
|
||||
self:setIndex(self.index + self.pageSize)
|
||||
elseif event.type == 'scroll_to' then
|
||||
self:setIndex(event.offset)
|
||||
elseif event.type == 'key_enter' then
|
||||
if self.selected then
|
||||
self:emit({ type = 'grid_select', selected = self.selected, element = self })
|
||||
end
|
||||
elseif event.type == 'copy' then
|
||||
if self.selected then
|
||||
os.queueEvent('clipboard_copy', self.selected)
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
Reference in New Issue
Block a user