Try to reimplement the inventory monitor by falconne in lua.

For no other reason than to provide a complete example of lua
interface for a native plugin :)

TODO: paint the graph in the right pane.
develop
Alexander Gavrilov 2012-11-30 19:10:17 +04:00
parent 2cb594ba89
commit 0bfe006016
4 changed files with 430 additions and 42 deletions

@ -2710,6 +2710,16 @@ containing newlines, or a table with the following possible fields:
Specifies a pen to paint as one tile before the main part of the token. Specifies a pen to paint as one tile before the main part of the token.
* ``token.width = ...``
If specified either as a value or a callback, the text field is padded
or truncated to the specified number.
* ``token.pad_char = '?'``
If specified together with ``width``, the padding area is filled with
this character instead of just being skipped over.
* ``token.key = '...'`` * ``token.key = '...'``
Specifies the keycode associated with the token. The string description Specifies the keycode associated with the token. The string description
@ -2842,6 +2852,7 @@ In addition to passing through all attributes supported by List, it
supports: supports:
:edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field.
:edit_below: If true, the edit field is placed below the list instead of above.
:not_found_label: Specifies the text of the label shown when no items match the filter. :not_found_label: Specifies the text of the label shown when no items match the filter.
The list choices may include the following attributes: The list choices may include the following attributes:

@ -112,10 +112,14 @@ function inset_frame(rect, inset, gap)
return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
end end
function compute_frame_body(wavail, havail, spec, inset, gap) function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame)
gap = gap or 0 gap = gap or 0
local l,t,r,b = parse_inset(inset) local l,t,r,b = parse_inset(inset)
local rect = compute_frame_rect(wavail, havail, spec, gap*2+l+r, gap*2+t+b) local xgap,ygap = 0,0
if inner_frame then
xgap,ygap = gap*2+l+r, gap*2+t+b
end
local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap)
local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
return rect, body return rect, body
end end
@ -623,7 +627,7 @@ end
function FramedScreen:computeFrame(parent_rect) function FramedScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height local sw, sh = parent_rect.width, parent_rect.height
local fw, fh = self:getWantedFrameSize(parent_rect) local fw, fh = self:getWantedFrameSize(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, true)
end end
function FramedScreen:onRenderFrame(dc, rect) function FramedScreen:onRenderFrame(dc, rect)

