Implement a dialog to specify a workflow constraint with all features.

develop
Alexander Gavrilov 2012-11-17 20:32:39 +04:00
parent 0b568942f4
commit 2953e4c386
5 changed files with 427 additions and 140 deletions

@ -23,6 +23,7 @@ MaterialDialog.ATTRS{
frame_title = 'Select Material',
-- new attrs
none_caption = 'none',
hide_none = false,
use_inorganic = true,
use_creature = true,
use_plant = true,
@ -68,7 +69,7 @@ function MaterialDialog:init(info)
end
function MaterialDialog:getWantedFrameSize(rect)
return math.max(40, #self.prompt), math.min(28, rect.height-8)
return math.max(self.frame_width or 40, #self.prompt), math.min(28, rect.height-8)
end
function MaterialDialog:onDestroy()
@ -78,9 +79,10 @@ function MaterialDialog:onDestroy()
end
function MaterialDialog:initBuiltinMode()
local choices = {
{ text = self.none_caption, mat_type = -1, mat_index = -1 },
}
local choices = {}
if not self.hide_none then
table.insert(choices, { text = self.none_caption, mat_type = -1, mat_index = -1 })
end
if self.use_inorganic then
table.insert(choices, {
@ -281,9 +283,15 @@ function ItemTypeDialog(args)
args.with_filter = true
args.icon_width = 2
local choices = { {
icon = '?', text = args.none_caption or 'none', item_type = -1, item_subtype = -1
} }
local choices = {}
if not args.hide_none then
table.insert(choices, {
icon = '?', text = args.none_caption or 'none',
item_type = -1, item_subtype = -1
})
end
local filter = args.item_filter
for itype = 0,df.item_type._last_item do

@ -488,6 +488,18 @@ function List:onRenderBody(dc)
local iend = math.min(#choices, top+self.page_size-1)
local iw = self.icon_width
local function paint_icon(icon, obj)
if type(icon) ~= 'string' then
dc:char(nil,icon)
else
if current then
dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen)
else
dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen)
end
end
end
for i = top,iend do
local obj = choices[i]
local current = (i == self.selected)
@ -499,30 +511,28 @@ function List:onRenderBody(dc)
end
local y = (i - top)*self.row_height
local icon = getval(obj.icon)
if iw and obj.icon then
local icon = getval(obj.icon)
if icon then
dc:seek(0, y)
if type(icon) ~= 'string' then
dc:char(nil,icon)
else
if current then
dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen)
else
dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen)
end
end
end
if iw and icon then
dc:seek(0, y)
paint_icon(icon, obj)
end
render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current)
local ip = dc.width
if obj.key then
local keystr = gui.getKeyDisplay(obj.key)
dc:seek(dc.width-2-#keystr,y):pen(self.text_pen)
ip = ip-2-#keystr
dc:seek(ip,y):pen(self.text_pen)
dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')')
end
if icon and not iw then
dc:seek(ip-1,y)
paint_icon(icon, obj)
end
end
end

@ -9,6 +9,7 @@ local utils = require 'utils'
* isEnabled()
* setEnabled(enable)
* listConstraints([job]) -> {...}
* findConstraint(token) -> {...} or nil
* setConstraint(token[, by_count, goal, gap]) -> {...}
* deleteConstraint(token) -> true/false
@ -255,7 +256,7 @@ function constraintToToken(cspec)
end
local mask_part
if cspec.mat_mask then
mask_part = table.concat(utils.list_bitfield_flags(cspec.mat_mask), ',')
mask_part = string.upper(table.concat(utils.list_bitfield_flags(cspec.mat_mask), ','))
end
local mat_part
if cspec.mat_type and cspec.mat_type >= 0 then
@ -270,8 +271,9 @@ function constraintToToken(cspec)
if cspec.is_local then
table.insert(qlist, "LOCAL")
end
if cspec.quality and cspec.quality > 0 then
table.insert(qlist, df.item_quality[cspec.quality] or error('invalid quality: '..cspec.quality))
if cspec.min_quality and cspec.min_quality > 0 then
local qn = df.item_quality[cspec.min_quality] or error('invalid quality: '..cspec.min_quality)
table.insert(qlist, qn)
end
local qpart
if #qlist > 0 then

@ -446,7 +446,7 @@ static void cleanup_state(color_ostream &out)
}
static void check_lost_jobs(color_ostream &out, int ticks);
static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg = NULL);
static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg = NULL, bool create = true);
static void start_protect(color_ostream &out)
{
@ -660,7 +660,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out)
* ITEM COUNT CONSTRAINT *
******************************/
static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg)
static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg, bool create)
{
std::vector<std::string> tokens;
split_string(&tokens, str, "/");
@ -683,7 +683,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str
if (item.subtype >= 0)
weight += 10000;
df::dfhack_material_category mat_mask;
df::dfhack_material_category mat_mask(0);
std::string maskstr = vector_get(tokens,1);
if (!maskstr.empty() && !parseJobMaterialCategory(&mat_mask, maskstr)) {
out.printerr("Cannot decode material mask: %s\n", maskstr.c_str());
@ -757,6 +757,9 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str
return ct;
}
if (!create)
return NULL;
ItemConstraint *nct = new ItemConstraint;
nct->is_craft = is_craft;
nct->item = item;
@ -1346,7 +1349,9 @@ static void push_constraint(lua_State *L, ItemConstraint *cv)
Lua::SetField(L, cv->is_craft, ctable, "is_craft");
lua_getglobal(L, "copyall");
Lua::PushDFObject(L, &cv->mat_mask);
lua_call(L, 1, 1);
lua_setfield(L, -2, "mat_mask");
Lua::SetField(L, cv->material.type, ctable, "mat_type");
@ -1417,6 +1422,22 @@ static int listConstraints(lua_State *L)
return 1;
}
static int findConstraint(lua_State *L)
{
auto token = luaL_checkstring(L, 1);
color_ostream &out = *Lua::GetOutput(L);
update_data_structures(out);
ItemConstraint *icv = get_constraint(out, token, NULL, false);
if (icv)
push_constraint(L, icv);
else
lua_pushnil(L);
return 1;
}
static int setConstraint(lua_State *L)
{
auto token = luaL_checkstring(L, 1);
@ -1425,6 +1446,7 @@ static int setConstraint(lua_State *L)
int gap = luaL_optint(L, 4, -1);
color_ostream &out = *Lua::GetOutput(L);
update_data_structures(out);
ItemConstraint *icv = get_constraint(out, token);
if (!icv)
@ -1451,6 +1473,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(listConstraints),
DFHACK_LUA_COMMAND(findConstraint),
DFHACK_LUA_COMMAND(setConstraint),
DFHACK_LUA_END
};

