diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index b732e504f..7e759bd10 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -1,7 +1,7 @@ local _ENV = mkmodule('plugins.sort.info') local gui = require('gui') -local overlay = require('plugins.overlay') +local sortoverlay = require('plugins.sort.sortoverlay') local widgets = require('gui.widgets') local utils = require('utils') @@ -12,8 +12,6 @@ local objects = info.artifacts local tasks = info.jobs local work_details = info.labor.work_details -local state = {} - -- these sort functions attempt to match the vanilla info panel sort behavior, which -- is not quite the same as the rest of DFHack. For example, in other DFHack sorts, -- we'd always sort by name descending as a secondary sort. To match vanilla sorting, @@ -98,49 +96,6 @@ local function get_sort() end end -local function copy_to_lua_table(vec) - local tab = {} - for k,v in ipairs(vec) do - tab[k+1] = v - end - return tab -end - -local function general_search(vec, get_search_key_fn, get_sort_fn, matches_filters_fn, data, filter, incremental) - if not data.saved_original then - data.saved_original = copy_to_lua_table(vec) - elseif not incremental then - vec:assign(data.saved_original) - end - if matches_filters_fn ~= DEFAULT_NIL or filter ~= '' then - local search_tokens = filter:split() - for idx = #vec-1,0,-1 do - local search_key = get_search_key_fn(vec[idx]) - if (search_key and not utils.search_text(search_key, search_tokens)) or - (matches_filters_fn ~= DEFAULT_NIL and not matches_filters_fn(vec[idx])) - then - vec:erase(idx) - end - end - end - data.saved_visible = copy_to_lua_table(vec) - if get_sort_fn then - table.sort(data.saved_visible, get_sort_fn()) - vec:assign(data.saved_visible) - end -end - --- add dynamically allocated elements that were not visible at the time of --- closure back to the vector so they can be cleaned up when it is next initialized -local function cri_unitst_cleanup(vec, data) - if not data.saved_visible or not data.saved_original then return end - for _,elem in ipairs(data.saved_original) do - if not utils.linear_index(data.saved_visible, elem) then - vec:insert('#', elem) - end - end -end - local function get_unit_search_key(unit) return ('%s %s %s'):format( dfhack.units.getReadableName(unit), -- last name is in english @@ -148,200 +103,46 @@ local function get_unit_search_key(unit) dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name end -local function make_cri_unitst_handlers(vec, sort_fn) - return { - search_fn=curry(general_search, vec, - function(elem) - return ('%s %s'):format( - elem.un and get_unit_search_key(elem.un) or '', - elem.job_sort_name) - end, - sort_fn), - cleanup_fn=curry(cri_unitst_cleanup, vec), - } +local function get_cri_unit_search_key(cri_unit) + return ('%s %s'):format( + cri_unit.un and get_unit_search_key(cri_unit.un) or '', + cri_unit.job_sort_name) +end + +local function get_race_name(raw_id) + local raw = df.creature_raw.find(raw_id) + if not raw then return end + return raw.name[1] end -local function overall_training_search(matches_filters_fn, data, filter, incremental) - general_search(creatures.atk_index, function(elem) - local raw = df.creature_raw.find(elem) - if not raw then return end - return raw.name[1] - end, nil, matches_filters_fn, data, filter, incremental) +local function get_trainer_search_key(unit) + if not unit then return end + return ('%s %s'):format(dfhack.TranslateName(unit.name), dfhack.units.getProfessionName(unit)) end -local function assign_trainer_search(matches_filters_fn, data, filter, incremental) - general_search(creatures.trainer, function(elem) - if not elem then return end - return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.units.getProfessionName(elem)) - end, nil, matches_filters_fn, data, filter, incremental) +-- get name in both dwarvish and English +local function get_artifact_search_key(artifact) + return ('%s %s'):format(dfhack.TranslateName(artifact.name), dfhack.TranslateName(artifact.name, true)) end -local function work_details_search(matches_filters_fn, data, filter, incremental) +local function work_details_search(vec, data, text, incremental) if work_details.selected_work_detail_index ~= data.selected then data.saved_original = nil data.selected = work_details.selected_work_detail_index end - general_search(work_details.assignable_unit, get_unit_search_key, - nil, matches_filters_fn, data, filter, incremental) -end - --- independent implementation of search algorithm since we need to --- keep two vectors in sync -local function interrogating_search(matches_filters_fn, data, filter, incremental) - local vec, flags_vec = justice.interrogation_list, justice.interrogation_list_flag - if not data.saved_original then - data.saved_original = copy_to_lua_table(vec) - data.saved_flags = copy_to_lua_table(flags_vec) - data.saved_idx_map = {} - for idx, unit in ipairs(data.saved_original) do - data.saved_idx_map[unit.id] = idx -- 1-based idx - end - else -- sync flag changes to saved vector - for idx, unit in ipairs(vec) do -- 0-based idx - data.saved_flags[data.saved_idx_map[unit.id]] = flags_vec[idx] - end - end - - if not incremental then - vec:assign(data.saved_original) - flags_vec:assign(data.saved_flags) - end - - if matches_filters_fn ~= DEFAULT_NIL or filter ~= '' then - local search_tokens = filter:split() - for idx = #vec-1,0,-1 do - local search_key = get_unit_search_key(vec[idx]) - if (search_key and not utils.search_text(search_key, search_tokens)) or - (matches_filters_fn ~= DEFAULT_NIL and not matches_filters_fn(vec[idx], idx)) - then - vec:erase(idx) - flags_vec:erase(idx) - end - end - end -end - -local function convicting_search(matches_filters_fn, data, filter, incremental) - general_search(justice.conviction_list, get_unit_search_key, - nil, matches_filters_fn, data, filter, incremental) -end - -local HANDLERS = { - CITIZEN=make_cri_unitst_handlers(creatures.cri_unit.CITIZEN, get_sort), - PET=make_cri_unitst_handlers(creatures.cri_unit.PET, get_sort), - OTHER=make_cri_unitst_handlers(creatures.cri_unit.OTHER, get_sort), - DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED, get_sort), - PET_OT={search_fn=overall_training_search}, - PET_AT={search_fn=assign_trainer_search}, - JOBS=make_cri_unitst_handlers(tasks.cri_job), - WORK_DETAILS={search_fn=work_details_search}, - INTERROGATING={search_fn=interrogating_search}, - CONVICTING={search_fn=convicting_search}, -} -for idx,name in ipairs(df.artifacts_mode_type) do - if idx < 0 then goto continue end - HANDLERS[name] = { - search_fn=curry(general_search, objects.list[idx], - function(elem) - return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.TranslateName(elem.name, true)) - end, nil) - } - ::continue:: -end - -local function get_key() - if info.current_mode == df.info_interface_mode_type.JUSTICE then - if justice.interrogating then - return 'INTERROGATING' - elseif justice.convicting then - return 'CONVICTING' - end - elseif info.current_mode == df.info_interface_mode_type.CREATURES then - if creatures.current_mode == df.unit_list_mode_type.PET then - if creatures.showing_overall_training then - return 'PET_OT' - elseif creatures.adding_trainer then - return 'PET_AT' - end - end - return df.unit_list_mode_type[creatures.current_mode] - elseif info.current_mode == df.info_interface_mode_type.JOBS then - return 'JOBS' - elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then - return df.artifacts_mode_type[objects.mode] - elseif info.current_mode == df.info_interface_mode_type.LABOR then - if info.labor.mode == df.labor_mode_type.WORK_DETAILS then - return 'WORK_DETAILS' - end - end -end - -local function check_context(self, key_ctx) - local key = get_key() - if state[key_ctx] ~= key then - state[key_ctx] = key - local prev_text = key and ensure_key(state, key).prev_text or '' - self.subviews.search:setText(prev_text) - end -end - -local function do_search(matches_filters_fn, text, force_full_search) - if not force_full_search and not next(state) and text == '' then return end - -- the EditField state is guaranteed to be consistent with the current - -- context since when clicking to switch tabs, onRenderBody is always called - -- before this text_input callback, even if a key is pressed before the next - -- graphical frame would otherwise be printed. if this ever becomes untrue, - -- then we can add an on_char handler to the EditField that also calls - -- check_context. - local key = get_key() - if not key then return end - local prev_text = ensure_key(state, key).prev_text - -- some screens reset their contents between context switches; regardless - -- a switch back to the context should results in an incremental search - local incremental = not force_full_search and prev_text and text:startswith(prev_text) - HANDLERS[key].search_fn(matches_filters_fn, state[key], text, incremental) - state[key].prev_text = text -end - -local function on_update(self) - if self.overlay_onupdate_max_freq_seconds == 0 and - not dfhack.gui.matchFocusString('dwarfmode/Info', dfhack.gui.getDFViewscreen(true)) - then - for k,v in pairs(state) do - local cleanup_fn = safe_index(HANDLERS, k, 'cleanup_fn') - if cleanup_fn then cleanup_fn(v) end - end - state = {} - self.subviews.search:setText('') - self.subviews.search:setFocus(false) - self.overlay_onupdate_max_freq_seconds = 60 - end -end - -local function on_input(self, clazz, keys) - if keys._MOUSE_R and self.subviews.search.focus and self:get_handled_key() then - self.subviews.search:setFocus(false) - return true - end - return clazz.super.onInput(self, keys) -end - -local function is_interrogate_or_convict() - local key = get_key() - return key == 'INTERROGATING' or key == 'CONVICTING' + sortoverlay.single_vector_search( + {get_search_key_fn=get_unit_search_key}, + vec, data, text, incremental) end -- ---------------------- -- InfoOverlay -- -InfoOverlay = defclass(InfoOverlay, overlay.OverlayWidget) +InfoOverlay = defclass(InfoOverlay, sortoverlay.SortOverlay) InfoOverlay.ATTRS{ default_pos={x=64, y=8}, - default_enabled=true, viewscreens='dwarfmode/Info', - hotspot=true, - overlay_onupdate_max_freq_seconds=0, frame={w=40, h=4}, } @@ -350,26 +151,71 @@ function InfoOverlay:init() widgets.BannerPanel{ view_id='panel', frame={l=0, t=0, r=0, h=1}, - visible=self:callback('get_handled_key'), + visible=self:callback('get_key'), subviews={ widgets.EditField{ view_id='search', frame={l=1, t=0, r=1}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=function(text) do_search(DEFAULT_NIL, text) end, + on_change=function(text) self:do_search(text) end, }, }, }, } -end -function InfoOverlay:overlay_onupdate() - on_update(self) -end - -function InfoOverlay:get_handled_key() - return not is_interrogate_or_convict() and get_key() or nil + local CRI_UNIT_VECS = { + CITIZEN=creatures.cri_unit.CITIZEN, + PET=creatures.cri_unit.PET, + OTHER=creatures.cri_unit.OTHER, + DECEASED=creatures.cri_unit.DECEASED, + } + for key,vec in pairs(CRI_UNIT_VECS) do + self:register_handler(key, vec, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=get_cri_unit_search_key, + get_sort_fn=get_sort + }), + true) + end + + self:register_handler('JOBS', tasks.cri_job, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_cri_unit_search_key}), + true) + self:register_handler('PET_OT', creatures.atk_index, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_race_name})) + self:register_handler('PET_AT', creatures.trainer, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_trainer_search_key})) + self:register_handler('WORK_DETAILS', work_details.assignable_unit, work_details_search) + + for idx,name in ipairs(df.artifacts_mode_type) do + if idx < 0 then goto continue end + self:register_handler(name, objects.list[idx], + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_artifact_search_key})) + ::continue:: + end +end + +function InfoOverlay:get_key() + if info.current_mode == df.info_interface_mode_type.CREATURES then + if creatures.current_mode == df.unit_list_mode_type.PET then + if creatures.showing_overall_training then + return 'PET_OT' + elseif creatures.adding_trainer then + return 'PET_AT' + end + end + return df.unit_list_mode_type[creatures.current_mode] + elseif info.current_mode == df.info_interface_mode_type.JOBS then + return 'JOBS' + elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then + return df.artifacts_mode_type[objects.mode] + elseif info.current_mode == df.info_interface_mode_type.LABOR then + if info.labor.mode == df.labor_mode_type.WORK_DETAILS then + return 'WORK_DETAILS' + end + end end local function resize_overlay(self) @@ -410,49 +256,34 @@ function InfoOverlay:updateFrames() end function InfoOverlay:onRenderBody(dc) - if next(state) then - check_context(self, InfoOverlay) - end + InfoOverlay.super.onRenderBody(self, dc) if self:updateFrames() then self:updateLayout() end if self.refresh_search then self.refresh_search = nil - do_search(DEFAULT_NIL, self.subviews.search.text) + self:do_search(self.subviews.search.text) end - self.overlay_onupdate_max_freq_seconds = 0 - InfoOverlay.super.onRenderBody(self, dc) end function InfoOverlay:onInput(keys) - if keys._MOUSE_L and get_key() == 'WORK_DETAILS' then + if keys._MOUSE_L and self:get_key() == 'WORK_DETAILS' then self.refresh_search = true end - return on_input(self, InfoOverlay, keys) + return InfoOverlay.super.onInput(self, keys) end -- ---------------------- -- InterrogationOverlay -- -InterrogationOverlay = defclass(InterrogationOverlay, overlay.OverlayWidget) +InterrogationOverlay = defclass(InterrogationOverlay, sortoverlay.SortOverlay) InterrogationOverlay.ATTRS{ default_pos={x=47, y=10}, - default_enabled=true, viewscreens='dwarfmode/Info/JUSTICE', frame={w=27, h=9}, - hotspot=true, - overlay_onupdate_max_freq_seconds=0, } -function InterrogationOverlay:overlay_onupdate() - on_update(self) -end - -function InterrogationOverlay:get_handled_key() - return is_interrogate_or_convict() and get_key() or nil -end - function InterrogationOverlay:init() self:addviews{ widgets.Panel{ @@ -460,14 +291,14 @@ function InterrogationOverlay:init() frame={l=0, t=4, h=5, r=0}, frame_background=gui.CLEAR_PEN, frame_style=gui.FRAME_MEDIUM, - visible=is_interrogate_or_convict, + visible=self:callback('get_key'), subviews={ widgets.EditField{ view_id='search', frame={l=0, t=0, r=0}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=function(text) do_search(self:callback('matches_filters'), text) end, + on_change=function(text) self:do_search(text) end, }, widgets.ToggleHotkeyLabel{ view_id='include_interviewed', @@ -479,9 +310,7 @@ function InterrogationOverlay:init() {label='Exclude', value=false, pen=COLOR_RED}, }, visible=function() return justice.interrogating end, - on_change=function() - do_search(self:callback('matches_filters'), self.subviews.search.text, true) - end, + on_change=function() self:do_search(self.subviews.search.text, true) end, }, widgets.CycleHotkeyLabel{ view_id='subset', @@ -498,13 +327,40 @@ function InterrogationOverlay:init() {label='Deceased or missing', value='deceased', pen=COLOR_MAGENTA}, {label='Others', value='others', pen=COLOR_GRAY}, }, - on_change=function() - do_search(self:callback('matches_filters'), self.subviews.search.text, true) - end, + on_change=function() self:do_search(self.subviews.search.text, true) end, }, }, }, } + + self:register_handler('INTERROGATING', justice.interrogation_list, + curry(sortoverlay.flags_vector_search, + { + get_search_key_fn=get_unit_search_key, + get_elem_id_fn=function(unit) return unit.id end, + matches_filters_fn=self:callback('matches_filters'), + }, + justice.interrogation_list_flag)) + self:register_handler('CONVICTING', justice.conviction_list, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=get_unit_search_key, + matches_filters_fn=self:callback('matches_filters'), + })) +end + +function InterrogationOverlay:reset() + InterrogationOverlay.super.reset(self) + self.subviews.include_interviewed:setOption(true, false) + self.subviews.subset:setOption('all') +end + +function InterrogationOverlay:get_key() + if justice.interrogating then + return 'INTERROGATING' + elseif justice.convicting then + return 'CONVICTING' + end end local RISKY_PROFESSIONS = utils.invert{ @@ -521,18 +377,20 @@ local function is_risky(unit) return not dfhack.units.isAlive(unit) -- detect intelligent undead end -function InterrogationOverlay:matches_filters(unit, idx) +function InterrogationOverlay:matches_filters(unit, flag) if justice.interrogating then local include_interviewed = self.subviews.include_interviewed:getOptionValue() - if not include_interviewed and justice.interrogation_list_flag[idx] == 2 then - return false - end + if not include_interviewed and flag == 2 then return false end end local subset = self.subviews.subset:getOptionValue() if subset == 'all' then return true elseif dfhack.units.isDead(unit) or not dfhack.units.isActive(unit) then return subset == 'deceased' + elseif dfhack.units.isInvader(unit) or dfhack.units.isOpposedToLife(unit) + or unit.flags2.visitor_uninvited or unit.flags4.agitated_wilderness_creature + then + return subset == 'others' elseif dfhack.units.isVisiting(unit) then local risky = is_risky(unit) return (subset == 'risky' and risky) or (subset == 'visitors' and not risky) @@ -569,19 +427,4 @@ function InterrogationOverlay:render(dc) InterrogationOverlay.super.render(self, dc) end -function InterrogationOverlay:onRenderBody(dc) - if next(state) then - check_context(self, InterrogationOverlay) - else - self.subviews.include_interviewed:setOption(true, false) - self.subviews.subset:setOption('all') - end - self.overlay_onupdate_max_freq_seconds = 0 - InterrogationOverlay.super.onRenderBody(self, dc) -end - -function InterrogationOverlay:onInput(keys) - return on_input(self, InterrogationOverlay, keys) -end - return _ENV diff --git a/plugins/lua/sort/locationselector.lua b/plugins/lua/sort/locationselector.lua index 8092db1e2..8a79a9d8f 100644 --- a/plugins/lua/sort/locationselector.lua +++ b/plugins/lua/sort/locationselector.lua @@ -51,7 +51,7 @@ function LocationSelectorOverlay:init() self:register_handler('TEMPLE', location_selector.valid_religious_practice_id, curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_religion_string}, - location_selector.valid_religious_practice)) + location_selector.valid_religious_practice)) self:register_handler('GUILDHALL', location_selector.valid_craft_guild_type, curry(sortoverlay.single_vector_search, {get_search_key_fn=get_profession_string})) end diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index 96c0f8ecc..b3067a8ba 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -56,13 +56,17 @@ function SortOverlay:overlay_onupdate() restore_filtered(self.handlers[key].vec, data) end end - self.state = {} - self.subviews.search:setText('') - self.subviews.search:setFocus(false) + self:reset() self.overlay_onupdate_max_freq_seconds = 300 end end +function SortOverlay:reset() + self.state = {} + self.subviews.search:setText('') + self.subviews.search:setFocus(false) +end + -- returns the current context key for dereferencing the handler -- subclasses must override function SortOverlay:get_key() @@ -77,6 +81,7 @@ function SortOverlay:onRenderBody(dc) self.state.cur_key = key local prev_text = key and ensure_key(self.state, key).prev_text or '' self.subviews.search:setText(prev_text) + self:do_search(self.subviews.search.text, true) end end self.overlay_onupdate_max_freq_seconds = 0 @@ -141,7 +146,7 @@ end -- doesn't support cleanup since nothing that uses this needs it yet function flags_vector_search(fns, flags_vec, vec, data, text, incremental) - local get_elem_id_fn = fns.get_elem_id_fn and fns.get_elem_id_fn(elem) or function(elem) return elem end + local get_elem_id_fn = fns.get_elem_id_fn or function(elem) return elem end if not data.saved_original then data.saved_original = copy_to_lua_table(vec) data.saved_flags = copy_to_lua_table(flags_vec)