Merge pull request #3863 from myk002/myk_location_selector

[sort] add support for location selector screen
develop
Myk 2023-10-10 04:12:58 -07:00 committed by GitHub
commit 9bbda642bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 382 additions and 285 deletions

@ -58,6 +58,7 @@ Template for new versions:
## New Features
- `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay
- `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice"
- `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate)
## Fixes
- `zone`: don't show animal assignment link for dungeon cages/restraints

@ -92,8 +92,8 @@ https://www.reddit.com/r/dwarffortress/comments/163kczo/enhancing_military_candi
"Mental stability" is explained here:
https://www.reddit.com/r/dwarffortress/comments/1617s11/enhancing_military_candidate_selection_part_2/
Info overlay
------------
Info tabs overlay
-----------------
The Info overlay adds search support to many of the fort-wide "Info" panels
(e.g. "Creatures", "Tasks", etc.). When searching for units, you can search by
@ -102,13 +102,21 @@ special status (like "necromancer"). If there is text in the second column, you
can search for that text as well. This is often a job name or a status, like
"caged".
Interrogation overlay
---------------------
In the interrogation and conviction screens under the "Justice" tab, you can
also filter by the classification of the unit. The classification groups are
ordered by how likely a member of that group is to be involved in a plot. The
groups are: All, Risky visitors, Other visitors, Residents, Citizens, Animals,
Deceased, and Others. "Risky" visitors are those who are especially likely to
be involved in plots, such as criminals, necromancers, necromancer experiments,
and intelligent undead.
search for units by name. You can also filter by the classification of the
unit. The classification groups are ordered by how likely a member of that
group is to be involved in a plot. The groups are: All, Risky visitors, Other
visitors, Residents, Citizens, Animals, Deceased, and Others. "Risky" visitors are those who are especially likely to be involved in plots, such as criminals,
necromancers, necromancer experiments, and intelligent undead.
On the interrogations screen, you can also filter units by whether they have
already been interrogated.
Location selection overlay
--------------------------
When choosing the type of guildhall or temple to dedicate, you can search for
the relevant profession, religion, or deity by name.

@ -2,6 +2,7 @@ local _ENV = mkmodule('plugins.sort')
local info = require('plugins.sort.info')
local gui = require('gui')
local locationselector = require('plugins.sort.locationselector')
local overlay = require('plugins.overlay')
local setbelief = reqscript('modtools/set-belief')
local textures = require('gui.textures')
@ -1288,6 +1289,7 @@ OVERLAY_WIDGETS = {
squad_annotation=SquadAnnotationOverlay,
info=info.InfoOverlay,
interrogation=info.InterrogationOverlay,
location_selector=locationselector.LocationSelectorOverlay,
}
dfhack.onStateChange[GLOBAL_KEY] = function(sc)

@ -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

@ -0,0 +1,67 @@
local _ENV = mkmodule('plugins.sort.locationselector')
local sortoverlay = require('plugins.sort.sortoverlay')
local widgets = require('gui.widgets')
local location_selector = df.global.game.main_interface.location_selector
-- ----------------------
-- LocationSelectorOverlay
--
LocationSelectorOverlay = defclass(LocationSelectorOverlay, sortoverlay.SortOverlay)
LocationSelectorOverlay.ATTRS{
default_pos={x=48, y=7},
viewscreens='dwarfmode/LocationSelector',
frame={w=26, h=1},
}
local function get_religion_string(religion_id, religion_type)
if religion_id == -1 then return end
local entity
if religion_type == 0 then
entity = df.historical_figure.find(religion_id)
elseif religion_type == 1 then
entity = df.historical_entity.find(religion_id)
end
if not entity then return end
return dfhack.TranslateName(entity.name, true)
end
local function get_profession_string(profession)
return df.profession[profession]:gsub('_', ' ')
end
function LocationSelectorOverlay:init()
self:addviews{
widgets.BannerPanel{
frame={l=0, t=0, r=0, h=1},
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) self:do_search(text) end,
},
},
},
}
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))
self:register_handler('GUILDHALL', location_selector.valid_craft_guild_type,
curry(sortoverlay.single_vector_search, {get_search_key_fn=get_profession_string}))
end
function LocationSelectorOverlay:get_key()
if location_selector.choosing_temple_religious_practice then
return 'TEMPLE'
elseif location_selector.choosing_craft_guild then
return 'GUILDHALL'
end
end
return _ENV

@ -0,0 +1,176 @@
local _ENV = mkmodule('plugins.sort.sortoverlay')
local overlay = require('plugins.overlay')
local utils = require('utils')
local function copy_to_lua_table(vec)
local tab = {}
for k,v in ipairs(vec) do
tab[k+1] = v
end
return tab
end
-- ----------------------
-- SortOverlay
--
SortOverlay = defclass(SortOverlay, overlay.OverlayWidget)
SortOverlay.ATTRS{
default_enabled=true,
hotspot=true,
overlay_onupdate_max_freq_seconds=0,
-- subclasses expected to provide default_pos, viewscreens (single string), and frame
-- viewscreens should be the top-level scope within which the search widget state is maintained
-- once the player leaves that scope, widget state will be reset
}
function SortOverlay:init()
self.state = {}
self.handlers = {}
-- subclasses expected to provide an EditField widget with view_id='search'
end
function SortOverlay:register_handler(key, vec, search_fn, restore_filtered_on_cleanup)
self.handlers[key] = {
vec=vec,
search_fn=search_fn,
restore_filtered_on_cleanup=restore_filtered_on_cleanup
}
end
local function restore_filtered(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
-- handles reset and clean up when the player exits the handled scope
function SortOverlay:overlay_onupdate()
if self.overlay_onupdate_max_freq_seconds == 0 and
not dfhack.gui.matchFocusString(self.viewscreens, dfhack.gui.getDFViewscreen(true))
then
for key,data in pairs(self.state) do
if safe_index(self.handlers, key, 'restore_filtered_on_cleanup') then
restore_filtered(self.handlers[key].vec, data)
end
end
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()
return nil
end
-- handles saving/restoring search strings when the player moves between different contexts
function SortOverlay:onRenderBody(dc)
if next(self.state) then
local key = self:get_key()
if self.state.cur_key ~= key then
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
SortOverlay.super.onRenderBody(self, dc)
end
function SortOverlay:onInput(keys)
if keys._MOUSE_R and self.subviews.search.focus and self:get_key() then
self.subviews.search:setFocus(false)
return true
end
return SortOverlay.super.onInput(self, keys)
end
function SortOverlay:do_search(text, force_full_search)
if not force_full_search and not next(self.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 checks for
-- context transitions.
local key = self:get_key()
if not key then return end
local prev_text = ensure_key(self.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)
local handler = self.handlers[key]
handler.search_fn(handler.vec, self.state[key], text, incremental)
self.state[key].prev_text = text
end
local function filter_vec(fns, flags_vec, vec, text, erase_fn)
if fns.matches_filters_fn or text ~= '' then
local search_tokens = text:split()
for idx = #vec-1,0,-1 do
local flag = flags_vec and flags_vec[idx] or nil
local search_key = fns.get_search_key_fn(vec[idx], flag)
if (search_key and not utils.search_text(search_key, search_tokens)) or
(fns.matches_filters_fn and not fns.matches_filters_fn(vec[idx], flag))
then
erase_fn(idx)
end
end
end
end
function single_vector_search(fns, vec, data, text, 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
filter_vec(fns, nil, vec, text, function(idx) vec:erase(idx) end)
data.saved_visible = copy_to_lua_table(vec)
if fns.get_sort_fn then
table.sort(data.saved_visible, fns.get_sort_fn())
vec:assign(data.saved_visible)
end
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 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)
data.saved_idx_map = {}
for idx,elem in ipairs(data.saved_original) do
data.saved_idx_map[get_elem_id_fn(elem)] = idx -- 1-based idx
end
else -- sync flag changes to saved vector
for idx,elem in ipairs(vec) do -- 0-based idx
data.saved_flags[data.saved_idx_map[get_elem_id_fn(elem)]] = flags_vec[idx]
end
end
if not incremental then
vec:assign(data.saved_original)
flags_vec:assign(data.saved_flags)
end
filter_vec(fns, flags_vec, vec, text, function(idx)
vec:erase(idx)
flags_vec:erase(idx)
end)
end
return _ENV