-- Show and modify properties of jobs in a workshop. --[[=begin gui/workshop-job ================ Bind to a key (the example config uses :kbd:`Alt`:kbd:`a`), and activate with a job selected in a workshop in the :kbd:`q` mode. .. image:: /docs/images/workshop-job.png The script shows a list of the input reagents of the selected job, and allows changing them like the `job` ``item-type`` and `job` ``item-material`` commands. Specifically, pressing the :kbd:`i` key pops up a dialog that lets you select an item type from a list. .. image:: /docs/images/workshop-job-item.png Pressing :kbd:`m`, unless the item type does not allow a material, lets you choose a material. .. image:: /docs/images/workshop-job-material.png Since there are a lot more materials than item types, this dialog is more complex and uses a hierarchy of sub-menus. List choices that open a sub-menu are marked with an arrow on the left. .. warning:: Due to the way input reagent matching works in DF, you must select an item type if you select a material, or the material will be matched incorrectly in some cases. If you press :kbd:`m` without choosing an item type, the script will auto-choose if there is only one valid choice, or pop up an error message box instead of the material selection dialog. Note that both materials and item types presented in the dialogs are filtered by the job input flags, and even the selected item type for material selection, or material for item type selection. Many jobs would let you select only one input item type. For example, if you choose a *plant* input item type for your prepare meal job, it will only let you select cookable materials. If you choose a *barrel* item instead (meaning things stored in barrels, like drink or milk), it will let you select any material, since in this case the material is matched against the barrel itself. Then, if you select, say, iron, and then try to change the input item type, now it won't let you select *plant*; you have to unset the material first. =end]] local utils = require 'utils' local gui = require 'gui' local guidm = require 'gui.dwarfmode' local guimat = require 'gui.materials' local widgets = require 'gui.widgets' local dlg = require 'gui.dialogs' JobDetails = defclass(JobDetails, guidm.MenuOverlay) JobDetails.focus_path = 'workshop-job' JobDetails.ATTRS { job = DEFAULT_NIL, frame_inset = 1, frame_background = COLOR_BLACK, } function JobDetails:init(args) self.building = dfhack.job.getHolder(self.job) local status = { text = 'No worker', pen = COLOR_DARKGREY } local worker = dfhack.job.getWorker(self.job) if self.job.flags.suspend then status = { text = 'Suspended', pen = COLOR_RED } elseif worker then status = { text = dfhack.TranslateName(dfhack.units.getVisibleName(worker)), pen = COLOR_GREEN } end self:addviews{ widgets.Label{ frame = { l = 0, t = 0 }, text = { { text = df.job_type.attrs[self.job.job_type].caption }, NEWLINE, NEWLINE, ' ', status } }, widgets.Label{ frame = { l = 0, t = 4 }, text = { { key = 'CUSTOM_I', text = ': Input item, ', enabled = self:callback('canChangeIType'), on_activate = self:callback('onChangeIType') }, { key = 'CUSTOM_M', text = ': Material', enabled = self:callback('canChangeMat'), on_activate = self:callback('onChangeMat') } } }, widgets.List{ view_id = 'list', frame = { t = 6, b = 2 }, row_height = 4, scroll_keys = widgets.SECONDSCROLL, }, widgets.Label{ frame = { l = 0, b = 0 }, text = { { key = 'LEAVESCREEN', text = ': Back', on_activate = self:callback('dismiss') } } }, } self:initListChoices() end function JobDetails:onGetSelectedBuilding() return self.building end function JobDetails:onGetSelectedJob() return self.job end function describe_item_type(iobj) local itemline = 'any item' if iobj.item_type >= 0 then itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type local def = dfhack.items.getSubtypeDef(iobj.item_type, iobj.item_subtype) local count = dfhack.items.getSubtypeCount(iobj.item_type, iobj.item_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) end function describe_material(iobj) local matline = 'any material' if is_caste_mat(iobj) then matline = 'material not applicable' elseif iobj.mat_type >= 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 list_flags(list, bitfield) for name,val in pairs(bitfield) do if val then table.insert(list, name) end end end function JobDetails:initListChoices() local items = {} for i,ref in ipairs(self.job.items) do local idx = ref.job_item_idx if idx >= 0 then items[idx] = (items[idx] or 0) + 1 end end local choices = {} for i,iobj in ipairs(self.job.job_items) do local head = 'Item '..(i+1)..': '..(items[i] or 0)..' of '..iobj.quantity if iobj.min_dimension > 0 then head = head .. '(size '..iobj.min_dimension..')' end local line1 = {} local reaction = df.reaction.find(iobj.reaction_id) if reaction and #iobj.contains > 0 then for _,ri in ipairs(iobj.contains) do table.insert(line1, 'has '..utils.call_with_string( reaction.reagents[ri],'getDescription',iobj.reaction_id )) end end if iobj.metal_ore >= 0 then local ore = dfhack.matinfo.decode(0, iobj.metal_ore) if ore then table.insert(line1, 'ore of '..ore:toString()) end end if iobj.has_material_reaction_product ~= '' then table.insert(line1, 'product '..iobj.has_material_reaction_product) end if iobj.reaction_class ~= '' then table.insert(line1, 'class '..iobj.reaction_class) end if iobj.has_tool_use >= 0 then table.insert(line1, 'has use '..df.tool_uses[iobj.has_tool_use]) end list_flags(line1, iobj.flags1) list_flags(line1, iobj.flags2) list_flags(line1, iobj.flags3) if #line1 == 0 then table.insert(line1, 'no flags') end table.insert(choices, { index = i, iobj = iobj, text = { head, NEWLINE, ' ', { text = curry(describe_item_type, iobj) }, NEWLINE, ' ', { text = curry(describe_material, iobj) }, NEWLINE, ' ', table.concat(line1, ', '), NEWLINE } }) end self.subviews.list:setChoices(choices) end function JobDetails:canChangeIType() local idx, obj = self.subviews.list:getSelected() return obj ~= nil end function JobDetails:setItemType(obj, item_type, item_subtype) obj.iobj.item_type = item_type obj.iobj.item_subtype = item_subtype if is_caste_mat(obj.iobj) then self:setMaterial(obj, -1, -1) end end function JobDetails:onChangeIType() local idx, obj = self.subviews.list:getSelected() guimat.ItemTypeDialog{ prompt = 'Please select a new item type for input '..idx, none_caption = 'any item', item_filter = curry(dfhack.job.isSuitableItem, obj.iobj), on_select = self:callback('setItemType', obj) }:show() end function JobDetails:canChangeMat() local idx, obj = self.subviews.list:getSelected() return obj ~= nil and not is_caste_mat(obj.iobj) end function JobDetails:setMaterial(obj, mat_type, mat_index) if obj.index == 0 and self.job.mat_type == obj.iobj.mat_type and self.job.mat_index == obj.iobj.mat_index and self.job.job_type ~= df.job_type.PrepareMeal then self.job.mat_type = mat_type self.job.mat_index = mat_index end obj.iobj.mat_type = mat_type obj.iobj.mat_index = mat_index end function JobDetails:findUnambiguousItem(iobj) local count = 0 local itype for i = 0,df.item_type._last_item do if dfhack.job.isSuitableItem(iobj, i, -1) then count = count + 1 if count > 1 then return nil end itype = i end end return itype end function JobDetails:onChangeMat() local idx, obj = self.subviews.list:getSelected() if obj.iobj.item_type == -1 and obj.iobj.mat_type == -1 then -- If the job allows only one specific item type, use it local vitype = self:findUnambiguousItem(obj.iobj) if vitype then obj.iobj.item_type = vitype else dlg.showMessage( 'Bug Alert', { 'Please set a specific item type first.\n\n', 'Otherwise the material will be matched\n', 'incorrectly due to a limitation in DF code.' }, COLOR_YELLOW ) return end end guimat.MaterialDialog{ prompt = 'Please select a new material for input '..idx, none_caption = 'any material', mat_filter = function(mat,parent,mat_type,mat_index) return dfhack.job.isSuitableMaterial(obj.iobj, mat_type, mat_index) end, on_select = self:callback('setMaterial', obj) }:show() end function JobDetails:onInput(keys) if self:propagateMoveKeys(keys) then if df.global.world.selected_building ~= self.building then self:dismiss() end else JobDetails.super.onInput(self, keys) end end 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") end local dlg = JobDetails{ job = dfhack.gui.getSelectedJob() } dlg:show()