@ -60,6 +60,7 @@ Panel = defclass(Panel, Widget)
Panel.ATTRS { Panel.ATTRS {
on_render = DEFAULT_NIL, on_render = DEFAULT_NIL,
on_layout = DEFAULT_NIL,
} }
function Panel:init(args) function Panel:init(args)
@ -70,6 +71,10 @@ function Panel:onRenderBody(dc)
if self.on_render then self.on_render(dc) end if self.on_render then self.on_render(dc) end
end end
function Panel:postComputeFrame(body)
if self.on_layout then self.on_layout(body) end
end
----------- -----------
-- Pages -- -- Pages --
----------- -----------
@ -242,7 +247,7 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled)
end end
if token.text or token.key then if token.text or token.key then
local text = getval(token.text) or '' local text = ''..(getval(token.text) or '')
local keypen local keypen
if dc then if dc then
@ -256,7 +261,23 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled)
end end
end end
local width = getval(token.width)
local padstr
if width then
x = x + width
if #text > width then
text = string.sub(text,1,width)
else
if token.pad_char then
padstr = string.rep(token.pad_char,width-#text)
end
if dc and token.rjustify then
if padstr then dc:string(padstr) else dc:advance(width-#text) end
end
end
else
x = x + #text x = x + #text
end
if token.key then if token.key then
local keystr = gui.getKeyDisplay(token.key) local keystr = gui.getKeyDisplay(token.key)
@ -281,6 +302,10 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled)
dc:string(text) dc:string(text)
end end
end end
if width and dc and not token.rjustify then
if padstr then dc:string(padstr) else dc:advance(width-#text) end
end
end end
token.x2 = x token.x2 = x
@ -591,10 +616,14 @@ end
FilteredList = defclass(FilteredList, Widget) FilteredList = defclass(FilteredList, Widget)
FilteredList.ATTRS {
edit_below = false,
}
function FilteredList:init(info) function FilteredList:init(info)
self.edit = EditField{ self.edit = EditField{
text_pen = info.edit_pen or info.cursor_pen, text_pen = info.edit_pen or info.cursor_pen,
frame = { l = info.icon_width, t = 0 }, frame = { l = info.icon_width, t = 0, h = 1 },
on_change = self:callback('onFilterChange'), on_change = self:callback('onFilterChange'),
on_char = self:callback('onFilterChar'), on_char = self:callback('onFilterChar'),
} }
@ -608,6 +637,10 @@ function FilteredList:init(info)
scroll_keys = info.scroll_keys, scroll_keys = info.scroll_keys,
icon_width = info.icon_width, icon_width = info.icon_width,
} }
if self.edit_below then
self.edit.frame = { l = info.icon_width, b = 0, h = 1 }
self.list.frame = { t = 0, b = 2 }
end
if info.on_select then if info.on_select then
self.list.on_select = function() self.list.on_select = function()
return info.on_select(self:getSelected()) return info.on_select(self:getSelected())
@ -627,7 +660,7 @@ function FilteredList:init(info)
visible = false, visible = false,
text = info.not_found_label or 'No matches', text = info.not_found_label or 'No matches',
text_pen = COLOR_LIGHTRED, text_pen = COLOR_LIGHTRED,
frame = { l = info.icon_width, t = 2 }, frame = { l = info.icon_width, t = self.list.frame.t },
} }
self:addviews{ self.edit, self.list, self.not_found } self:addviews{ self.edit, self.list, self.not_found }
self:setChoices(info.choices, info.selected) self:setChoices(info.choices, info.selected)

