Add label and list widgets, and switch stock dialogs to them.

develop
Alexander Gavrilov 2012-10-16 14:18:35 +04:00
parent abfe2754fb
commit d336abfd97
5 changed files with 502 additions and 126 deletions

@ -125,6 +125,10 @@ end
-- Misc functions -- Misc functions
NEWLINE = "\n"
COMMA = ","
PERIOD = "."
function printall(table) function printall(table)
local ok,f,t,k = pcall(pairs,table) local ok,f,t,k = pcall(pairs,table)
if ok then if ok then

@ -116,7 +116,7 @@ function blink_visible(delay)
return math.floor(dfhack.getTickCount()/delay) % 2 == 0 return math.floor(dfhack.getTickCount()/delay) % 2 == 0
end end
local function to_pen(default, pen, bg, bold) function to_pen(default, pen, bg, bold)
if pen == nil then if pen == nil then
return default or {} return default or {}
elseif type(pen) ~= 'table' then elseif type(pen) ~= 'table' then
@ -363,6 +363,8 @@ function View:init(args)
end end
function View:addviews(list) function View:addviews(list)
if not list then return end
local sv = self.subviews local sv = self.subviews
for _,obj in ipairs(list) do for _,obj in ipairs(list) do
@ -413,11 +415,15 @@ function View:updateLayout(parent_rect)
self.frame_parent_rect = parent_rect self.frame_parent_rect = parent_rect
end end
self:invoke_before('preUpdateLayout', parent_rect)
local frame_rect,body_rect = self:computeFrame(parent_rect) local frame_rect,body_rect = self:computeFrame(parent_rect)
self.frame_rect = frame_rect self.frame_rect = frame_rect
self.frame_body = parent_rect:viewport(body_rect or frame_rect) self.frame_body = parent_rect:viewport(body_rect or frame_rect)
self:invoke_after('postComputeFrame', self.frame_body)
self:updateSubviewLayout(self.frame_body) self:updateSubviewLayout(self.frame_body)
self:invoke_after('postUpdateLayout', self.frame_body) self:invoke_after('postUpdateLayout', self.frame_body)
@ -432,6 +438,8 @@ function View:renderSubviews(dc)
end end
function View:render(dc) function View:render(dc)
self:onRenderFrame(dc, self.frame_rect)
local sub_dc = Painter{ local sub_dc = Painter{
view_rect = self.frame_body, view_rect = self.frame_body,
clip_view = dc clip_view = dc
@ -442,6 +450,9 @@ function View:render(dc)
self:renderSubviews(sub_dc) self:renderSubviews(sub_dc)
end end
function View:onRenderFrame(dc,rect)
end
function View:onRenderBody(dc) function View:onRenderBody(dc)
end end
@ -609,8 +620,7 @@ function FramedScreen:computeFrame(parent_rect)
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1) return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1)
end end
function FramedScreen:render(dc) function FramedScreen:onRenderFrame(dc, rect)
local rect = self.frame_rect
local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2 local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2
if rect.wgap <= 0 and rect.hgap <= 0 then if rect.wgap <= 0 and rect.hgap <= 0 then
@ -621,8 +631,6 @@ function FramedScreen:render(dc)
end end
paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title) paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title)
FramedScreen.super.render(self, dc)
end end
return _ENV return _ENV

