diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 9fc94a025..3f57e5722 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -125,6 +125,10 @@ end -- Misc functions +NEWLINE = "\n" +COMMA = "," +PERIOD = "." + function printall(table) local ok,f,t,k = pcall(pairs,table) if ok then diff --git a/library/lua/gui.lua b/library/lua/gui.lua index ea2a79da8..125b59544 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -116,7 +116,7 @@ function blink_visible(delay) return math.floor(dfhack.getTickCount()/delay) % 2 == 0 end -local function to_pen(default, pen, bg, bold) +function to_pen(default, pen, bg, bold) if pen == nil then return default or {} elseif type(pen) ~= 'table' then @@ -363,6 +363,8 @@ function View:init(args) end function View:addviews(list) + if not list then return end + local sv = self.subviews for _,obj in ipairs(list) do @@ -413,11 +415,15 @@ function View:updateLayout(parent_rect) self.frame_parent_rect = parent_rect end + self:invoke_before('preUpdateLayout', parent_rect) + local frame_rect,body_rect = self:computeFrame(parent_rect) self.frame_rect = 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:invoke_after('postUpdateLayout', self.frame_body) @@ -432,6 +438,8 @@ function View:renderSubviews(dc) end function View:render(dc) + self:onRenderFrame(dc, self.frame_rect) + local sub_dc = Painter{ view_rect = self.frame_body, clip_view = dc @@ -442,6 +450,9 @@ function View:render(dc) self:renderSubviews(sub_dc) end +function View:onRenderFrame(dc,rect) +end + function View:onRenderBody(dc) end @@ -609,8 +620,7 @@ function FramedScreen:computeFrame(parent_rect) return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1) end -function FramedScreen:render(dc) - local rect = self.frame_rect +function FramedScreen:onRenderFrame(dc, rect) local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2 if rect.wgap <= 0 and rect.hgap <= 0 then @@ -621,8 +631,6 @@ function FramedScreen:render(dc) end paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title) - - FramedScreen.super.render(self, dc) end return _ENV diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 10a43c22e..7d8058a93 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -14,47 +14,35 @@ MessageBox.focus_path = 'MessageBox' MessageBox.ATTRS{ frame_style = gui.GREY_LINE_FRAME, + frame_inset = 1, -- new attrs - text = {}, on_accept = DEFAULT_NIL, on_cancel = DEFAULT_NIL, on_close = DEFAULT_NIL, - text_pen = DEFAULT_NIL, } -function MessageBox:preinit(info) - if type(info.text) == 'string' then - info.text = utils.split_string(info.text, "\n") - end +function MessageBox:init(info) + self:addviews{ + widgets.Label{ + view_id = 'label', + text = info.text, + text_pen = info.text_pen, + frame = { l = 0, t = 0 }, + auto_height = true + } + } end function MessageBox:getWantedFrameSize() - local text = self.text - local w = #(self.frame_title or '') + 4 - w = math.max(w, 20) - 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 + local label = self.subviews.label + local width = math.max(self.frame_width or 0, 20, #(self.frame_title or '') + 4) + return math.max(width, label:getTextWidth()), label:getTextHeight() end -function MessageBox:onRenderBody(dc) - if #self.text > 0 then - 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 - +function MessageBox:onRenderFrame(dc,rect) + MessageBox.super.onRenderFrame(self,dc,rect) if self.on_accept then - local fr = self.frame_rect - 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') + dc:seek(rect.x1+2,rect.y2):key('LEAVESCREEN'):string('/'):key('MENU_CONFIRM') end end @@ -75,6 +63,8 @@ function MessageBox:onInput(keys) if self.on_cancel then self.on_cancel() end + else + self:inputToSubviews(keys) end end @@ -115,14 +105,14 @@ function InputBox:init(info) view_id = 'edit', text = info.input, text_pen = info.input_pen, - frame = { l = 1, r = 1, h = 1 }, + frame = { l = 0, r = 0, h = 1 }, } } end function InputBox:getWantedFrameSize() local mw, mh = InputBox.super.getWantedFrameSize(self) - self.subviews.edit.frame.t = mh + self.subviews.edit.frame.t = mh+1 return mw, mh+2 end @@ -165,107 +155,47 @@ ListBox.ATTRS{ on_select = DEFAULT_NIL } -function InputBox:preinit(info) +function ListBox:preinit(info) info.on_accept = nil end function ListBox:init(info) - self.page_top = 1 -end + local spen = gui.to_pen(COLOR_CYAN, info.select_pen, nil, false) + local cpen = gui.to_pen(COLOR_LIGHTCYAN, info.cursor_pen or info.select_pen, nil, true) -local function choice_text(entry) - if type(entry)=="table" then - return entry.caption or entry[1] - else - return entry - end + self:addviews{ + widgets.List{ + view_id = 'list', + selected = info.selected, + choices = info.choices, + 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 function ListBox:getWantedFrameSize() - local mw, mh = ListBox.super.getWantedFrameSize(self) - for _,v in ipairs(self.choices) do - local text = choice_text(v) - mw = math.max(mw, #text+2) - 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 + local mw, mh = InputBox.super.getWantedFrameSize(self) + local list = self.subviews.list + list.frame.t = mh+1 + return math.max(mw, list:getContentWidth()), mh+1+list:getContentHeight() end function ListBox:onInput(keys) - if keys.SELECT 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 + if keys.LEAVESCREEN then self:dismiss() if self.on_cancel then self.on_cancel() end - elseif keys.STANDARDSCROLL_UP then - self:moveCursor(-1) - 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) + else + self:inputToSubviews(keys) end end diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 010ea5510..f3796d0e3 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -7,6 +7,21 @@ local utils = require('utils') 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 -- ------------ @@ -24,12 +39,62 @@ function Widget:computeFrame(parent_rect) return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset) end -function Widget:render(dc) +function Widget:onRenderFrame(dc, rect) if self.frame_background then - dc:fill(self.frame_rect, self.frame_background) + dc:fill(rect, self.frame_background) end +end + +----------- +-- Panel -- +----------- + +Panel = defclass(Panel, Widget) - Widget.super.render(self, dc) +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 ---------------- @@ -43,6 +108,7 @@ EditField.ATTRS{ text_pen = DEFAULT_NIL, on_char = DEFAULT_NIL, on_change = DEFAULT_NIL, + on_submit = DEFAULT_NIL, } function EditField:onRenderBody(dc) @@ -60,7 +126,10 @@ function EditField:onRenderBody(dc) end 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 if keys._STRING == 0 then self.text = string.sub(old, 1, #old-1) @@ -77,4 +146,351 @@ function EditField:onInput(keys) 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 diff --git a/library/lua/utils.lua b/library/lua/utils.lua index b46363cd7..2507c9964 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -302,6 +302,24 @@ function sort_vector(vector,field,cmp) return vector 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 function binsearch(vector,key,field,cmp,min,max) if not(min and max) then