local _ENV = mkmodule('plugins.buildingplan.filterselection') local gui = require('gui') local pens = require('plugins.buildingplan.pens') local widgets = require('gui.widgets') local uibs = df.global.buildreq local to_pen = dfhack.pen.parse local function get_cur_filters() return dfhack.buildings.getFiltersByType({}, uibs.building_type, uibs.building_subtype, uibs.custom_type) end -------------------------------- -- Slider -- Slider = defclass(Slider, widgets.Widget) Slider.ATTRS{ num_stops=DEFAULT_NIL, get_left_idx_fn=DEFAULT_NIL, get_right_idx_fn=DEFAULT_NIL, on_left_change=DEFAULT_NIL, on_right_change=DEFAULT_NIL, } function Slider:preinit(init_table) init_table.frame = init_table.frame or {} init_table.frame.h = init_table.frame.h or 1 end function Slider:init() if self.num_stops < 2 then error('too few Slider stops') end self.is_dragging_target = nil -- 'left', 'right', or 'both' self.is_dragging_idx = nil -- offset from leftmost dragged tile end local function slider_get_width_per_idx(self) return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) end function Slider:onInput(keys) if not keys._MOUSE_L_DOWN then return false end local x = self:getMousePos() if not x then return false end local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() local width_per_idx = slider_get_width_per_idx(self) local left_pos = width_per_idx*(left_idx-1) local right_pos = width_per_idx*(right_idx-1) + 4 if x < left_pos then self.on_left_change(self.get_left_idx_fn() - 1) elseif x < left_pos+3 then self.is_dragging_target = 'left' self.is_dragging_idx = x - left_pos elseif x < right_pos then self.is_dragging_target = 'both' self.is_dragging_idx = x - left_pos elseif x < right_pos+3 then self.is_dragging_target = 'right' self.is_dragging_idx = x - right_pos else self.on_right_change(self.get_right_idx_fn() + 1) end return true end local function slider_do_drag(self, width_per_idx) local x = self.frame_body:localXY(dfhack.screen.getMousePos()) local cur_pos = x - self.is_dragging_idx cur_pos = math.max(0, cur_pos) cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) local offset = self.is_dragging_target == 'right' and -2 or 1 local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 local new_left_idx, new_right_idx if self.is_dragging_target == 'right' then new_right_idx = new_idx else new_left_idx = new_idx if self.is_dragging_target == 'both' then new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() if new_right_idx > self.num_stops then return end end end if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then self.on_left_change(new_left_idx) end if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then self.on_right_change(new_right_idx) end end local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} function Slider:onRenderBody(dc, rect) local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() local width_per_idx = slider_get_width_per_idx(self) -- draw track dc:seek(1,0) dc:char(nil, SLIDER_LEFT_END) dc:char(nil, SLIDER_TRACK) for stop_idx=1,self.num_stops-1 do local track_stop_pen = SLIDER_TRACK_STOP_SELECTED local track_pen = SLIDER_TRACK_SELECTED if left_idx > stop_idx or right_idx < stop_idx then track_stop_pen = SLIDER_TRACK_STOP track_pen = SLIDER_TRACK elseif right_idx == stop_idx then track_pen = SLIDER_TRACK end dc:char(nil, track_stop_pen) for i=2,width_per_idx do dc:char(nil, track_pen) end end if right_idx >= self.num_stops then dc:char(nil, SLIDER_TRACK_STOP_SELECTED) else dc:char(nil, SLIDER_TRACK_STOP) end dc:char(nil, SLIDER_TRACK) dc:char(nil, SLIDER_RIGHT_END) -- draw tabs dc:seek(width_per_idx*(left_idx-1)) dc:char(nil, SLIDER_TAB_LEFT) dc:char(nil, SLIDER_TAB_CENTER) dc:char(nil, SLIDER_TAB_RIGHT) dc:seek(width_per_idx*(right_idx-1)+4) dc:char(nil, SLIDER_TAB_LEFT) dc:char(nil, SLIDER_TAB_CENTER) dc:char(nil, SLIDER_TAB_RIGHT) -- manage dragging if self.is_dragging_target then slider_do_drag(self, width_per_idx) end if df.global.enabler.mouse_lbut == 0 then self.is_dragging_target = nil self.is_dragging_idx = nil end end -------------------------------- -- QualityAndMaterialsPage -- QualityAndMaterialsPage = defclass(QualityAndMaterialsPage, widgets.Panel) QualityAndMaterialsPage.ATTRS{ frame={t=0, l=0}, index=DEFAULT_NIL, desc=DEFAULT_NIL, } local TYPE_COL_WIDTH = 20 local HEADER_HEIGHT = 7 local QUALITY_HEIGHT = 9 local FOOTER_HEIGHT = 4 -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. local function can_be_improved(idx) local filter = get_cur_filters()[idx] if filter.flags2 and filter.flags2.building_material then return false; end return filter.item_type ~= df.item_type.WOOD and filter.item_type ~= df.item_type.BLOCKS and filter.item_type ~= df.item_type.BAR and filter.item_type ~= df.item_type.BOULDER end local function mat_sort_by_name(a, b) return a.name < b.name end local function mat_sort_by_quantity(a, b) return a.quantity > b.quantity or (a.quantity == b.quantity and mat_sort_by_name(a, b)) end function QualityAndMaterialsPage:init() self.dirty = true self.summary = '' local enable_item_quality = can_be_improved(self.index) self:addviews{ widgets.Panel{ view_id='header', frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, frame_inset={l=1}, subviews={ widgets.Label{ frame={l=0, t=0}, text='Current filter:', }, widgets.WrappedLabel{ frame={l=16, t=0, h=2, r=0}, text_pen=COLOR_LIGHTCYAN, text_to_wrap=function() return self.summary end, auto_height=false, }, widgets.CycleHotkeyLabel{ view_id='mat_sort', frame={l=0, t=3, w=21}, label='Sort by:', key='CUSTOM_SHIFT_R', options={ {label='name', value=mat_sort_by_name}, {label='available', value=mat_sort_by_quantity} }, on_change=function() self.dirty = true end, }, widgets.ToggleHotkeyLabel{ view_id='hide_zero', frame={l=0, t=4, w=24}, label='Hide unavailable:', key='CUSTOM_SHIFT_H', initial_option=false, on_change=function() self.dirty = true end, }, widgets.EditField{ view_id='search', frame={l=26, t=3}, label_text='Search: ', on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Label{ frame={l=1, b=0}, text='Type', text_pen=COLOR_LIGHTRED, }, widgets.Label{ frame={l=TYPE_COL_WIDTH, b=0}, text='Material', text_pen=COLOR_LIGHTRED, }, }, }, widgets.Panel{ view_id='materials_lists', frame={l=0, t=HEADER_HEIGHT, r=0, b=FOOTER_HEIGHT+QUALITY_HEIGHT}, frame_style=gui.INTERIOR_FRAME, subviews={ widgets.List{ view_id='materials_categories', frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, scroll_keys={}, icon_width=2, cursor_pen=COLOR_CYAN, on_submit=self:callback('toggle_category'), }, widgets.FilteredList{ view_id='materials_mats', frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, icon_width=2, on_submit=self:callback('toggle_material'), }, }, }, widgets.Panel{ view_id='divider', frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=FOOTER_HEIGHT+QUALITY_HEIGHT, w=1}, on_render=self:callback('draw_divider'), }, widgets.Panel{ view_id='quality_panel', frame={l=0, r=0, h=QUALITY_HEIGHT, b=FOOTER_HEIGHT}, frame_style=gui.INTERIOR_FRAME, frame_title='Item quality', subviews={ widgets.CycleHotkeyLabel{ view_id='decorated', frame={l=0, t=1, w=23}, key='CUSTOM_SHIFT_D', label='Decorated only:', options={ {label='No', value=false}, {label='Yes', value=true}, }, enabled=enable_item_quality, on_change=self:callback('set_decorated'), }, widgets.CycleHotkeyLabel{ view_id='min_quality', frame={l=0, t=3, w=18}, label='Min quality:', label_below=true, key_back='CUSTOM_SHIFT_Z', key='CUSTOM_SHIFT_X', options={ {label='Ordinary', value=0}, {label='Well Crafted', value=1}, {label='Finely Crafted', value=2}, {label='Superior', value=3}, {label='Exceptional', value=4}, {label='Masterful', value=5}, {label='Artifact', value=6}, }, enabled=enable_item_quality, on_change=function(val) self:set_min_quality(val+1) end, }, widgets.CycleHotkeyLabel{ view_id='max_quality', frame={r=1, t=3, w=18}, label='Max quality:', label_below=true, key_back='CUSTOM_SHIFT_Q', key='CUSTOM_SHIFT_W', options={ {label='Ordinary', value=0}, {label='Well Crafted', value=1}, {label='Finely Crafted', value=2}, {label='Superior', value=3}, {label='Exceptional', value=4}, {label='Masterful', value=5}, {label='Artifact', value=6}, }, enabled=enable_item_quality, on_change=function(val) self:set_max_quality(val+1) end, }, Slider{ frame={l=0, t=6}, num_stops=7, get_left_idx_fn=function() return self.subviews.min_quality:getOptionValue() + 1 end, get_right_idx_fn=function() return self.subviews.max_quality:getOptionValue() + 1 end, on_left_change=self:callback('set_min_quality'), on_right_change=self:callback('set_max_quality'), active=enable_item_quality, }, }, }, widgets.Panel{ view_id='footer', frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, frame_inset={t=1, l=1}, subviews={ widgets.HotkeyLabel{ frame={l=0, t=0}, label='Toggle', auto_width=true, key='SELECT', }, widgets.HotkeyLabel{ frame={l=0, t=2}, label='Done', auto_width=true, key='LEAVESCREEN', }, widgets.HotkeyLabel{ frame={l=30, t=0}, label='Invert selection', auto_width=true, key='CUSTOM_SHIFT_I', on_activate=self:callback('invert_materials'), }, widgets.HotkeyLabel{ frame={l=30, t=2}, label='Reset filter', auto_width=true, key='CUSTOM_SHIFT_X', on_activate=self:callback('clear_filter'), }, }, } } -- replace the FilteredList's built-in EditField with our own self.subviews.materials_mats.list.frame.t = 0 self.subviews.materials_mats.edit.visible = false self.subviews.materials_mats.edit = self.subviews.search self.subviews.search.on_change = self.subviews.materials_mats:callback('onFilterChange') end local MAT_ENABLED_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} local MAT_DISABLED_PEN = to_pen{ch='x', fg=COLOR_RED} local function make_cat_choice(label, cat, key, cats) local enabled = cats[cat] local icon = nil if not cats.unset then icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN end return { text=label, key=key, enabled=enabled, cat=cat, icon=icon, } end local function make_mat_choice(name, props, enabled, cats) local quantity = tonumber(props.count) local text = ('%5d - %s'):format(quantity, name) local icon = nil if not cats.unset then icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN end return { text=text, enabled=enabled, icon=icon, name=name, cat=props.category, quantity=quantity, } end function QualityAndMaterialsPage:refresh() local summary = self.desc local subviews = self.subviews local buildingplan = require('plugins.buildingplan') local heat = buildingplan.getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type) if heat >= 2 then summary = 'Magma safe ' .. summary elseif heat == 1 then summary = 'Fire safe ' .. summary else summary = 'Any ' .. summary end local specials = buildingplan.getSpecials(uibs.building_type, uibs.building_subtype, uibs.custom_type) if next(specials) then local specials_list = {} for special in pairs(specials) do table.insert(specials_list, special) end summary = summary .. ' [' .. table.concat(specials_list, ', ') .. ']' end local quality = buildingplan.getQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) subviews.decorated:setOption(quality.decorated ~= 0) subviews.min_quality:setOption(quality.min_quality) subviews.max_quality:setOption(quality.max_quality) local cats = buildingplan.getMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) local category_choices={ make_cat_choice('Stone', 'stone', 'CUSTOM_SHIFT_S', cats), make_cat_choice('Wood', 'wood', 'CUSTOM_SHIFT_O', cats), make_cat_choice('Metal', 'metal', 'CUSTOM_SHIFT_M', cats), make_cat_choice('Glass', 'glass', 'CUSTOM_SHIFT_G', cats), make_cat_choice('Clay', 'clay', 'CUSTOM_SHIFT_C', cats), } self.subviews.materials_categories:setChoices(category_choices) local mats = buildingplan.getMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) local mat_choices = {} local hide_zero = self.subviews.hide_zero:getOptionValue() local enabled_mat_names = {} for name,props in pairs(mats) do local enabled = props.enabled == 'true' and cats[props.category] if not cats.unset and enabled then table.insert(enabled_mat_names, name) end if not hide_zero or tonumber(props.count) > 0 then table.insert(mat_choices, make_mat_choice(name, props, enabled, cats)) end end table.sort(mat_choices, self.subviews.mat_sort:getOptionValue()) local prev_filter = self.subviews.search.text self.subviews.materials_mats:setChoices(mat_choices) self.subviews.materials_mats:setFilter(prev_filter) if #enabled_mat_names > 0 then table.sort(enabled_mat_names) summary = summary .. (' of %s'):format(table.concat(enabled_mat_names, ', ')) end self.summary = summary self.dirty = false self:updateLayout() end function QualityAndMaterialsPage:toggle_category(_, choice) local cats = {} if not choice.icon then -- toggling from unset to something is set table.insert(cats, choice.cat) else choice.enabled = not choice.enabled for _,c in ipairs(self.subviews.materials_categories:getChoices()) do if c.enabled then table.insert(cats, c.cat) end end end require('plugins.buildingplan').setMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, cats) self.dirty = true end function QualityAndMaterialsPage:toggle_material(_, choice) local mats = {} if not choice.icon then -- toggling from unset to something is set table.insert(mats, choice.name) else for _,c in ipairs(self.subviews.materials_mats:getChoices()) do local enabled = c.enabled if choice.name == c.name then enabled = not c.enabled end if enabled then table.insert(mats, c.name) end end end require('plugins.buildingplan').setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats) self.dirty = true end function QualityAndMaterialsPage:invert_materials() local mats = {} for _,c in ipairs(self.subviews.materials_mats:getChoices()) do if not c.icon then return end if not c.enabled then table.insert(mats, c.name) end end require('plugins.buildingplan').setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats) self.dirty = true end function QualityAndMaterialsPage:clear_filter() require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) self.dirty = true end function QualityAndMaterialsPage:set_decorated(decorated) local subviews = self.subviews require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, decorated and 1 or 0, subviews.min_quality:getOptionValue(), subviews.max_quality:getOptionValue()) self.dirty = true end function QualityAndMaterialsPage:set_min_quality(idx) idx = math.min(6, math.max(0, idx-1)) local subviews = self.subviews subviews.min_quality:setOption(idx) if subviews.max_quality:getOptionValue() < idx then subviews.max_quality:setOption(idx) end require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, subviews.decorated:getOptionValue() and 1 or 0, idx, subviews.max_quality:getOptionValue()) self.dirty = true end function QualityAndMaterialsPage:set_max_quality(idx) idx = math.min(6, math.max(0, idx-1)) local subviews = self.subviews subviews.max_quality:setOption(idx) if subviews.min_quality:getOptionValue() > idx then subviews.min_quality:setOption(idx) end require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, subviews.decorated:getOptionValue() and 1 or 0, subviews.min_quality:getOptionValue(), idx) self.dirty = true end function QualityAndMaterialsPage:draw_divider(dc) local y2 = dc.height - 1 for y=0,y2 do dc:seek(0, y) if y == 0 then dc:char(nil, pens.VERT_TOP_PEN) elseif y == y2 then dc:char(nil, pens.VERT_BOT_PEN) else dc:char(nil, pens.VERT_MID_PEN) end end end function QualityAndMaterialsPage:onRenderFrame(dc, rect) QualityAndMaterialsPage.super.onRenderFrame(self, dc, rect) if self.dirty then self:refresh() end end -------------------------------- -- GlobalSettingsPage -- GlobalSettingsPage = defclass(GlobalSettingsPage, widgets.ResizingPanel) GlobalSettingsPage.ATTRS{ autoarrange_subviews=true, frame={t=0, l=0}, frame_style=gui.INTERIOR_FRAME, } function GlobalSettingsPage:init() self:addviews{ widgets.WrappedLabel{ frame={l=0}, text_to_wrap='These options will affect the selection of "Generic Materials" for all future buildings.', }, widgets.Panel{ frame={h=1}, }, widgets.ToggleHotkeyLabel{ view_id='blocks', frame={l=0}, key='CUSTOM_B', label='Blocks', label_width=8, on_change=self:callback('update_setting', 'blocks'), }, widgets.ToggleHotkeyLabel{ view_id='logs', frame={l=0}, key='CUSTOM_L', label='Logs', label_width=8, on_change=self:callback('update_setting', 'logs'), }, widgets.ToggleHotkeyLabel{ view_id='boulders', frame={l=0}, key='CUSTOM_O', label='Boulders', label_width=8, on_change=self:callback('update_setting', 'boulders'), }, widgets.ToggleHotkeyLabel{ view_id='bars', frame={l=0}, key='CUSTOM_R', label='Bars', label_width=8, on_change=self:callback('update_setting', 'bars'), }, } self:init_settings() end function GlobalSettingsPage:init_settings() local settings = require('plugins.buildingplan').getGlobalSettings() local subviews = self.subviews subviews.blocks:setOption(settings.blocks) subviews.logs:setOption(settings.logs) subviews.boulders:setOption(settings.boulders) subviews.bars:setOption(settings.bars) end function GlobalSettingsPage:update_setting(setting, val) dfhack.run_command('buildingplan', 'set', setting, tostring(val)) self:init_settings() end -------------------------------- -- FilterSelection -- FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', frame={w=55, h=53, l=30, t=8}, frame_inset={t=1}, resizable=true, index=DEFAULT_NIL, desc=DEFAULT_NIL, autoarrange_subviews=true, } function FilterSelection:init() self:addviews{ widgets.TabBar{ frame={t=0}, labels={ 'Quality and materials', 'Global settings', }, on_select=function(idx) self.subviews.pages:setSelected(idx) self:updateLayout() end, get_cur_page=function() return self.subviews.pages:getSelected() end, key='CUSTOM_CTRL_T', }, widgets.Widget{ frame={h=1}, }, widgets.Pages{ view_id='pages', frame={t=5, l=0, b=0, r=0}, subviews={ QualityAndMaterialsPage{ index=self.index, desc=self.desc }, GlobalSettingsPage{}, }, }, } end FilterSelectionScreen = defclass(FilterSelectionScreen, gui.ZScreen) FilterSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', pass_movement_keys=true, pass_mouse_clicks=false, defocusable=false, index=DEFAULT_NIL, desc=DEFAULT_NIL, } function FilterSelectionScreen:init() self:addviews{ FilterSelection{ index=self.index, desc=self.desc } } end function FilterSelectionScreen:onShow() -- don't let the building "shadow" follow the mouse cursor while this screen is open df.global.game.main_interface.bottom_mode_selected = -1 end function FilterSelectionScreen:onDismiss() -- re-enable building shadow df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT end return _ENV