@ -14,47 +14,35 @@ MessageBox.focus_path = 'MessageBox'
MessageBox.ATTRS{ MessageBox.ATTRS{
frame_style = gui.GREY_LINE_FRAME, frame_style = gui.GREY_LINE_FRAME,
frame_inset = 1,
-- new attrs -- new attrs
text = {},
on_accept = DEFAULT_NIL, on_accept = DEFAULT_NIL,
on_cancel = DEFAULT_NIL, on_cancel = DEFAULT_NIL,
on_close = DEFAULT_NIL, on_close = DEFAULT_NIL,
text_pen = DEFAULT_NIL,
} }
function MessageBox:preinit(info) function MessageBox:init(info)
if type(info.text) == 'string' then self:addviews{
info.text = utils.split_string(info.text, "\n") widgets.Label{
end view_id = 'label',
text = info.text,
text_pen = info.text_pen,
frame = { l = 0, t = 0 },
auto_height = true
}
}
end end
function MessageBox:getWantedFrameSize() function MessageBox:getWantedFrameSize()
local text = self.text local label = self.subviews.label
local w = #(self.frame_title or '') + 4 local width = math.max(self.frame_width or 0, 20, #(self.frame_title or '') + 4)
w = math.max(w, 20) return math.max(width, label:getTextWidth()), label:getTextHeight()
w = math.max(self.frame_width or w, w)
for _, l in ipairs(text) do
w = math.max(w, #l)
end
local h = #text+1
if h > 1 then
h = h+1
end
return w+2, #text+2
end end
function MessageBox:onRenderBody(dc) function MessageBox:onRenderFrame(dc,rect)
if #self.text > 0 then MessageBox.super.onRenderFrame(self,dc,rect)
dc:newline(1):pen(self.text_pen or COLOR_GREY)
for _, l in ipairs(self.text or {}) do
dc:string(l):newline(1)
end
end
if self.on_accept then if self.on_accept then
local fr = self.frame_rect dc:seek(rect.x1+2,rect.y2):key('LEAVESCREEN'):string('/'):key('MENU_CONFIRM')
local dc2 = gui.Painter.new_xy(fr.x1+1,fr.y2+1,fr.x2-8,fr.y2+1)
dc2:key('LEAVESCREEN'):string('/'):key('MENU_CONFIRM')
end end
end end
@ -75,6 +63,8 @@ function MessageBox:onInput(keys)
if self.on_cancel then if self.on_cancel then
self.on_cancel() self.on_cancel()
end end
else
self:inputToSubviews(keys)
end end
end end
@ -115,14 +105,14 @@ function InputBox:init(info)
view_id = 'edit', view_id = 'edit',
text = info.input, text = info.input,
text_pen = info.input_pen, text_pen = info.input_pen,
frame = { l = 1, r = 1, h = 1 }, frame = { l = 0, r = 0, h = 1 },
} }
} }
end end
function InputBox:getWantedFrameSize() function InputBox:getWantedFrameSize()
local mw, mh = InputBox.super.getWantedFrameSize(self) local mw, mh = InputBox.super.getWantedFrameSize(self)
self.subviews.edit.frame.t = mh self.subviews.edit.frame.t = mh+1
return mw, mh+2 return mw, mh+2
end end
@ -165,107 +155,47 @@ ListBox.ATTRS{
on_select = DEFAULT_NIL on_select = DEFAULT_NIL
} }
function InputBox:preinit(info) function ListBox:preinit(info)
info.on_accept = nil info.on_accept = nil
end end
function ListBox:init(info) function ListBox:init(info)
self.page_top = 1 local spen = gui.to_pen(COLOR_CYAN, info.select_pen, nil, false)
end local cpen = gui.to_pen(COLOR_LIGHTCYAN, info.cursor_pen or info.select_pen, nil, true)
local function choice_text(entry) self:addviews{
if type(entry)=="table" then widgets.List{
return entry.caption or entry[1] view_id = 'list',
else selected = info.selected,
return entry choices = info.choices,
end text_pen = spen,
cursor_pen = cpen,
on_submit = function(sel,obj)
self:dismiss()
if self.on_select then self.on_select(sel, obj) end
local cb = obj.on_select or obj[2]
if cb then cb(obj, sel) end
end,
frame = { l = 0, r = 0 },
}
}
end end
function ListBox:getWantedFrameSize() function ListBox:getWantedFrameSize()
local mw, mh = ListBox.super.getWantedFrameSize(self) local mw, mh = InputBox.super.getWantedFrameSize(self)
for _,v in ipairs(self.choices) do local list = self.subviews.list
local text = choice_text(v) list.frame.t = mh+1
mw = math.max(mw, #text+2) return math.max(mw, list:getContentWidth()), mh+1+list:getContentHeight()
end
return mw, mh+#self.choices+1
end
function ListBox:postUpdateLayout()
self.page_size = self.frame_rect.height - #self.text - 3
self:moveCursor(0)
end
function ListBox:moveCursor(delta)
local page = math.max(1, self.page_size)
local cnt = #self.choices
local off = self.selection+delta-1
local ds = math.abs(delta)
if ds > 1 then
if off >= cnt+ds-1 then
off = 0
else
off = math.min(cnt-1, off)
end
if off <= -ds then
off = cnt-1
else
off = math.max(0, off)
end
end
self.selection = 1 + off % cnt
self.page_top = 1 + page * math.floor((self.selection-1) / page)
end
function ListBox:onRenderBody(dc)
ListBox.super.onRenderBody(self, dc)
dc:newline(1):pen(self.select_pen or COLOR_CYAN)
local choices = self.choices
local iend = math.min(#choices, self.page_top+self.page_size-1)
for i = self.page_top,iend do
local text = choice_text(choices[i])
if text then
dc.cur_pen.bold = (i == self.selection);
dc:string(text)
else
dc:string('?ERROR?', COLOR_LIGHTRED)
end
dc:newline(1)
end
end end
function ListBox:onInput(keys) function ListBox:onInput(keys)
if keys.SELECT then if keys.LEAVESCREEN then
self:dismiss()
local choice=self.choices[self.selection]
if self.on_select then
self.on_select(self.selection, choice)
end
if choice then
local callback = choice.on_select or choice[2]
if callback then
callback(choice, self.selection)
end
end
elseif keys.LEAVESCREEN then
self:dismiss() self:dismiss()
if self.on_cancel then if self.on_cancel then
self.on_cancel() self.on_cancel()
end end
elseif keys.STANDARDSCROLL_UP then else
self:moveCursor(-1) self:inputToSubviews(keys)
elseif keys.STANDARDSCROLL_DOWN then
self:moveCursor(1)
elseif keys.STANDARDSCROLL_PAGEUP then
self:moveCursor(-self.page_size)
elseif keys.STANDARDSCROLL_PAGEDOWN then
self:moveCursor(self.page_size)
end end
end end

@ -7,6 +7,21 @@ local utils = require('utils')
local dscreen = dfhack.screen local dscreen = dfhack.screen
local function show_view(view,vis,act)
if view then
view.visible = vis
view.active = act
end
end
local function getval(obj)
if type(obj) == 'function' then
return obj()
else
return obj
end
end
------------ ------------
-- Widget -- -- Widget --
------------ ------------
@ -24,12 +39,62 @@ function Widget:computeFrame(parent_rect)
return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset) return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset)
end end
function Widget:render(dc) function Widget:onRenderFrame(dc, rect)
if self.frame_background then if self.frame_background then
dc:fill(self.frame_rect, self.frame_background) dc:fill(rect, self.frame_background)
end end
end
-----------
-- Panel --
-----------
Widget.super.render(self, dc) Panel = defclass(Panel, Widget)
Panel.ATTRS {
on_render = DEFAULT_NIL,
}
function Panel:init(args)
self:addviews(args.subviews)
end
function Panel:onRenderBody(dc)
if self.on_render then self.on_render(dc) end
end
-----------
-- Pages --
-----------
Pages = defclass(Pages, Panel)
function Pages:init(args)
for _,v in ipairs(self.subviews) do
show_view(v, false, false)
end
self:setSelected(args.selected or 1)
end
function Pages:setSelected(idx)
if type(idx) ~= 'number' then
local key = idx
if type(idx) == 'string' then
key = self.subviews[key]
end
idx = utils.linear_index(self.subviews, key)
if not idx then
error('Unknown page: '..key)
end
end
show_view(self.subviews[self.selected], false, false)
self.selected = math.min(math.max(1, idx), #self.subviews)
show_view(self.subviews[self.selected], true, true)
end
function Pages:getSelected()
return self.selected, self.subviews[self.selected]
end end
---------------- ----------------
@ -43,6 +108,7 @@ EditField.ATTRS{
text_pen = DEFAULT_NIL, text_pen = DEFAULT_NIL,
on_char = DEFAULT_NIL, on_char = DEFAULT_NIL,
on_change = DEFAULT_NIL, on_change = DEFAULT_NIL,
on_submit = DEFAULT_NIL,
} }
function EditField:onRenderBody(dc) function EditField:onRenderBody(dc)
@ -60,7 +126,10 @@ function EditField:onRenderBody(dc)
end end
function EditField:onInput(keys) function EditField:onInput(keys)
if keys._STRING then if self.on_submit and keys.SELECT then
self.on_submit(self.text)
return true
elseif keys._STRING then
local old = self.text local old = self.text
if keys._STRING == 0 then if keys._STRING == 0 then
self.text = string.sub(old, 1, #old-1) self.text = string.sub(old, 1, #old-1)
@ -77,4 +146,351 @@ function EditField:onInput(keys)
end end
end end
-----------
-- Label --
-----------
function parse_label_text(obj)
local text = obj.text or {}
if type(text) ~= 'table' then
text = { text }
end
local curline = nil
local out = { }
local active = nil
local idtab = nil
for _,v in ipairs(text) do
local vv
if type(v) == 'string' then
vv = utils.split_string(v, NEWLINE)
else
vv = { v }
end
for i = 1,#vv do
local cv = vv[i]
if i > 1 then
if not curline then
table.insert(out, {})
end
curline = nil
end
if cv ~= '' then
if not curline then
curline = {}
table.insert(out, curline)
end
if type(cv) == 'string' then
table.insert(curline, { text = cv })
else
table.insert(curline, cv)
if cv.on_activate then
active = active or {}
table.insert(active, cv)
end
if cv.id then
idtab = idtab or {}
idtab[cv.id] = cv
end
end
end
end
end
obj.text_lines = out
obj.text_active = active
obj.text_ids = idtab
end
function render_text(obj,dc,x0,y0,pen,dpen)
local width = 0
for iline,line in ipairs(obj.text_lines) do
local x = 0
if dc then
dc:seek(x+x0,y0+iline-1)
end
for _,token in ipairs(line) do
token.line = iline
token.x1 = x
if token.gap then
x = x + token.gap
if dc then
dc:advance(token.gap)
end
end
if token.text or token.key then
local text = getval(token.text) or ''
local keypen
if dc then
if getval(token.disabled) then
dc:pen(getval(token.dpen) or dpen)
keypen = COLOR_GREEN
else
dc:pen(getval(token.pen) or pen)
keypen = COLOR_LIGHTGREEN
end
end
x = x + #text
if token.key then
local keystr = gui.getKeyDisplay(token.key)
local sep = token.key_sep or ''
if sep == '()' then
if dc then
dc:string(text)
dc:string(' ('):string(keystr,keypen):string(')')
end
x = x + 3
else
if dc then
dc:string(keystr,keypen):string(sep):string(text)
end
x = x + #sep
end
else
if dc then
dc:string(text)
end
end
end
token.x2 = x
end
width = math.max(width, x)
end
obj.text_width = width
end
function check_text_keys(self, keys)
if self.text_active then
for _,item in ipairs(self.text_active) do
if item.key and keys[item.key] and not getval(item.disabled) then
item.on_activate()
return true
end
end
end
end
Label = defclass(Label, Widget)
Label.ATTRS{
text_pen = COLOR_WHITE,
text_dpen = COLOR_DARKGREY,
auto_height = true,
}
function Label:init(args)
self:setText(args.text)
end
function Label:setText(text)
self.text = text
parse_label_text(self)
if self.auto_height then
self.frame = self.frame or {}
self.frame.h = self:getTextHeight()
end
end
function Label:itemById(id)
if self.text_ids then
return self.text_ids[id]
end
end
function Label:getTextHeight()
return #self.text_lines
end
function Label:getTextWidth()
render_text(self)
return self.text_width
end
function Label:onRenderBody(dc)
render_text(self,dc,0,0,self.text_pen,self.text_dpen)
end
function Label:onInput(keys)
return check_text_keys(self, keys)
end
----------
-- List --
----------
List = defclass(List, Widget)
STANDARDSCROLL = {
STANDARDSCROLL_UP = -1,
STANDARDSCROLL_DOWN = 1,
STANDARDSCROLL_PAGEUP = '-page',
STANDARDSCROLL_PAGEDOWN = '+page',
}
List.ATTRS{
text_pen = COLOR_CYAN,
cursor_pen = COLOR_LIGHTCYAN,
cursor_dpen = DEFAULT_NIL,
inactive_pen = DEFAULT_NIL,
on_select = DEFAULT_NIL,
on_submit = DEFAULT_NIL,
row_height = 1,
scroll_keys = STANDARDSCROLL,
}
function List:init(info)
self.page_top = 1
self.page_size = 1
self:setChoices(info.choices, info.selected)
end
function List:setChoices(choices, selected)
self.choices = choices or {}
for i,v in ipairs(self.choices) do
if type(v) ~= 'table' then
v = { text = v }
self.choices[i] = v
end
v.text = v.text or v.caption or v[1]
parse_label_text(v)
end
self:setSelected(selected)
end
function List:setSelected(selected)
self.selected = selected or self.selected or 1
self:moveCursor(0, true)
return self.selected
end
function List:getSelected()
return self.selected, self.choices[self.selected]
end
function List:getContentWidth()
local width = 0
for i,v in ipairs(self.choices) do
render_text(v)
local roww = v.text_width
if v.key then
roww = roww + 3 + #gui.getKeyDisplay(v.key)
end
width = math.max(width, roww)
end
return width
end
function List:getContentHeight()
return #self.choices * self.row_height
end
function List:postComputeFrame(body)
self.page_size = math.max(1, math.floor(body.height / self.row_height))
self:moveCursor(0)
end
function List:moveCursor(delta, force_cb)
local page = math.max(1, self.page_size)
local cnt = #self.choices
local off = self.selected+delta-1
local ds = math.abs(delta)
if ds > 1 then
if off >= cnt+ds-1 then
off = 0
else
off = math.min(cnt-1, off)
end
if off <= -ds then
off = cnt-1
else
off = math.max(0, off)
end
end
self.selected = 1 + off % cnt
self.page_top = 1 + page * math.floor((self.selected-1) / page)
if (force_cb or delta ~= 0) and self.on_select then
self.on_select(self:getSelected())
end
end
function List:onRenderBody(dc)
local choices = self.choices
local top = self.page_top
local iend = math.min(#choices, top+self.page_size-1)
for i = top,iend do
local obj = choices[i]
local current = (i == self.selected)
local cur_pen = self.text_pen
local cur_dpen = cur_pen
if current and active then
cur_pen = self.cursor_pen
cur_dpen = self.cursor_dpen or self.text_pen
elseif current then
cur_pen = self.inactive_pen or self.cursor_pen
cur_dpen = self.inactive_pen or self.text_pen
end
local y = (i - top)*self.row_height
render_text(obj, dc, 0, y, cur_pen, cur_dpen)
if obj.key then
local keystr = gui.getKeyDisplay(obj.key)
dc:seek(dc.width-2-#keystr,y):pen(self.text_pen)
dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')')
end
end
end
function List:onInput(keys)
if self.on_submit and keys.SELECT then
self.on_submit(self:getSelected())
return true
else
for k,v in pairs(self.scroll_keys) do
if keys[k] then
if v == '+page' then
v = self.page_size
elseif v == '-page' then
v = -self.page_size
end
self:moveCursor(v)
return true
end
end
for i,v in ipairs(self.choices) do
if v.key and keys[v.key] then
self:setSelected(i)
if self.on_submit then
self.on_submit(self:getSelected())
end
return true
end
end
local current = self.choices[self.selected]
if current then
return check_text_keys(current, keys)
end
end
end
return _ENV return _ENV

@ -302,6 +302,24 @@ function sort_vector(vector,field,cmp)
return vector return vector
end end
-- Linear search
function linear_index(vector,obj)
local min,max
if df.isvalid(vector) then
min,max = 0,#vector-1
else
min,max = 1,#vector
end
for i=min,max do
if vector[i] == obj then
return i
end
end
return nil
end
-- Binary search in a vector or lua table -- Binary search in a vector or lua table
function binsearch(vector,key,field,cmp,min,max) function binsearch(vector,key,field,cmp,min,max)
if not(min and max) then if not(min and max) then