diff --git a/docs/changelog.txt b/docs/changelog.txt index 7010bdd31..4a3428031 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -50,6 +50,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``dfhack.job.attachJobItem()``: allows you to attach specific items to a job - ``dfhack.screen.paintTile()``: you can now explicitly clear the interface cursor from a map tile by passing ``0`` as the tile value - ``widgets.Label``: token ``tile`` properties can now be functions that return a value +- ``widgets.CycleHotkeyLabel``: add ``label_below`` attribute for compact 2-line output -@ ``widgets.FilteredList``: search key matching is now case insensitive by default -@ ``gui.INTERIOR_FRAME``: a panel frame style for use in highlighting off interior areas of a UI diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index f9aafe4e0..9f2386660 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4896,6 +4896,8 @@ It has the following attributes: hotkey. :label_width: The number of spaces to allocate to the ``label`` (for use in aligning a column of ``CycleHotkeyLabel`` labels). +:label_below: If ``true``, then the option value will apear below the label + instead of to the right of it. Defaults to ``false``. :options: A list of strings or tables of ``{label=string, value=string[, pen=pen]}``. String options use the same string for the label and value and the default pen. The optional ``pen`` diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index ab018a50a..b86e31710 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1490,6 +1490,7 @@ CycleHotkeyLabel.ATTRS{ key_back=DEFAULT_NIL, label=DEFAULT_NIL, label_width=DEFAULT_NIL, + label_below=false, options=DEFAULT_NIL, initial_option=1, on_change=DEFAULT_NIL, @@ -1498,12 +1499,17 @@ CycleHotkeyLabel.ATTRS{ function CycleHotkeyLabel:init() self:setOption(self.initial_option) + local val_gap = 1 + if self.label_below then + val_gap = 0 + (self.key_back and 1 or 0) + (self.key and 3 or 0) + end + self:setText{ self.key_back ~= nil and {key=self.key_back, key_sep='', width=0, on_activate=self:callback('cycle', true)} or {}, {key=self.key, key_sep=': ', text=self.label, width=self.label_width, on_activate=self:callback('cycle')}, - ' ', - {text=self:callback('getOptionLabel'), + self.label_below and NEWLINE or '', + {gap=val_gap, text=self:callback('getOptionLabel'), pen=self:callback('getOptionPen')}, } end @@ -1580,7 +1586,7 @@ end function CycleHotkeyLabel:onInput(keys) if CycleHotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN and self:getMousePos() then + elseif keys._MOUSE_L_DOWN and self:getMousePos() and not is_disabled(self) then self:cycle() return true end diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 4fa119ebc..7479d348d 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -52,6 +52,7 @@ void set_config_bool(PersistentDataItem &c, int index, bool value) { static PersistentDataItem config; // for use in counting available materials for the UI +static vector mat_cache; static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; static unordered_map cur_item_filters; @@ -142,6 +143,47 @@ static const vector & get_job_items(color_ostream &out, Bu return jitems; } +static void cache_matched(int16_t type, int32_t index) { + static const df::dfhack_material_category building_material_categories( + df::dfhack_material_category::mask_glass | + df::dfhack_material_category::mask_metal | + df::dfhack_material_category::mask_soap | + df::dfhack_material_category::mask_stone | + df::dfhack_material_category::mask_wood + ); + + MaterialInfo mi; + mi.decode(type, index); + if (mi.matches(building_material_categories)) { + DEBUG(status).print("cached material: %s\n", mi.toString().c_str()); + mat_cache.emplace_back(mi); + } + else + TRACE(status).print("not matched: %s\n", mi.toString().c_str()); +} + +static void load_material_cache() { + df::world_raws &raws = world->raws; + for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) + if (raws.mat_table.builtin[i]) + cache_matched(i, -1); + + for (size_t i = 0; i < raws.inorganics.size(); i++) + cache_matched(0, i); + + for (size_t i = 0; i < raws.plants.all.size(); i++) { + df::plant_raw *p = raws.plants.all[i]; + if (p->material.size() <= 1) + continue; + for (size_t j = 0; j < p->material.size(); j++) { + if (p->material[j]->id == "WOOD") { + cache_matched(DFHack::MaterialInfo::PLANT_BASE+j, i); + break; + } + } + } +} + static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { if (cur_heat_safety.count(key)) return cur_heat_safety.at(key); @@ -221,6 +263,7 @@ static void clear_state(color_ostream &out) { } } job_item_cache.clear(); + mat_cache.clear(); } DFhackCExport command_result plugin_load_data (color_ostream &out) { @@ -236,6 +279,8 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(status,out).print("loading persisted state\n"); clear_state(out); + load_material_cache(); + vector filter_configs; World::GetPersistentData(&filter_configs, FILTER_CONFIG_KEY); for (auto &cfg : filter_configs) { @@ -250,7 +295,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { PlannedBuilding pb(out, building_configs[idx]); df::building *bld = df::building::find(pb.id); if (!bld) { - INFO(status).print("building %d no longer exists; skipping\n", pb.id); + INFO(status,out).print("building %d no longer exists; skipping\n", pb.id); pb.remove(out); continue; } @@ -323,9 +368,34 @@ static command_result do_command(color_ostream &out, vector ¶meters) // core will already be suspended when coming in through here // -static string getBucket(const df::job_item & ji) { +static string getBucket(const df::job_item & ji, const PlannedBuilding & pb, int idx) { + if (idx < 0 || (size_t)idx < pb.item_filters.size()) + return "INVALID"; + std::ostringstream ser; + // put elements in front that significantly affect the difficulty of matching + // the filter. ensure the lexicographically "less" value is the pickier value. + const ItemFilter & item_filter = pb.item_filters[idx]; + + if (item_filter.getDecoratedOnly()) + ser << "Da"; + else + ser << "Db"; + + if (ji.flags2.bits.magma_safe || pb.heat_safety == HEAT_SAFETY_MAGMA) + ser << "Ha"; + else if (ji.flags2.bits.fire_safe || pb.heat_safety == HEAT_SAFETY_FIRE) + ser << "Hb"; + else + ser << "Hc"; + + size_t num_materials = item_filter.getMaterials().size(); + if (num_materials == 0 || num_materials >= 9 || item_filter.getMaterialMask().whole) + ser << "M9"; + else + ser << "M" << num_materials; + // pull out and serialize only known relevant fields. if we miss a few, then // the filter bucket will be slighly less specific than it could be, but // that's probably ok. we'll just end up bucketing slightly different items @@ -337,6 +407,8 @@ static string getBucket(const df::job_item & ji) { << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' << ji.metal_ore << ':' << ji.has_tool_use; + ser << ':' << item_filter.serialize(); + return ser.str(); } @@ -394,7 +466,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { int32_t id = bld->id; for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item); + auto bucket = getBucket(*job_item, pb, job_item_idx); // if there are multiple vector_ids, schedule duplicate tasks. after // the correct number of items are matched, the extras will get popped @@ -576,6 +648,20 @@ static int getAvailableItems(lua_State *L) { return 1; } +static int getGlobalSettings(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering getGlobalSettings\n"); + map settings; + settings.emplace("blocks", get_config_bool(config, CONFIG_BLOCKS)); + settings.emplace("logs", get_config_bool(config, CONFIG_LOGS)); + settings.emplace("boulders", get_config_bool(config, CONFIG_BOULDERS)); + settings.emplace("bars", get_config_bool(config, CONFIG_BARS)); + Lua::Push(L, settings); + return 1; +} + static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(status,out).print( "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", @@ -588,12 +674,22 @@ static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtyp BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); for (auto &filter : filters.getItemFilters()) { - if (filter.isEmpty()) + if (!filter.isEmpty()) return true; } return false; } +static void clearFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + TRACE(status,out).print("entering clearFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key); + if (index < 0 || filters.getItemFilters().size() <= (size_t)index) + return; + filters.setItemFilter(out, ItemFilter(), index); + call_buildingplan_lua(&out, "signal_reset"); +} + static void setMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, string filter) { DEBUG(status,out).print("entering setMaterialFilter\n"); call_buildingplan_lua(&out, "signal_reset"); @@ -610,8 +706,8 @@ static int getMaterialFilter(lua_State *L) { DEBUG(status,*out).print( "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); - vector filter; - Lua::PushVector(L, filter); + map counts_per_material; + Lua::Push(L, counts_per_material); return 1; } @@ -641,6 +737,45 @@ static int getHeatSafetyFilter(lua_State *L) { return 1; } +static void setQualityFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, + int decorated, int min_quality, int max_quality) { + DEBUG(status,out).print("entering setQualityFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key).getItemFilters(); + if (index < 0 || filters.size() <= (size_t)index) + return; + ItemFilter filter = filters[index]; + filter.setDecoratedOnly(decorated != 0); + filter.setMinQuality(min_quality); + filter.setMaxQuality(max_quality); + get_item_filters(out, key).setItemFilter(out, filter, index); + call_buildingplan_lua(&out, "signal_reset"); +} + +static int getQualityFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getQualityFilter building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(*out, key).getItemFilters(); + if (index < 0 || filters.size() <= (size_t)index) + return 0; + auto &filter = filters[index]; + map ret; + ret.emplace("decorated", filter.getDecoratedOnly()); + ret.emplace("min_quality", filter.getMinQuality()); + ret.emplace("max_quality", filter.getMaxQuality()); + Lua::Push(L, ret); + return 1; +} + static bool validate_pb(color_ostream &out, df::building *bld, int index) { if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) return false; @@ -682,7 +817,7 @@ static int getQueuePosition(color_ostream &out, df::building *bld, int index) { if (!tasks.count(vec_id)) continue; auto &buckets = tasks.at(vec_id); - string bucket_id = getBucket(*job_item); + string bucket_id = getBucket(*job_item, pb, index); if (!buckets.count(bucket_id)) continue; int bucket_pos = -1; @@ -711,7 +846,7 @@ static void makeTopPriority(color_ostream &out, df::building *bld) { if (!tasks.count(vec_id)) continue; auto &buckets = tasks.at(vec_id); - string bucket_id = getBucket(*job_items[index]); + string bucket_id = getBucket(*job_items[index], pb, index); if (!buckets.count(bucket_id)) continue; auto &bucket = buckets.at(bucket_id); @@ -738,8 +873,10 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_FUNCTION(hasFilter), + DFHACK_LUA_FUNCTION(clearFilter), DFHACK_LUA_FUNCTION(setMaterialFilter), DFHACK_LUA_FUNCTION(setHeatSafetyFilter), + DFHACK_LUA_FUNCTION(setQualityFilter), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_FUNCTION(makeTopPriority), @@ -747,8 +884,10 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { }; DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getGlobalSettings), DFHACK_LUA_COMMAND(getAvailableItems), DFHACK_LUA_COMMAND(getMaterialFilter), DFHACK_LUA_COMMAND(getHeatSafetyFilter), + DFHACK_LUA_COMMAND(getQualityFilter), DFHACK_LUA_END }; diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 655dc8c1a..f401c90a8 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -5,11 +5,13 @@ #include "modules/Items.h" #include "modules/Job.h" +#include "modules/Maps.h" #include "modules/Materials.h" #include "df/building_design.h" #include "df/item.h" #include "df/job.h" +#include "df/map_block.h" #include "df/world.h" #include @@ -151,6 +153,22 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, return NULL; } +// This is tricky. we want to choose an item that can be brought to the job site, but that's not +// necessarily the same as job->pos. it could be many tiles off in any direction (e.g. for bridges), or +// up or down (e.g. for stairs). For now, just return if the item is on a walkable tile. +static bool isAccessibleFrom(color_ostream &out, df::item *item, df::job *job) { + df::coord item_pos = Items::getPosition(item); + df::map_block *block = Maps::getTileBlock(item_pos); + bool is_walkable = false; + if (block) { + uint16_t walkability_group = index_tile(block->walkable, item_pos); + is_walkable = walkability_group != 0; + TRACE(cycle,out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "" : "not "); + } + return is_walkable; +} + static void doVector(color_ostream &out, df::job_item_vector_id vector_id, map &buckets, unordered_map &planned_buildings) { @@ -182,9 +200,10 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto job = bld->jobs[0]; auto filter_idx = task.second; auto &pb = planned_buildings.at(id); - if (matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, + if (isAccessibleFrom(out, item, job) + && matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, pb.item_filters[filter_idx]) - && Job::attachJobItem(job, item, + && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { MaterialInfo material; @@ -235,6 +254,7 @@ struct VectorsToScanLast { vectors.push_back(df::job_item_vector_id::BOULDER); vectors.push_back(df::job_item_vector_id::WOOD); vectors.push_back(df::job_item_vector_id::BAR); + vectors.push_back(df::job_item_vector_id::IN_PLAY); } }; @@ -248,7 +268,7 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, for (auto it = tasks.begin(); it != tasks.end(); ) { auto vector_id = it->first; - // we could make this a set, but it's only three elements + // we could make this a set, but it's only a few elements if (std::find(vectors_to_scan_last.vectors.begin(), vectors_to_scan_last.vectors.end(), vector_id) != vectors_to_scan_last.vectors.end()) { diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index a714b62d4..86c9c1378 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -131,37 +131,6 @@ void ItemFilter::setMaterials(const vector &materials) { this->materials = materials; } -string ItemFilter::getMinQuality() const { - return ENUM_KEY_STR(item_quality, min_quality); -} - -string ItemFilter::getMaxQuality() const { - return ENUM_KEY_STR(item_quality, max_quality); -} - -bool ItemFilter::getDecoratedOnly() const { - return decorated_only; -} - -uint32_t ItemFilter::getMaterialMask() const { - return mat_mask.whole; -} - -static string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } - -vector ItemFilter::getMaterials() const { - vector descriptions; - transform_(materials, descriptions, material_to_string_fn); - - if (descriptions.size() == 0) - bitfield_to_string(&descriptions, mat_mask); - - if (descriptions.size() == 0) - descriptions.push_back("any"); - - return descriptions; -} - static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) { return mat_mask.whole ? mat.matches(mat_mask) : true; } diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 6eb7551b4..29eb7226c 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -20,11 +20,11 @@ public: void setMaterialMask(uint32_t mask); void setMaterials(const std::vector &materials); - std::string getMinQuality() const; - std::string getMaxQuality() const; - bool getDecoratedOnly() const; - uint32_t getMaterialMask() const; - std::vector getMaterials() const; + df::item_quality getMinQuality() const { return min_quality; } + df::item_quality getMaxQuality() const {return max_quality; } + bool getDecoratedOnly() const { return decorated_only; } + df::dfhack_material_category getMaterialMask() const { return mat_mask; } + std::vector getMaterials() const { return materials; } bool matches(df::dfhack_material_category mask) const; bool matches(DFHack::MaterialInfo &material) const; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 0e1341a3b..413dd545a 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -310,7 +310,7 @@ end function ItemSelection:get_choices(sort_fn) local item_ids = getAvailableItems(uibs.building_type, - uibs.building_subtype, uibs.custom_type, self.index - 1) + uibs.building_subtype, uibs.custom_type, self.index-1) local buckets = {} for _,item_id in ipairs(item_ids) do local item = df.item.find(item_id) @@ -475,9 +475,157 @@ function ItemSelectionScreen:init() end -------------------------------- --- FilterSelection +-- 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, +} + +local TYPE_COL_WIDTH = 20 +local HEADER_HEIGHT = 8 +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) @@ -491,236 +639,184 @@ local function can_be_improved(idx) filter.item_type ~= df.item_type.BOULDER end -local OPTIONS_COL_WIDTH = 28 -local TYPE_COL_WIDTH = 20 -local HEADER_HEIGHT = 5 -local FOOTER_HEIGHT = 4 - -FilterSelection = defclass(FilterSelection, widgets.Window) -FilterSelection.ATTRS{ - frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]', - frame={w=80, h=53, l=30, t=8}, - resizable=true, - index=DEFAULT_NIL, -} +function QualityAndMaterialsPage:init() + self.lowest_other_item_heat_safety = 2 + self.dirty = true -local STANDIN_PEN = to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '} + local enable_item_quality = can_be_improved(self.index) -function FilterSelection:init() self:addviews{ widgets.Panel{ - view_id='options_panel', - frame={l=0, t=0, b=FOOTER_HEIGHT, w=OPTIONS_COL_WIDTH}, - autoarrange_subviews=true, + view_id='header', + frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, + frame_inset={l=1}, subviews={ - widgets.Panel{ - view_id='quality_panel', - frame={l=0, r=0, h=24}, - frame_inset={t=1}, - frame_style=gui.INTERIOR_FRAME, - frame_title='Item quality', - subviews={ - widgets.HotkeyLabel{ - frame={l=0, t=0}, - key='CUSTOM_SHIFT_Q', - }, - widgets.HotkeyLabel{ - frame={l=1, t=0}, - key='CUSTOM_SHIFT_W', - label='Set max quality', - }, - widgets.Panel{ - view_id='quality_slider', - frame={l=0, t=2, w=3, h=15}, - frame_background=STANDIN_PEN, - }, - widgets.Label{ - frame={l=3, t=3}, - text='- Artifact (1)', - }, - widgets.Label{ - frame={l=3, t=5}, - text='- Masterful (3)', - }, - widgets.Label{ - frame={l=3, t=7}, - text='- Exceptional (34)', - }, - widgets.Label{ - frame={l=3, t=9}, - text='- Superior (50)', - }, - widgets.Label{ - frame={l=3, t=11}, - text='- FinelyCrafted (67)', - }, - widgets.Label{ - frame={l=3, t=13}, - text='- WellCrafted (79)', - }, - widgets.Label{ - frame={l=3, t=15}, - text='- Ordinary (206)', - }, - widgets.HotkeyLabel{ - frame={l=0, t=18}, - key='CUSTOM_SHIFT_Z', - }, - widgets.HotkeyLabel{ - frame={l=1, t=18}, - key='CUSTOM_SHIFT_X', - label='Set min quality', - }, - widgets.CycleHotkeyLabel{ - frame={l=0, t=20}, - key='CUSTOM_SHIFT_D', - label='Decorated only:', - options={'No', 'Yes'}, - }, + widgets.Label{ + frame={l=0, t=0, h=1, r=0}, + text={ + 'Current filter:', + {gap=1, pen=COLOR_LIGHTCYAN, text=self:callback('get_summary')} }, }, - widgets.ResizingPanel{ - view_id='building_panel', - frame={l=0, r=0}, - frame_inset={t=1}, - frame_style=gui.INTERIOR_FRAME, - frame_title='Building options', - autoarrange_subviews=true, - autoarrange_gap=1, - subviews={ - widgets.WrappedLabel{ - frame={l=0}, - text_to_wrap='These options will affect all items for the current building type.', - }, - widgets.CycleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_G', - label='Building safety:', - options={ - {label='Any', value=0}, - {label='Magma', value=2, pen=COLOR_RED}, - {label='Fire', value=1, pen=COLOR_LIGHTRED}, - }, - }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={t=2, l=0, w=35}, + key='CUSTOM_SHIFT_G', + label='Building heat safety:', + options={ + {label='Fire Magma', value=0, pen=COLOR_GREY}, + {label='Fire Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, + on_change=self:callback('set_heat_safety'), + }, + widgets.Label{ + frame={t=2, l=30}, + text='Magma', + auto_width=true, + text_pen=COLOR_GREY, + visible=function() return self.subviews.safety:getOptionValue() == 1 end, + }, + widgets.Label{ + frame={t=3, l=3}, + text='Other items for this building may not be able to use all of their selected materials.', + visible=function() return self.subviews.safety:getOptionValue() > self.lowest_other_item_heat_safety end, + }, + widgets.EditField{ + frame={l=0, t=4, w=23}, + label_text='Search: ', + on_char=function(ch) return ch:match('%l') end, + }, + widgets.CycleHotkeyLabel{ + frame={l=24, t=4, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_R', + options={'name', 'available'}, }, - widgets.Panel{ - view_id='global_panel', - frame={l=0, r=0, b=0}, - frame_inset={t=1}, - frame_style=gui.INTERIOR_FRAME, - frame_title='Global options', - autoarrange_subviews=true, - subviews={ - widgets.WrappedLabel{ - frame={l=0}, - text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.', - }, - widgets.Panel{ - frame={h=1}, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_B', - label='Blocks', - label_width=8, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_L', - label='Logs', - label_width=8, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_O', - label='Boulders', - label_width=8, - }, - widgets.ToggleHotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_P', - label='Bars', - label_width=8, - }, + widgets.ToggleHotkeyLabel{ + frame={l=24, t=5, w=24}, + label='Hide unavailable:', + key='CUSTOM_SHIFT_H', + initial_option=false, + }, + 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={}, + choices={ + {text='Stone', key='CUSTOM_SHIFT_S'}, + {text='Wood', key='CUSTOM_SHIFT_O'}, + {text='Metal', key='CUSTOM_SHIFT_M'}, + {text='Other', key='CUSTOM_SHIFT_T'}, + }, + }, + widgets.List{ + view_id='materials_mats', + frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, + choices={ + {text='9 - granite'}, + {text='0 - graphite'}, }, }, }, }, widgets.Panel{ - view_id='materials_panel', - frame={l=OPTIONS_COL_WIDTH, t=0, b=FOOTER_HEIGHT, r=0}, + 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.Panel{ - view_id='header', - frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, - subviews={ - widgets.EditField{ - frame={l=1, t=0}, - label_text='Search: ', - on_char=function(ch) return ch:match('%l') end, - }, - widgets.CycleHotkeyLabel{ - frame={l=1, t=2, w=21}, - label='Sort by:', - key='CUSTOM_SHIFT_R', - options={'name', 'available'}, - }, - widgets.ToggleHotkeyLabel{ - frame={l=24, t=2, w=24}, - label='Hide unavailable:', - key='CUSTOM_SHIFT_H', - initial_option=false, - }, - 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.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.Panel{ - view_id='materials_lists', - frame={l=0, t=HEADER_HEIGHT, r=0, b=0}, - 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={}, - choices={ - {text='Stone', key='CUSTOM_SHIFT_S'}, - {text='Wood', key='CUSTOM_SHIFT_W'}, - {text='Metal', key='CUSTOM_SHIFT_M'}, - {text='Other', key='CUSTOM_SHIFT_O'}, - }, - }, - widgets.List{ - view_id='materials_mats', - frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, - choices={ - {text='9 - granite'}, - {text='0 - graphite'}, - }, - }, + 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='divider', - frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=0, w=1}, - on_render=self:callback('draw_divider'), - } }, }, widgets.Panel{ view_id='footer', frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, - frame_inset={l=20, t=1}, + frame_inset={t=1, l=1}, subviews={ widgets.HotkeyLabel{ frame={l=0, t=0}, @@ -757,6 +853,68 @@ function FilterSelection:init() } end +function QualityAndMaterialsPage:refresh() + local summary = '' + local subviews = self.subviews + + local heat = getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type) + subviews.safety:setOption(heat) + if heat >= 2 then summary = summary .. 'Magma safe ' + elseif heat == 1 then summary = summary .. 'Fire safe ' + end + + local quality = 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 materials = getMaterialFilter(ibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) + + self.summary = summary + self.dirty = false +end + +function QualityAndMaterialsPage:get_summary() + -- TODO: summarize materials + return self.summary +end + +function QualityAndMaterialsPage:set_heat_safety(heat) + setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + self.dirty = true +end + +function QualityAndMaterialsPage:set_decorated(decorated) + local subviews = self.subviews + 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 + 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 + 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 + local texpos = dfhack.textures.getThinBordersTexposStart() local tp = function(offset) if texpos == -1 then return nil end @@ -767,7 +925,7 @@ local TOP_PEN = to_pen{tile=tp(10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} local MID_PEN = to_pen{tile=tp(4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK} local BOT_PEN = to_pen{tile=tp(11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} -function FilterSelection:draw_divider(dc) +function QualityAndMaterialsPage:draw_divider(dc) local y2 = dc.height - 1 for y=0,y2 do dc:seek(0, y) @@ -781,6 +939,127 @@ function FilterSelection:draw_divider(dc) 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_inset={l=1, r=1}, +} + +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 = 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 [MOCK -- NOT FUNCTIONAL]', + frame={w=53, h=53, l=30, t=8}, + frame_inset={t=1}, + resizable=true, + index=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}, + GlobalSettingsPage{}, + }, + }, + } +end + FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) FilterSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', @@ -794,10 +1073,12 @@ function FilterSelectionScreen:init() 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 @@ -1224,7 +1505,7 @@ function PlannerOverlay:set_filter(idx) end function PlannerOverlay:clear_filter(idx) - setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1, "") + clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end local function get_placement_data() @@ -1625,11 +1906,27 @@ function InspectorOverlay:make_top_priority() self:reset() end +local RESUME_BUTTON_FRAME = {t=15, h=3, r=73, w=25} + +local function mouse_is_over_resume_button(rect) + local x,y = dfhack.screen.getMousePos() + if not x then return false end + if y < RESUME_BUTTON_FRAME.t or y > RESUME_BUTTON_FRAME.t + RESUME_BUTTON_FRAME.h - 1 then + return false + end + if x > rect.x2 - RESUME_BUTTON_FRAME.r + 1 or x < rect.x2 - RESUME_BUTTON_FRAME.r - RESUME_BUTTON_FRAME.w + 2 then + return false + end + return true +end + function InspectorOverlay:onInput(keys) if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then return false end - if keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then + return true + elseif keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then self:reset() end return InspectorOverlay.super.onInput(self, keys) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 56ad72372..3d476bf0d 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -493,6 +493,9 @@ function feed_viewscreen_widgets(vs_name, keys) return false end gui.markMouseClicksHandled(keys) + if keys._MOUSE_L_DOWN then + df.global.enabler.mouse_lbut = 0 + end return true end