@ -43,9 +43,357 @@ function check_repeat(job, cb)
end
end
function describe_item_type(iobj)
local itemline = 'any item'
if iobj.is_craft then
itemline = 'any craft'
elseif iobj.item_type >= 0 then
itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type
local subtype = iobj.item_subtype or -1
local def = dfhack.items.getSubtypeDef(iobj.item_type, subtype)
local count = dfhack.items.getSubtypeCount(iobj.item_type, subtype)
if def then
itemline = def.name
elseif count >= 0 then
itemline = 'any '..itemline
end
end
return itemline
end
function is_caste_mat(iobj)
return dfhack.items.isCasteMaterial(iobj.item_type or -1)
end
function describe_material(iobj)
local matline = 'any material'
if is_caste_mat(iobj) then
matline = 'no material'
elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
if info then
matline = info:toString()
else
matline = iobj.mat_type..':'..iobj.mat_index
end
end
return matline
end
local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false }
RangeEditor = defclass(RangeEditor, widgets.Label)
RangeEditor.ATTRS {
get_cb = DEFAULT_NIL,
save_cb = DEFAULT_NIL
}
function RangeEditor:init(args)
self:setText{
{ key = 'BUILDING_TRIGGER_ENABLE_CREATURE',
text = function()
local cons = self.get_cb() or null_cons
if cons.goal_by_count then
return ': Count stacks '
else
return ': Count items '
end
end,
on_activate = self:callback('onChangeUnit') },
{ key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify',
on_activate = self:callback('onEditRange') },
NEWLINE, ' ',
{ key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN',
on_activate = self:callback('onIncRange', 'goal_gap', 5) },
{ key = 'BUILDING_TRIGGER_MIN_SIZE_UP',
on_activate = self:callback('onIncRange', 'goal_gap', -1) },
{ text = function()
local cons = self.get_cb() or null_cons
return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap)
end },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
on_activate = self:callback('onIncRange', 'goal_value', -1) },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_UP',
on_activate = self:callback('onIncRange', 'goal_value', 5) },
{ text = function()
local cons = self.get_cb() or null_cons
return string.format(': Max %-4d', cons.goal_value)
end },
}
end
function RangeEditor:onChangeUnit()
local cons = self.get_cb()
cons.goal_by_count = not cons.goal_by_count
self.save_cb(cons)
end
function RangeEditor:onEditRange()
local cons = self.get_cb()
dlg.showInputPrompt(
'Input Range',
'Enter the new constraint range:',
COLOR_WHITE,
(cons.goal_value-cons.goal_gap)..'-'..cons.goal_value,
function(text)
local maxv = string.match(text, '^%s*(%d+)%s*$')
if maxv then
cons.goal_value = maxv
return self.save_cb(cons)
end
local minv,maxv = string.match(text, '^%s*(%d+)-(%d+)%s*$')
if minv and maxv and minv ~= maxv then
cons.goal_value = math.max(minv,maxv)
cons.goal_gap = math.abs(maxv-minv)
return self.save_cb(cons)
end
dlg.showMessage('Invalid Range', 'This range is invalid: '..text, COLOR_LIGHTRED)
end
)
end
function RangeEditor:onIncRange(field, delta)
local cons = self.get_cb()
if not cons.goal_by_count then
delta = delta * 5
end
cons[field] = math.max(1, cons[field] + delta)
self.save_cb(cons)
end
NewConstraint = defclass(NewConstraint, gui.FramedScreen)
NewConstraint.focus_path = 'workflow/new'
NewConstraint.ATTRS {
frame_style = gui.GREY_LINE_FRAME,
frame_title = 'New workflow constraint',
frame_width = 39,
frame_height = 20,
frame_inset = 1,
constraint = DEFAULT_NIL,
on_submit = DEFAULT_NIL,
}
function NewConstraint:init(args)
self.constraint = args.constraint or {}
rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false })
local matlist = {}
local matsel = 1
local matmask = self.constraint.mat_mask
for i,v in ipairs(df.dfhack_material_category) do
if v and v ~= 'wood2' then
table.insert(matlist, { icon = self:callback('isMatSelected', v), text = v })
if matmask and matmask[v] and matsel == 1 then
matsel = #matlist
end
end
end
self:addviews{
widgets.Label{
frame = { l = 0, t = 0 },
text = 'Items matching:'
},
widgets.Label{
frame = { l = 1, t = 2, w = 26 },
text = {
'Type: ',
{ pen = COLOR_LIGHTCYAN,
text = function() return describe_item_type(self.constraint) end },
NEWLINE, ' ',
{ key = 'CUSTOM_T', text = ': Select, ',
on_activate = self:callback('chooseType') },
{ key = 'CUSTOM_SHIFT_C', text = ': Crafts',
on_activate = self:callback('chooseCrafts') },
NEWLINE, NEWLINE,
'Material: ',
{ pen = COLOR_LIGHTCYAN,
text = function() return describe_material(self.constraint) end },
NEWLINE, ' ',
{ key = 'CUSTOM_P', text = ': Specific',
on_activate = self:callback('chooseMaterial') },
NEWLINE, NEWLINE,
'Other:',
NEWLINE, ' ',
{ key = 'D_MILITARY_SUPPLIES_WATER_DOWN',
on_activate = self:callback('incQuality', -1) },
{ key = 'D_MILITARY_SUPPLIES_WATER_UP', key_sep = ': ',
text = function()
return df.item_quality[self.constraint.min_quality or 0]..' quality'
end,
on_activate = self:callback('incQuality', 1) },
NEWLINE, ' ',
{ key = 'CUSTOM_L', key_sep = ': ',
text = function()
if self.constraint.is_local then
return 'Locally made only'
else
return 'Include foreign'
end
end,
on_activate = self:callback('toggleLocal') },
}
},
widgets.Label{
frame = { l = 0, t = 13 },
text = {
'Desired range: ',
{ pen = COLOR_LIGHTCYAN,
text = function()
local cons = self.constraint
local goal = (cons.goal_value-cons.goal_gap)..'-'..cons.goal_value
if cons.goal_by_count then
return goal .. ' stacks'
else
return goal .. ' items'
end
end },
}
},
RangeEditor{
frame = { l = 1, t = 15 },
get_cb = self:cb_getfield('constraint'),
save_cb = self:callback('onRangeChange'),
},
widgets.Label{
frame = { l = 30, t = 0 },
text = 'Mat class'
},
widgets.List{
view_id = 'matlist',
frame = { l = 30, t = 2, w = 9, h = 18 },
scroll_keys = widgets.STANDARDSCROLL,
choices = matlist,
selected = matsel,
on_submit = self:callback('onToggleMatclass')
},
widgets.Label{
frame = { l = 0, b = 0, w = 29 },
text = {
{ key = 'LEAVESCREEN', text = ': Cancel, ',
on_activate = self:callback('dismiss') },
{ key = 'MENU_CONFIRM', key_sep = ': ',
text = function()
if self.is_existing then return 'Update' else return 'Create new' end
end,
on_activate = function()
self:dismiss()
if self.on_submit then
self.on_submit(self.constraint)
end
end },
}
},
}
end
function NewConstraint:postinit()
self:onChange()
end
function NewConstraint:onChange()
local token = workflow.constraintToToken(self.constraint)
local out = workflow.findConstraint(token)
if out then
self.constraint = out
self.is_existing = true
else
self.constraint.token = token
self.is_existing = false
end
end
function NewConstraint:chooseType()
guimat.ItemTypeDialog{
prompt = 'Please select a new item type',
hide_none = true,
on_select = function(itype,isub)
local cons = self.constraint
cons.item_type = itype
cons.item_subtype = isub
cons.is_craft = nil
self:onChange()
end
}:show()
end
function NewConstraint:chooseCrafts()
local cons = self.constraint
cons.item_type = -1
cons.item_subtype = -1
cons.is_craft = true
self:onChange()
end
function NewConstraint:chooseMaterial()
local cons = self.constraint
guimat.MaterialDialog{
prompt = 'Please select a new material',
none_caption = 'any material',
frame_width = 37,
on_select = function(mat_type, mat_index)
local cons = self.constraint
cons.mat_type = mat_type
cons.mat_index = mat_index
cons.mat_mask = nil
self:onChange()
end
}:show()
end
function NewConstraint:incQuality(diff)
local cons = self.constraint
local nq = (cons.min_quality or 0) + diff
if nq < 0 then
nq = df.item_quality.Masterful
elseif nq > df.item_quality.Masterful then
nq = 0
end
cons.min_quality = nq
self:onChange()
end
function NewConstraint:toggleLocal()
local cons = self.constraint
cons.is_local = not cons.is_local
self:onChange()
end
function NewConstraint:isMatSelected(token)
if self.constraint.mat_mask and self.constraint.mat_mask[token] then
return { ch = '\xfb', fg = COLOR_LIGHTGREEN }
else
return nil
end
end
function NewConstraint:onToggleMatclass(idx,obj)
local cons = self.constraint
if cons.mat_mask and cons.mat_mask[obj.text] then
cons.mat_mask[obj.text] = false
else
cons.mat_mask = cons.mat_mask or {}
cons.mat_mask[obj.text] = true
cons.mat_type = -1
cons.mat_index = -1
end
self:onChange()
end
function NewConstraint:onRangeChange()
local cons = self.constraint
local lim = math.max(cons.goal_value-5, math.floor(cons.goal_value/2))
cons.goal_gap = math.max(1, math.min(cons.goal_gap, lim))
end
JobConstraints = defclass(JobConstraints, guidm.MenuOverlay)
JobConstraints.focus_path = 'workflow-job'
JobConstraints.focus_path = 'workflow/job'
JobConstraints.ATTRS {
job = DEFAULT_NIL,
@ -53,8 +401,6 @@ JobConstraints.ATTRS {
frame_background = COLOR_BLACK,
}
local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false }
function JobConstraints:init(args)
self.building = dfhack.job.getHolder(self.job)
@ -71,40 +417,11 @@ function JobConstraints:init(args)
row_height = 4,
scroll_keys = widgets.SECONDSCROLL,
},
widgets.Label{
RangeEditor{
frame = { l = 0, b = 3 },
enabled = self:callback('isAnySelected'),
text = {
{ key = 'BUILDING_TRIGGER_ENABLE_CREATURE',
text = function()
local cons = self:getCurConstraint() or null_cons
if cons.goal_by_count then
return ': Count stacks '
else
return ': Count items '
end
end,
on_activate = self:callback('onChangeUnit') },
{ key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify',
on_activate = self:callback('onEditRange') },
NEWLINE, ' ',
{ key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN',
on_activate = self:callback('onIncRange', 'goal_gap', 5) },
{ key = 'BUILDING_TRIGGER_MIN_SIZE_UP',
on_activate = self:callback('onIncRange', 'goal_gap', -1) },
{ text = function()
local cons = self:getCurConstraint() or null_cons
return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap)
end },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
on_activate = self:callback('onIncRange', 'goal_value', -1) },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_UP',
on_activate = self:callback('onIncRange', 'goal_value', 5) },
{ text = function()
local cons = self:getCurConstraint() or null_cons
return string.format(': Max %-4d', cons.goal_value)
end },
}
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
},
widgets.Label{
frame = { l = 0, b = 0 },
@ -132,43 +449,6 @@ function JobConstraints:onGetSelectedJob()
return self.job
end
function describe_item_type(iobj)
local itemline = 'any item'
if iobj.is_craft then
itemline = 'any craft'
elseif iobj.item_type >= 0 then
itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type
local subtype = iobj.item_subtype or -1
local def = dfhack.items.getSubtypeDef(iobj.item_type, subtype)
local count = dfhack.items.getSubtypeCount(iobj.item_type, subtype)
if def then
itemline = def.name
elseif count >= 0 then
itemline = 'any '..itemline
end
end
return itemline
end
function is_caste_mat(iobj)
return dfhack.items.isCasteMaterial(iobj.item_type or -1)
end
function describe_material(iobj)
local matline = 'any material'
if is_caste_mat(iobj) then
matline = 'no material'
elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
if info then
matline = info:toString()
else
matline = iobj.mat_type..':'..iobj.mat_index
end
end
return matline
end
function JobConstraints:initListChoices(clist, sel_token)
clist = clist or workflow.listConstraints(self.job)
@ -247,45 +527,6 @@ function JobConstraints:saveConstraint(cons)
self:initListChoices(nil, out.token)
end
function JobConstraints:onChangeUnit()
local cons = self:getCurConstraint()
cons.goal_by_count = not cons.goal_by_count
self:saveConstraint(cons)
end
function JobConstraints:onEditRange()
local cons = self:getCurConstraint()
dlg.showInputPrompt(
'Input Range',
'Enter the new constraint range:',
COLOR_WHITE,
(cons.goal_value-cons.goal_gap)..'-'..cons.goal_value,
function(text)
local maxv = string.match(text, '^%s*(%d+)%s*$')
if maxv then
cons.goal_value = maxv
return self:saveConstraint(cons)
end
local minv,maxv = string.match(text, '^%s*(%d+)-(%d+)%s*$')
if minv and maxv and minv ~= maxv then
cons.goal_value = math.max(minv,maxv)
cons.goal_gap = math.abs(maxv-minv)
return self:saveConstraint(cons)
end
dlg.showMessage('Invalid Range', 'This range is invalid: '..text, COLOR_LIGHTRED)
end
)
end
function JobConstraints:onIncRange(field, delta)
local cons = self:getCurConstraint()
if not cons.goal_by_count then
delta = delta * 5
end
cons[field] = math.max(1, cons[field] + delta)
self:saveConstraint(cons)
end
function JobConstraints:onNewConstraint()
local outputs = workflow.listJobOutputs(self.job)
if #outputs == 0 then
@ -318,7 +559,10 @@ function JobConstraints:onNewConstraint()
COLOR_WHITE,
choices,
function(idx,item)
self:saveConstraint(item.obj)
NewConstraint{
constraint = item.obj,
on_submit = self:callback('saveConstraint')
}:show()
end
)
end