@ -66,20 +66,64 @@ function is_caste_mat(iobj)
end end
function describe_material(iobj) function describe_material(iobj)
local matline = 'any material' local matflags = utils.list_bitfield_flags(iobj.mat_mask)
if #matflags > 0 then
matflags = 'any '..table.concat(matflags, '/')
else
matflags = nil
end
if is_caste_mat(iobj) then if is_caste_mat(iobj) then
matline = 'no material' return 'no material'
elseif (iobj.mat_type or -1) >= 0 then elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
local matline
if info then if info then
matline = info:toString() matline = info:toString()
else else
matline = iobj.mat_type..':'..iobj.mat_index matline = iobj.mat_type..':'..iobj.mat_index
end end
return matline, matflags
else
return matflags or 'any material'
end
end
function current_stock(iobj)
if iobj.goal_by_count then
return iobj.cur_count
else
return iobj.cur_amount
end
end
function if_by_count(iobj,bc,ba)
if iobj.goal_by_count then
return bc
else
return ba
end
end
function compute_trend(history,field)
local count = #history
if count == 0 then
return 0
end
local sumX,sumY,sumXY,sumXX = 0,0,0,0
for i,v in ipairs(history) do
sumX = sumX + i
sumY = sumY + v[field]
sumXY = sumXY + i*v[field]
sumXX = sumXX + i*i
end end
return matline return (count * sumXY - sumX * sumY) / (count * sumXX - sumX * sumX)
end end
------------------------
-- RANGE EDITOR GROUP --
------------------------
local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false } local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false }
RangeEditor = defclass(RangeEditor, widgets.Label) RangeEditor = defclass(RangeEditor, widgets.Label)
@ -162,6 +206,10 @@ function RangeEditor:onIncRange(field, delta)
self.save_cb(cons) self.save_cb(cons)
end end
---------------------------
-- NEW CONSTRAINT DIALOG --
---------------------------
NewConstraint = defclass(NewConstraint, gui.FramedScreen) NewConstraint = defclass(NewConstraint, gui.FramedScreen)
NewConstraint.focus_path = 'workflow/new' NewConstraint.focus_path = 'workflow/new'
@ -177,7 +225,7 @@ NewConstraint.ATTRS {
} }
function NewConstraint:init(args) function NewConstraint:init(args)
self.constraint = args.constraint or {} self.constraint = args.constraint or { item_type = -1 }
rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false }) rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false })
local matlist = {} local matlist = {}
@ -202,8 +250,16 @@ function NewConstraint:init(args)
frame = { l = 1, t = 2, w = 26 }, frame = { l = 1, t = 2, w = 26 },
text = { text = {
'Type: ', 'Type: ',
{ pen = COLOR_LIGHTCYAN, { pen = function()
text = function() return describe_item_type(self.constraint) end }, if self:isValid() then return COLOR_LIGHTCYAN else return COLOR_LIGHTRED end
end,
text = function()
if self:isValid() then
return describe_item_type(self.constraint)
else
return 'item not set'
end
end },
NEWLINE, ' ', NEWLINE, ' ',
{ key = 'CUSTOM_T', text = ': Select, ', { key = 'CUSTOM_T', text = ': Select, ',
on_activate = self:callback('chooseType') }, on_activate = self:callback('chooseType') },
@ -277,6 +333,7 @@ function NewConstraint:init(args)
{ key = 'LEAVESCREEN', text = ': Cancel, ', { key = 'LEAVESCREEN', text = ': Cancel, ',
on_activate = self:callback('dismiss') }, on_activate = self:callback('dismiss') },
{ key = 'MENU_CONFIRM', key_sep = ': ', { key = 'MENU_CONFIRM', key_sep = ': ',
enabled = self:callback('isValid'),
text = function() text = function()
if self.is_existing then return 'Update' else return 'Create new' end if self.is_existing then return 'Update' else return 'Create new' end
end, end,
@ -295,9 +352,17 @@ function NewConstraint:postinit()
self:onChange() self:onChange()
end end
function NewConstraint:isValid()
return self.constraint.item_type >= 0
end
function NewConstraint:onChange() function NewConstraint:onChange()
local token = workflow.constraintToToken(self.constraint) local token = workflow.constraintToToken(self.constraint)
local out = workflow.findConstraint(token) local out
if self:isValid() then
out = workflow.findConstraint(token)
end
if out then if out then
self.constraint = out self.constraint = out
@ -390,6 +455,288 @@ function NewConstraint:onRangeChange()
cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1)) cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1))
end end
------------------------------
-- GLOBAL CONSTRAINT SCREEN --
------------------------------
ConstraintList = defclass(ConstraintList, gui.FramedScreen)
ConstraintList.focus_path = 'workflow/list'
ConstraintList.ATTRS {
frame_title = 'Workflow Status',
frame_inset = 0,
frame_background = COLOR_BLACK,
frame_style = gui.BOUNDARY_FRAME,
}
function ConstraintList:init(args)
local fwidth_cb = self:cb_getfield('fwidth')
self.fwidth = 20
self.sort_by_severity = false
self:addviews{
widgets.Panel{
frame = { w = 31, r = 0, h = 6, t = 0 },
frame_inset = 1,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
enabled = self:callback('isAnySelected'),
text = {
{ text = function()
local cur = self:getCurConstraint()
if cur then
return string.format(
'Currently %d (%d in use)',
current_stock(cur),
if_by_count(cur, cur.cur_in_use_count, cur.cur_in_use_amount)
)
else
return 'No constraint selected'
end
end }
}
},
RangeEditor{
frame = { l = 0, t = 2 },
enabled = self:callback('isAnySelected'),
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
},
}
},
widgets.Widget{
active = false,
frame = { w = 1, r = 31 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Widget{
active = false,
frame = { w = 31, r = 0, h = 1, t = 6 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Panel{
frame = { l = 0, r = 32 },
frame_inset = 1,
on_layout = function(body)
self.fwidth = body.width - (12+1+1+7+1+1+1+7)
end,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
text_pen = COLOR_CYAN,
text = {
{ text = 'Item', width = 12 }, ' ',
{ text = 'Material etc', width = fwidth_cb }, ' ',
{ text = 'Stock / Limit' },
}
},
widgets.FilteredList{
view_id = 'list',
frame = { t = 2, b = 2 },
edit_below = true,
not_found_label = 'No matching constraints',
edit_pen = COLOR_LIGHTCYAN,
text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK },
cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN },
},
widgets.Label{
frame = { b = 0, h = 1 },
text = {
{ key = 'CUSTOM_SHIFT_A', text = ': Add',
on_activate = self:callback('onNewConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_X', text = ': Delete',
on_activate = self:callback('onDeleteConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_O', text = ': Severity Order',
on_activate = self:callback('onSwitchSort'),
pen = function()
if self.sort_by_severity then
return COLOR_LIGHTCYAN
else
return COLOR_WHITE
end
end }, ', ',
{ key = 'CUSTOM_SHIFT_S', text = ': Search',
on_activate = function()
self.subviews.list.edit.active = not self.subviews.list.edit.active
end,
pen = function()
if self.subviews.list.edit.active then
return COLOR_LIGHTCYAN
else
return COLOR_WHITE
end
end }
}
}
}
},
}
self.subviews.list.edit.active = false
self:initListChoices()
end
function stock_trend_color(cons)
local stock = current_stock(cons)
if stock >= cons.goal_value - cons.goal_gap then
return COLOR_LIGHTGREEN, 0
elseif stock <= cons.goal_gap then
return COLOR_LIGHTRED, 4
elseif stock >= cons.goal_value - 2*cons.goal_gap then
return COLOR_GREEN, 1
elseif stock <= 2*cons.goal_gap then
return COLOR_RED, 3
else
local trend = if_by_count(cons, cons.trend_count, cons.trend_amount)
if trend > 0.3 then
return COLOR_GREEN, 1
elseif trend < -0.3 then
return COLOR_RED, 3
else
return COLOR_GREY, 2
end
end
end
function ConstraintList:initListChoices(clist, sel_token)
clist = clist or workflow.listConstraints(nil, true)
local fwidth_cb = self:cb_getfield('fwidth')
local choices = {}
for i,cons in ipairs(clist) do
cons.trend_count = compute_trend(cons.history, 'cur_count')
cons.trend_amount = compute_trend(cons.history, 'cur_amount')
local itemstr = describe_item_type(cons)
local matstr,matflagstr = describe_material(cons)
if matflagstr then
matstr = matflagstr .. ' ' .. matstr
end
if cons.min_quality > 0 or cons.is_local then
local lst = {}
if cons.is_local then
table.insert(lst, 'local')
end
if cons.min_quality > 0 then
table.insert(lst, string.lower(df.item_quality[cons.min_quality]))
end
matstr = matstr .. ' ('..table.concat(lst,',')..')'
end
local goal_color = COLOR_GREY
if #cons.jobs == 0 then
goal_color = COLOR_RED
elseif cons.is_delayed then
goal_color = COLOR_YELLOW
end
table.insert(choices, {
text = {
{ text = itemstr, width = 12, pad_char = ' ' }, ' ',
{ text = matstr, width = fwidth_cb, pad_char = ' ' }, ' ',
{ text = curry(current_stock,cons), width = 7, rjustify = true,
pen = function() return { fg = stock_trend_color(cons) } end },
{ text = curry(if_by_count,cons,'S','I'), gap = 1,
pen = { fg = COLOR_GREY } },
{ text = function() return cons.goal_value end, gap = 1,
pen = { fg = goal_color } }
},
severity = select(2, stock_trend_color(cons)),
search_key = itemstr .. ' | ' .. matstr,
token = cons.token,
obj = cons
})
end
self:setChoices(choices, sel_token)
end
function ConstraintList:isAnySelected()
return self.subviews.list:getSelected() ~= nil
end
function ConstraintList:getCurConstraint()
local selidx,selobj = self.subviews.list:getSelected()
if selobj then return selobj.obj end
end
function ConstraintList:onSwitchSort()
self.sort_by_severity = not self.sort_by_severity
self:setChoices(self.subviews.list:getChoices())
end
function ConstraintList:setChoices(choices, sel_token)
if self.sort_by_severity then
table.sort(choices, function(a,b)
return a.severity > b.severity
or (a.severity == b.severity and
current_stock(a.obj)/a.obj.goal_value < current_stock(b.obj)/b.obj.goal_value)
end)
else
table.sort(choices, function(a,b) return a.search_key < b.search_key end)
end
local selidx = nil
if sel_token then
selidx = utils.linear_index(choices, sel_token, 'token')
end
local list = self.subviews.list
local filter = list:getFilter()
list:setChoices(choices, selidx)
if filter ~= '' then
list:setFilter(filter, selidx)
if selidx and list:getSelected() ~= selidx then
list:setFilter('', selidx)
end
end
end
function ConstraintList:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
else
ConstraintList.super.onInput(self, keys)
end
end
function ConstraintList:onNewConstraint()
NewConstraint{
on_submit = self:callback('saveConstraint')
}:show()
end
function ConstraintList:saveConstraint(cons)
local out = workflow.setConstraint(cons.token, cons.goal_by_count, cons.goal_value, cons.goal_gap)
self:initListChoices(nil, out.token)
end
function ConstraintList:onDeleteConstraint()
local cons = self:getCurConstraint()
dlg.showYesNoPrompt(
'Delete Constraint',
'Really delete the current constraint?',
COLOR_YELLOW,
function()
workflow.deleteConstraint(cons.token)
self:initListChoices()
end
)
end
-------------------------------
-- WORKSHOP JOB INFO OVERLAY --
-------------------------------
JobConstraints = defclass(JobConstraints, guidm.MenuOverlay) JobConstraints = defclass(JobConstraints, guidm.MenuOverlay)
JobConstraints.focus_path = 'workflow/job' JobConstraints.focus_path = 'workflow/job'
@ -480,24 +827,12 @@ function JobConstraints:initListChoices(clist, sel_token)
end end
itemstr = itemstr .. ' ('..table.concat(lst,',')..')' itemstr = itemstr .. ' ('..table.concat(lst,',')..')'
end end
local matstr = describe_material(cons) local matstr,matflagstr = describe_material(cons)
local matflagstr = ''
local matflags = utils.list_bitfield_flags(cons.mat_mask)
if #matflags > 0 then
matflags[1] = 'any '..matflags[1]
if matstr == 'any material' then
matstr = table.concat(matflags, ', ')
matflags = {}
end
end
if #matflags > 0 then
matflagstr = table.concat(matflags, ', ')
end
table.insert(choices, { table.insert(choices, {
text = { text = {
goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE, goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE,
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr ' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', (matflagstr or '')
}, },
token = cons.token, token = cons.token,
obj = cons obj = cons
@ -593,13 +928,18 @@ function JobConstraints:onInput(keys)
end end
end end
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then local args = {...}
if args[1] == 'list' then
check_enabled(function() ConstraintList{}:show() end)
else
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then
qerror("This script requires a workshop job selected in the 'q' mode") qerror("This script requires a workshop job selected in the 'q' mode")
end end
local job = dfhack.gui.getSelectedJob() local job = dfhack.gui.getSelectedJob()
check_enabled(function() check_enabled(function()
check_repeat(job, function() check_repeat(job, function()
local clist = workflow.listConstraints(job) local clist = workflow.listConstraints(job)
if not clist then if not clist then
@ -608,5 +948,5 @@ check_enabled(function()
end end
JobConstraints{ job = job, clist = clist }:show() JobConstraints{ job = job, clist = clist }:show()
end) end)
end) end)
end