From b466050858cfc8425f8c662e14d12376849739f8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 5 Apr 2024 23:55:27 -0700 Subject: [PATCH 01/26] add skeleton and overlay link --- gui/manipulator.lua | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 gui/manipulator.lua diff --git a/gui/manipulator.lua b/gui/manipulator.lua new file mode 100644 index 0000000000..9e29e645aa --- /dev/null +++ b/gui/manipulator.lua @@ -0,0 +1,75 @@ +--@module = true + +local gui = require("gui") +local widgets = require("gui.widgets") +local overlay = require('plugins.overlay') + +------------------------ +-- Manipulator +-- + +Manipulator = defclass(Manipulator, widgets.Window) +Manipulator.ATTRS { + frame_title='Unit Overview and Manipulator', + frame={w=110, h=40}, + resizable=true, + resize_min={w=70, h=15}, +} + +function Manipulator:init() + self:addviews{ + } +end + +------------------------ +-- ManipulatorScreen +-- + +ManipulatorScreen = defclass(ManipulatorScreen, gui.ZScreen) +ManipulatorScreen.ATTRS { + focus_path='manipulator', +} + +function ManipulatorScreen:init() + self:addviews{Manipulator{}} +end + +function ManipulatorScreen:onDismiss() + view = nil +end + +------------------------ +-- ManipulatorOverlay +-- + +ManipulatorOverlay = defclass(ManipulatorOverlay, overlay.OverlayWidget) +ManipulatorOverlay.ATTRS{ + desc='Adds a hotkey to the vanilla units screen to launch the DFHack units interface.', + default_pos={x=50, y=-5}, + default_enabled=true, + viewscreens='dwarfmode/Info/CREATURES/CITIZEN', + frame={w=34, h=1}, +} + +function ManipulatorOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='DFHack citizen interface', + key='CUSTOM_CTRL_N', + on_activate=function() dfhack.run_script('gui/manipulator') end, + }, + } +end + +OVERLAY_WIDGETS = { + launcher=ManipulatorOverlay, +} + +if dfhack_flags.module then return end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror("This script requires a fortress map to be loaded") +end + +view = view and view:raise() or ManipulatorScreen{}:show() From db1e07bc9a68324b3b39f9cc67a6eb6b6146424c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Apr 2024 20:07:31 -0700 Subject: [PATCH 02/26] get basic display and scrolling working --- gui/manipulator.lua | 301 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 297 insertions(+), 4 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 9e29e645aa..37f3f48b8d 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -1,23 +1,316 @@ --@module = true local gui = require("gui") -local widgets = require("gui.widgets") +local json = require('json') local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require("gui.widgets") + +local CONFIG_FILE = 'dfhack-config/manipulator.json' + +local config = json.open(CONFIG_FILE) + +------------------------ +-- Column +-- + +Column = defclass(Column, widgets.Panel) +Column.ATTRS{ + label='', + data_fn=DEFAULT_NIL, + count_fn=DEFAULT_NIL, + make_sort_order_fn=DEFAULT_NIL, + group=DEFAULT_NIL, + label_inset=0, + data_width=4, + hidden=DEFAULT_NIL, + autoarrange_subviews=true, +} + +function Column:init() + self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) + + if not self.make_sort_order_fn then + self.make_sort_order_fn = function(unit_ids) + local spec = {key=function(choice) return self.data_fn(df.unit.find(choice.unit_id)) end} + return utils.make_sort_order(choices, {spec}) + end + end + + if self.hidden == nil then + self.hidden = safe_index(config.data, 'cols', self.label, 'hidden') + end + + self:addviews{ + widgets.Panel{ + frame={l=0, h=5}, + subviews={ + widgets.Divider{ + view_id='col_stem', + frame={l=self.label_inset, t=4, w=1, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_b=false, + }, + widgets.HotkeyLabel{ + view_id='col_label', + frame={l=self.label_inset, t=4}, + label=self.label, + on_activate=function() end, -- TODO: sort by this column + }, + }, + }, + widgets.Label{ + view_id='col_current', + frame={l=1+self.label_inset, w=4}, + }, + widgets.Label{ + view_id='col_total', + frame={l=1+self.label_inset, w=4}, + }, + widgets.List{ + view_id='col_list', + frame={l=0, w=self.data_width}, + }, + } + + self.subviews.col_list.scrollbar.visible = false +end + +function Column:set_data(units, unit_ids, sort_order) + self.unit_ids, self.sort_order = unit_ids, sort_order + + local choices = {} + local current, total = 0, 0 + local next_id_idx = 1 + for _, unit in ipairs(units) do + local val = self.count_fn(unit) + if unit.id == unit_ids[next_id_idx] then + local data = self.data_fn(unit) + table.insert(choices, (not data or data == 0) and '-' or tostring(data)) + current = current + val + next_id_idx = next_id_idx + 1 + end + total = total + val + end + self.subviews.col_current:setText(tostring(current)) + self.subviews.col_total:setText(tostring(total)) + self.subviews.col_list:setChoices(choices) +end + +function Column:set_stem_height(h) + self.subviews.col_label.frame.t = 4 - h + self.subviews.col_stem.frame.t = 4 - h + self.subviews.col_stem.frame.h = h + 1 +end + +------------------------ +-- DataColumn +-- + +DataColumn = defclass(DataColumn, Column) +DataColumn.ATTRS{ +} + +function DataColumn:init() + if not self.count_fn then + self.count_fn = function(unit) + local data = self.data_fn(unit) + if not data then return 0 end + if type(data) == 'number' then return data > 0 and 1 or 0 end + return 1 + end + end +end + +------------------------ +-- ToggleColumn +-- + +ToggleColumn = defclass(ToggleColumn, Column) +ToggleColumn.ATTRS{ + on_toggle=DEFAULT_NIL, +} + +function ToggleColumn:init() + if not self.count_fn then + self.count_fn = function(unit) return self.data_fn(unit) and 1 or 0 end + end +end + +------------------------ +-- Spreadsheet +-- + +Spreadsheet = defclass(Spreadsheet, widgets.Panel) + +function Spreadsheet:init() + self.left_col = 1 + + local cols = widgets.Panel{} + self.cols = cols + + cols:addviews{ + ToggleColumn{ + label='Favorites', + data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, + }, + } + + for i in ipairs(df.job_skill) do + local caption = df.job_skill.attrs[i].caption + if caption then + cols:addviews{ + DataColumn{ + label=caption, + data_fn=function(unit) + return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating + end, + group='skills', + } + } + end + end + + self:addviews{ + widgets.Label{ + frame={t=5, l=0}, + text='Shown:', + }, + widgets.Label{ + frame={t=6, l=0}, + text='Total:', + }, + DataColumn{ + view_id='name', + frame={w=30}, + label='Name', + label_inset=8, + data_fn=dfhack.units.getReadableName, + data_width=30, + }, + cols, + } + + self.list = self.subviews.name.subviews.col_list + self:addviews{ + widgets.Scrollbar{ + view_id='scrollbar', + frame={t=7, r=0}, + on_scroll=self.list:callback('on_scrollbar'), + } + } + self.list.scrollbar = self.subviews.scrollbar + + self:refresh() +end + +-- TODO: apply search and filtering +function Spreadsheet:get_visible_unit_ids(units) + local visible_unit_ids = {} + for _, unit in ipairs(units) do + table.insert(visible_unit_ids, unit.id) + end + return visible_unit_ids +end + +function Spreadsheet:update_col_layout(idx, col, width, max_width) + col.visible = not col.hidden and idx >= self.left_col and width + col.frame.w <= max_width + col.frame.l = width + return width + (col.visible and col.data_width+1 or 0) +end + +function Spreadsheet:refresh() + local units = dfhack.units.getCitizens() + local visible_unit_ids = self:get_visible_unit_ids(units) + --local sort_order = self.subviews.name.sort_order or self.subviews.name.make_sort_order_fn(visible_unit_ids) + local max_width = self.frame_body and self.frame_body.width or 0 + local ord, width = 1, self.subviews.name.data_width + 1 + self.subviews.name:set_data(units, visible_unit_ids, sort_order) + for idx, col in ipairs(self.cols.subviews) do + col:set_data(units, visible_unit_ids, sort_order) + if not col.hidden then + col:set_stem_height((6-ord)%5) + ord = ord + 1 + end + width = self:update_col_layout(idx, col, width, max_width) + end +end + +function Spreadsheet:preUpdateLayout(parent_rect) + local width = self.subviews.name.data_width + 1 + for idx, col in ipairs(self.cols.subviews) do + width = self:update_col_layout(idx, col, width, parent_rect.width) + end +end + +function Spreadsheet:render(dc) + local page_top = self.list.page_top + for idx, col in ipairs(self.cols.subviews) do + col.subviews.col_list.page_top = page_top + end + Spreadsheet.super.render(self, dc) +end ------------------------ -- Manipulator -- Manipulator = defclass(Manipulator, widgets.Window) -Manipulator.ATTRS { +Manipulator.ATTRS{ frame_title='Unit Overview and Manipulator', frame={w=110, h=40}, resizable=true, - resize_min={w=70, h=15}, + resize_min={w=70, h=25}, } function Manipulator:init() self:addviews{ + widgets.EditField{ + view_id='search', + frame={l=0, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + on_change=function() self.subviews.sheet:refresh() end, + }, + widgets.Divider{ + frame={l=0, r=0, t=2, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_l=false, + frame_style_r=false, + }, + Spreadsheet{ + view_id='sheet', + frame={l=0, t=3, r=0, b=7}, + }, + widgets.Divider{ + frame={l=0, r=0, b=6, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_l=false, + frame_style_r=false, + }, + widgets.Panel{ + frame={l=0, r=0, b=0, h=5}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Use arrow keys to navigate cells.', + }, + widgets.HotkeyLabel{ + frame={b=2, l=0}, + label='Sort/reverse sort by current column', + key='CUSTOM_SHIFT_S', + on_activate=function() end, -- TODO + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + auto_width=true, + label='Refresh', -- TODO add warning if citizen list has changed and needs refreshing + key='CUSTOM_SHIFT_R', + on_activate=function() end, -- TODO + }, + -- TODO moar hotkeys + }, + }, } end @@ -26,7 +319,7 @@ end -- ManipulatorScreen = defclass(ManipulatorScreen, gui.ZScreen) -ManipulatorScreen.ATTRS { +ManipulatorScreen.ATTRS{ focus_path='manipulator', } From 666af6173b8a9f9eaf48829aeb3ad3881b935015 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 9 Apr 2024 09:59:28 -0700 Subject: [PATCH 03/26] implement horizontal scanning and jumping to group labels --- gui/manipulator.lua | 206 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 168 insertions(+), 38 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 37f3f48b8d..a844e8b35f 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -20,11 +20,10 @@ Column.ATTRS{ data_fn=DEFAULT_NIL, count_fn=DEFAULT_NIL, make_sort_order_fn=DEFAULT_NIL, - group=DEFAULT_NIL, + group='', label_inset=0, data_width=4, hidden=DEFAULT_NIL, - autoarrange_subviews=true, } function Column:init() @@ -42,8 +41,14 @@ function Column:init() end self:addviews{ + widgets.TextButton{ + view_id='col_group', + frame={t=0, l=0, h=1, w=#self.group+2}, + label=self.group, + visible=#self.group > 0, + }, widgets.Panel{ - frame={l=0, h=5}, + frame={t=2, l=0, h=5}, subviews={ widgets.Divider{ view_id='col_stem', @@ -61,32 +66,33 @@ function Column:init() }, widgets.Label{ view_id='col_current', - frame={l=1+self.label_inset, w=4}, + frame={t=7, l=1+self.label_inset, w=4}, }, widgets.Label{ view_id='col_total', - frame={l=1+self.label_inset, w=4}, + frame={t=8, l=1+self.label_inset, w=4}, }, widgets.List{ view_id='col_list', - frame={l=0, w=self.data_width}, + frame={t=10, l=0, w=self.data_width}, }, } self.subviews.col_list.scrollbar.visible = false end -function Column:set_data(units, unit_ids, sort_order) - self.unit_ids, self.sort_order = unit_ids, sort_order - +function Column:set_data(units, visible_unit_ids) local choices = {} local current, total = 0, 0 local next_id_idx = 1 for _, unit in ipairs(units) do local val = self.count_fn(unit) - if unit.id == unit_ids[next_id_idx] then + if unit.id == visible_unit_ids[next_id_idx] then local data = self.data_fn(unit) - table.insert(choices, (not data or data == 0) and '-' or tostring(data)) + table.insert(choices, { + text=(not data or data == 0) and '-' or tostring(data), + unit_id=unit.id, + }) current = current + val next_id_idx = next_id_idx + 1 end @@ -153,6 +159,7 @@ function Spreadsheet:init() ToggleColumn{ label='Favorites', data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, + group='tags', }, } @@ -171,13 +178,35 @@ function Spreadsheet:init() end end + for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do + cols:addviews{ + ToggleColumn{ + label=wd.name, + data_fn=function(unit) + return utils.binsearch(wd.assigned_units, unit.id) and true or false + end, + group='work details', + } + } + end + self:addviews{ + widgets.TextButton{ + view_id='left_group', + frame={t=0, l=0, h=1}, + visible=false, + }, + widgets.TextButton{ + view_id='right_group', + frame={t=0, r=0, h=1}, + visible=false, + }, widgets.Label{ - frame={t=5, l=0}, + frame={t=7, l=0}, text='Shown:', }, widgets.Label{ - frame={t=6, l=0}, + frame={t=8, l=0}, text='Total:', }, DataColumn{ @@ -213,44 +242,105 @@ function Spreadsheet:get_visible_unit_ids(units) return visible_unit_ids end -function Spreadsheet:update_col_layout(idx, col, width, max_width) - col.visible = not col.hidden and idx >= self.left_col and width + col.frame.w <= max_width - col.frame.l = width - return width + (col.visible and col.data_width+1 or 0) +function Spreadsheet:sort_by_current_row() +end + +function Spreadsheet:filter(search) +end + +function Spreadsheet:hide_current_row() +end + +function Spreadsheet:jump_to_group(group) + for i, col in ipairs(self.cols.subviews) do + if not col.hidden and col.group == group then + self.left_col = i + break + end + end + self:updateLayout() end function Spreadsheet:refresh() local units = dfhack.units.getCitizens() local visible_unit_ids = self:get_visible_unit_ids(units) --local sort_order = self.subviews.name.sort_order or self.subviews.name.make_sort_order_fn(visible_unit_ids) - local max_width = self.frame_body and self.frame_body.width or 0 - local ord, width = 1, self.subviews.name.data_width + 1 - self.subviews.name:set_data(units, visible_unit_ids, sort_order) - for idx, col in ipairs(self.cols.subviews) do - col:set_data(units, visible_unit_ids, sort_order) + local ord = 1 + self.subviews.name:set_data(units, visible_unit_ids) + for _, col in ipairs(self.cols.subviews) do + col:set_data(units, visible_unit_ids) if not col.hidden then - col:set_stem_height((6-ord)%5) + col:set_stem_height((5-ord)%5) ord = ord + 1 end - width = self:update_col_layout(idx, col, width, max_width) + end + if (self.frame_parent_rect) then + self:updateLayout() end end +function Spreadsheet:update_col_layout(idx, col, width, group, max_width) + col.visible = not col.hidden and idx >= self.left_col and width + col.frame.w <= max_width + col.frame.l = width + if not col.visible then + return width, group + end + local col_group = col.subviews.col_group + col_group.label.on_activate=self:callback('jump_to_group', col.group) + col_group.visible = group ~= col.group + return width + col.data_width + 1, col.group +end + function Spreadsheet:preUpdateLayout(parent_rect) - local width = self.subviews.name.data_width + 1 + local left_group, right_group = self.subviews.left_group, self.subviews.right_group + left_group.visible, right_group.visible = false, false + + local width, group, cur_col_group = self.subviews.name.data_width + 1, '', '' + local prev_col_group, next_col_group for idx, col in ipairs(self.cols.subviews) do - width = self:update_col_layout(idx, col, width, parent_rect.width) + local prev_group = group + width, group = self:update_col_layout(idx, col, width, group, parent_rect.width) + if not next_col_group and group ~= '' and not col.visible and col.group ~= cur_col_group then + next_col_group = col.group + local str = next_col_group .. string.char(26) -- right arrow + right_group:setLabel(str) + right_group.frame.w = #str + 2 + right_group.label.on_activate=self:callback('jump_to_group', next_col_group) + right_group.visible = true + end + if cur_col_group ~= col.group then + prev_col_group = cur_col_group + end + cur_col_group = col.group + if prev_group == '' and group ~= '' and prev_col_group and prev_col_group ~= '' then + local str = string.char(27) .. prev_col_group -- left arrow + left_group:setLabel(str) + left_group.frame.w = #str + 2 + left_group.label.on_activate=self:callback('jump_to_group', prev_col_group) + left_group.visible = true + end end end function Spreadsheet:render(dc) local page_top = self.list.page_top - for idx, col in ipairs(self.cols.subviews) do + for _, col in ipairs(self.cols.subviews) do col.subviews.col_list.page_top = page_top end Spreadsheet.super.render(self, dc) end +function Spreadsheet:onInput(keys) + if keys.KEYBOARD_CURSOR_LEFT then + self.left_col = math.max(1, self.left_col - 1) + self:updateLayout() + elseif keys.KEYBOARD_CURSOR_RIGHT then + self.left_col = math.min(#self.cols.subviews, self.left_col + 1) + self:updateLayout() + end + return Spreadsheet.super.onInput(self, keys) +end + ------------------------ -- Manipulator -- @@ -259,8 +349,9 @@ Manipulator = defclass(Manipulator, widgets.Window) Manipulator.ATTRS{ frame_title='Unit Overview and Manipulator', frame={w=110, h=40}, + frame_inset={t=1, l=1, r=1, b=0}, resizable=true, - resize_min={w=70, h=25}, + resize_min={w=70, h=30}, } function Manipulator:init() @@ -268,9 +359,9 @@ function Manipulator:init() widgets.EditField{ view_id='search', frame={l=0, t=0}, + key='FILTER', label_text='Search: ', - on_char=function(ch) return ch:match('[%l -]') end, - on_change=function() self.subviews.sheet:refresh() end, + on_change=function(val) self.subviews.sheet:filter(val) end, }, widgets.Divider{ frame={l=0, r=0, t=2, h=1}, @@ -291,24 +382,63 @@ function Manipulator:init() widgets.Panel{ frame={l=0, r=0, b=0, h=5}, subviews={ - widgets.Label{ + widgets.WrappedLabel{ frame={t=0, l=0}, - text='Use arrow keys to navigate cells.', + text_to_wrap='Use arrow keys or middle click drag to navigate cells. Left click or ENTER to toggle current cell.', }, - widgets.HotkeyLabel{ + widgets.Label{ frame={b=2, l=0}, - label='Sort/reverse sort by current column', + text='Current column:', + }, + widgets.HotkeyLabel{ + frame={b=2, l=17}, + auto_width=true, + label='Sort/reverse sort', key='CUSTOM_SHIFT_S', - on_activate=function() end, -- TODO + on_activate=function() self.subviews.sheet:sort_by_current_row() end, + }, + widgets.HotkeyLabel{ + frame={b=2, l=39}, + auto_width=true, + label='Hide', + key='CUSTOM_SHIFT_H', + on_activate=function() self.subviews.sheet:hide_current_row() end, + }, + widgets.Label{ + frame={b=1, l=0}, + text='Current group:', + }, + widgets.HotkeyLabel{ + frame={b=1, l=17}, + auto_width=true, + label='Next group', + key='CUSTOM_CTRL_T', + on_activate=function() end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=37}, + auto_width=true, + label='Hide', + key='CUSTOM_CTRL_H', + on_activate=function() self.subviews.sheet:hide_current_row() end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=51}, + auto_width=true, + label='Show hidden', + key='CUSTOM_CTRL_W', + on_activate=function() self.subviews.sheet:hide_current_row() end, }, widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, - label='Refresh', -- TODO add warning if citizen list has changed and needs refreshing + label='Refresh', -- TODO: add warning if citizen list has changed and needs refreshing key='CUSTOM_SHIFT_R', - on_activate=function() end, -- TODO + on_activate=function() + self.subviews.sheet:refresh() + self.subviews.sheet:filter(self.subviews.search.text) + end, }, - -- TODO moar hotkeys }, }, } From fd263b537a8aeda028a088496e05fe8a381f9c8f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 10 Apr 2024 18:08:00 -0700 Subject: [PATCH 04/26] beginning infrastructure for sorting --- gui/manipulator.lua | 165 +++++++++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 48 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index a844e8b35f..5c21d85b51 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -8,7 +8,7 @@ local widgets = require("gui.widgets") local CONFIG_FILE = 'dfhack-config/manipulator.json' -local config = json.open(CONFIG_FILE) +config = config or json.open(CONFIG_FILE) ------------------------ -- Column @@ -16,30 +16,21 @@ local config = json.open(CONFIG_FILE) Column = defclass(Column, widgets.Panel) Column.ATTRS{ - label='', - data_fn=DEFAULT_NIL, - count_fn=DEFAULT_NIL, - make_sort_order_fn=DEFAULT_NIL, + idx=DEFAULT_NIL, + label=DEFAULT_NIL, group='', label_inset=0, data_width=4, hidden=DEFAULT_NIL, + shared=DEFAULT_NIL, + data_fn=DEFAULT_NIL, + count_fn=DEFAULT_NIL, + cmp_fn=DEFAULT_NIL, } function Column:init() self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) - if not self.make_sort_order_fn then - self.make_sort_order_fn = function(unit_ids) - local spec = {key=function(choice) return self.data_fn(df.unit.find(choice.unit_id)) end} - return utils.make_sort_order(choices, {spec}) - end - end - - if self.hidden == nil then - self.hidden = safe_index(config.data, 'cols', self.label, 'hidden') - end - self:addviews{ widgets.TextButton{ view_id='col_group', @@ -60,7 +51,7 @@ function Column:init() view_id='col_label', frame={l=self.label_inset, t=4}, label=self.label, - on_activate=function() end, -- TODO: sort by this column + on_activate=self:callback('sort'), }, }, }, @@ -79,28 +70,75 @@ function Column:init() } self.subviews.col_list.scrollbar.visible = false + self.dirty = true end -function Column:set_data(units, visible_unit_ids) - local choices = {} +function Column:sort() + if self.dirty then + self:refresh() + end + if self.shared.sort_idx == self.idx then + self.shared.sort_rev = not self.shared.sort_rev + else + self.shared.sort_idx = self.idx + self.shared.sort_rev = false + end + local spec = {compare=self.cmp_fn, reverse=self.shared.sort_rev, key=function(choice) return choice.data end} + local ordered_col_data = {} + for i, ordered_i in ipairs(self.shared.sort_order) do + end + local sort_order = utils.make_sort_order(ordered_col_data, {spec}) +end + +function Column:get_units() + if self.shared.cache.units then return self.shared.cache.units end + local units = {} + for _, unit_id in ipairs(self.shared.unit_ids) do + local unit = df.unit.find(unit_id) + if unit then + table.insert(units, unit) + else + self.shared.fault = true + end + end + self.shared.cache.units = units + return units +end + +function Column:refresh() + local col_data, choices = {}, {} local current, total = 0, 0 local next_id_idx = 1 - for _, unit in ipairs(units) do - local val = self.count_fn(unit) - if unit.id == visible_unit_ids[next_id_idx] then - local data = self.data_fn(unit) + for _, unit in ipairs(self:get_units()) do + local data = self.data_fn(unit) + local val = self.count_fn(data) + if unit.id == self.shared.filtered_unit_ids[next_id_idx] then + table.insert(col_data, data) table.insert(choices, { - text=(not data or data == 0) and '-' or tostring(data), - unit_id=unit.id, + text=function() + local ordered_data = col_data[self.shared.sort_order[next_id_idx]] + return (not data or data == 0) and '-' or tostring(data) + end, }) current = current + val next_id_idx = next_id_idx + 1 end total = total + val end + + self.col_data = col_data self.subviews.col_current:setText(tostring(current)) self.subviews.col_total:setText(tostring(total)) self.subviews.col_list:setChoices(choices) + + self.dirty = false +end + +function Column:render(dc) + if self.dirty then + self:refresh() + end + Column.super.render(self, dc) end function Column:set_stem_height(h) @@ -119,8 +157,7 @@ DataColumn.ATTRS{ function DataColumn:init() if not self.count_fn then - self.count_fn = function(unit) - local data = self.data_fn(unit) + self.count_fn = function(data) if not data then return 0 end if type(data) == 'number' then return data > 0 and 1 or 0 end return 1 @@ -139,7 +176,7 @@ ToggleColumn.ATTRS{ function ToggleColumn:init() if not self.count_fn then - self.count_fn = function(unit) return self.data_fn(unit) and 1 or 0 end + self.count_fn = function(data) return data and 1 or 0 end end end @@ -151,15 +188,21 @@ Spreadsheet = defclass(Spreadsheet, widgets.Panel) function Spreadsheet:init() self.left_col = 1 + self.dirty = true + + self.shared = {sort_idx=-1, sort_rev=false, cache={}, unit_ids={}, filtered_unit_ids={}, sort_order={}} local cols = widgets.Panel{} self.cols = cols cols:addviews{ ToggleColumn{ + view_id='favorites', + idx=#cols.subviews+1, label='Favorites', data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, group='tags', + shared=self.shared, }, } @@ -168,11 +211,13 @@ function Spreadsheet:init() if caption then cols:addviews{ DataColumn{ + idx=#cols.subviews+1, label=caption, data_fn=function(unit) return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating end, group='skills', + shared=self.shared, } } end @@ -181,11 +226,13 @@ function Spreadsheet:init() for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do cols:addviews{ ToggleColumn{ + idx=#cols.subviews+1, label=wd.name, data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false end, group='work details', + shared=self.shared, } } end @@ -193,12 +240,12 @@ function Spreadsheet:init() self:addviews{ widgets.TextButton{ view_id='left_group', - frame={t=0, l=0, h=1}, + frame={t=1, l=0, h=1}, visible=false, }, widgets.TextButton{ view_id='right_group', - frame={t=0, r=0, h=1}, + frame={t=1, r=0, h=1}, visible=false, }, widgets.Label{ @@ -212,10 +259,12 @@ function Spreadsheet:init() DataColumn{ view_id='name', frame={w=30}, + idx=0, label='Name', label_inset=8, data_fn=dfhack.units.getReadableName, data_width=30, + shared=self.shared, }, cols, } @@ -230,16 +279,7 @@ function Spreadsheet:init() } self.list.scrollbar = self.subviews.scrollbar - self:refresh() -end - --- TODO: apply search and filtering -function Spreadsheet:get_visible_unit_ids(units) - local visible_unit_ids = {} - for _, unit in ipairs(units) do - table.insert(visible_unit_ids, unit.id) - end - return visible_unit_ids + self:update_headers() end function Spreadsheet:sort_by_current_row() @@ -261,22 +301,46 @@ function Spreadsheet:jump_to_group(group) self:updateLayout() end -function Spreadsheet:refresh() - local units = dfhack.units.getCitizens() - local visible_unit_ids = self:get_visible_unit_ids(units) - --local sort_order = self.subviews.name.sort_order or self.subviews.name.make_sort_order_fn(visible_unit_ids) +function Spreadsheet:update_headers() local ord = 1 - self.subviews.name:set_data(units, visible_unit_ids) for _, col in ipairs(self.cols.subviews) do - col:set_data(units, visible_unit_ids) if not col.hidden then col:set_stem_height((5-ord)%5) ord = ord + 1 end end - if (self.frame_parent_rect) then - self:updateLayout() +end + +-- TODO: apply search and filtering +function Spreadsheet:get_visible_units(units) + local visible_units, visible_unit_ids = {}, {} + for _, unit in ipairs(units) do + table.insert(visible_units, unit) + table.insert(visible_unit_ids, unit.id) + end + return visible_units, visible_unit_ids +end + +function Spreadsheet:refresh() + self.shared.fault = false + self.subviews.name.dirty = true + for _, col in ipairs(self.cols.subviews) do + col.dirty = true + end + local units = dfhack.units.getCitizens() + self.shared.cache.units = units + self.shared.cache.visible_units, self.shared.visible_unit_ids = self:get_visible_units(units) + local sort_idx = self.shared.sort_idx + self.shared.sort_rev = not self.shared.sort_rev + if sort_idx == -1 then + self.subviews.name:sort() + self.subviews.favorites:sort() + elseif sort_idx == 0 then + self.subviews.name:sort() + else + self.cols.subviews[sort_idx]:sort() end + self.dirty = false end function Spreadsheet:update_col_layout(idx, col, width, group, max_width) @@ -323,11 +387,16 @@ function Spreadsheet:preUpdateLayout(parent_rect) end function Spreadsheet:render(dc) + if self.dirty or self.shared.fault then + self:refresh() + self:updateLayout() + end local page_top = self.list.page_top for _, col in ipairs(self.cols.subviews) do col.subviews.col_list.page_top = page_top end Spreadsheet.super.render(self, dc) + self.shared.cache = {} end function Spreadsheet:onInput(keys) From 71cd34fd7da64e35273bddf1627606da5d8f1313 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 10 May 2024 18:28:55 -0700 Subject: [PATCH 05/26] implement refresh hint --- gui/manipulator.lua | 149 +++++++++++++++++++++++++++---- internal/manipulator/presets.lua | 10 +++ 2 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 internal/manipulator/presets.lua diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 5c21d85b51..0df0cd8498 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -3,12 +3,69 @@ local gui = require("gui") local json = require('json') local overlay = require('plugins.overlay') +local presets = reqscript('internal/manipulator/presets') local utils = require('utils') local widgets = require("gui.widgets") +------------------------ +-- persistent state +-- + +local GLOBAL_KEY = 'manipulator' local CONFIG_FILE = 'dfhack-config/manipulator.json' -config = config or json.open(CONFIG_FILE) +-- persistent player (global) state schema +local function get_default_config() + return { + tags={}, + presets={}, + } +end + +-- persistent per-fort state schema +local function get_default_state() + return { + favorites={}, + tagged={}, + } +end + +-- preset schema +local function get_default_preset() + return { + hidden_groups={}, + hidden_cols={}, + pinned={}, + } +end + +local function get_config() + local data = get_default_config() + local cfg = json.open(CONFIG_FILE) + utils.assign(data, cfg.data) + cfg.data = data + return cfg +end + +config = config or get_config() +state = state or get_default_state() +preset = preset or get_default_preset() + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state = get_default_state() + return + end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) +end ------------------------ -- Column @@ -185,6 +242,9 @@ end -- Spreadsheet = defclass(Spreadsheet, widgets.Panel) +Spreadsheet.ATTRS{ + get_units_fn=DEFAULT_NIL, +} function Spreadsheet:init() self.left_col = 1 @@ -403,9 +463,15 @@ function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then self.left_col = math.max(1, self.left_col - 1) self:updateLayout() + elseif keys.KEYBOARD_CURSOR_LEFT_FAST then + self.left_col = math.max(1, self.left_col - 10) + self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT then self.left_col = math.min(#self.cols.subviews, self.left_col + 1) self:updateLayout() + elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then + self.left_col = math.min(#self.cols.subviews, self.left_col + 10) + self:updateLayout() end return Spreadsheet.super.onInput(self, keys) end @@ -414,6 +480,8 @@ end -- Manipulator -- +local REFRESH_MS = 1000 + Manipulator = defclass(Manipulator, widgets.Window) Manipulator.ATTRS{ frame_title='Unit Overview and Manipulator', @@ -424,6 +492,17 @@ Manipulator.ATTRS{ } function Manipulator:init() + if dfhack.world.isFortressMode() then + self.get_units_fn = dfhack.units.getCitizens + elseif dfhack.world.isAdventureMode() then + self.get_units_fn = qerror('get party members') + else + self.get_units_fn = function() return utils.clone(df.global.world.units.active) end + end + + self.needs_refresh, self.prev_unit_count, self.prev_last_unit_id = false, 0, -1 + self:update_needs_refresh(true) + self:addviews{ widgets.EditField{ view_id='search', @@ -441,6 +520,7 @@ function Manipulator:init() Spreadsheet{ view_id='sheet', frame={l=0, t=3, r=0, b=7}, + get_units_fn=self.get_units_fn, }, widgets.Divider{ frame={l=0, r=0, b=6, h=1}, @@ -464,14 +544,14 @@ function Manipulator:init() auto_width=true, label='Sort/reverse sort', key='CUSTOM_SHIFT_S', - on_activate=function() self.subviews.sheet:sort_by_current_row() end, + on_activate=function() self.subviews.sheet:sort_by_current_col() end, }, widgets.HotkeyLabel{ frame={b=2, l=39}, auto_width=true, label='Hide', key='CUSTOM_SHIFT_H', - on_activate=function() self.subviews.sheet:hide_current_row() end, + on_activate=function() self.subviews.sheet:hide_current_col() end, }, widgets.Label{ frame={b=1, l=0}, @@ -480,28 +560,33 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=1, l=17}, auto_width=true, - label='Next group', - key='CUSTOM_CTRL_T', - on_activate=function() end, + label='Prev group', + key='CUSTOM_CTRL_Y', + on_activate=function() self.subviews.sheet:zoom_to_prev_group() end, }, widgets.HotkeyLabel{ frame={b=1, l=37}, auto_width=true, - label='Hide', - key='CUSTOM_CTRL_H', - on_activate=function() self.subviews.sheet:hide_current_row() end, + label='Next group', + key='CUSTOM_CTRL_T', + on_activate=function() self.subviews.sheet:zoom_to_next_group() end, }, widgets.HotkeyLabel{ - frame={b=1, l=51}, + frame={b=1, l=54}, auto_width=true, - label='Show hidden', - key='CUSTOM_CTRL_W', - on_activate=function() self.subviews.sheet:hide_current_row() end, + label='Hide', + key='CUSTOM_CTRL_H', + on_activate=function() self.subviews.sheet:hide_current_col_group() end, }, widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, - label='Refresh', -- TODO: add warning if citizen list has changed and needs refreshing + label=function() + return self.needs_refresh and 'Refresh (unit list has changed)' or 'Refresh' + end, + text_pen=function() + return self.needs_refresh and COLOR_LIGHTRED or nil + end, key='CUSTOM_SHIFT_R', on_activate=function() self.subviews.sheet:refresh() @@ -513,6 +598,36 @@ function Manipulator:init() } end +function Manipulator:update_needs_refresh(initialize) + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + + local units = self.get_units_fn() + local unit_count = #units + if unit_count ~= self.prev_unit_count then + self.needs_refresh = true + self.prev_unit_count = unit_count + end + if unit_count <= 0 then + self.prev_last_unit_id = -1 + else + local last_unit_id = units[#units] + if last_unit_id ~= self.prev_last_unit_id then + self.needs_refresh = true + self.prev_last_unit_id = last_unit_id + end + end + if initialize then + self.needs_refresh = false + end +end + +function Manipulator:render(dc) + if self.next_refresh_ms <= dfhack.getTickCount() then + self:update_needs_refresh() + end + Manipulator.super.render(self, dc) +end + ------------------------ -- ManipulatorScreen -- @@ -537,7 +652,7 @@ end ManipulatorOverlay = defclass(ManipulatorOverlay, overlay.OverlayWidget) ManipulatorOverlay.ATTRS{ desc='Adds a hotkey to the vanilla units screen to launch the DFHack units interface.', - default_pos={x=50, y=-5}, + default_pos={x=50, y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/CREATURES/CITIZEN', frame={w=34, h=1}, @@ -560,8 +675,8 @@ OVERLAY_WIDGETS = { if dfhack_flags.module then return end -if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then - qerror("This script requires a fortress map to be loaded") +if not dfhack.isMapLoaded() then + qerror("This script requires a map to be loaded") end view = view and view:raise() or ManipulatorScreen{}:show() diff --git a/internal/manipulator/presets.lua b/internal/manipulator/presets.lua new file mode 100644 index 0000000000..757e625cda --- /dev/null +++ b/internal/manipulator/presets.lua @@ -0,0 +1,10 @@ +--@module=true + +PRESETS = { + { + name='', + groups={ + + }, + }, +} \ No newline at end of file From be4f488ed9fb8b3d2fe444cdef4dcd1dbb84a339 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 May 2024 15:14:24 -0700 Subject: [PATCH 06/26] shift scan by horizontal pages --- gui/manipulator.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 0df0cd8498..6237f21d5d 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -459,18 +459,28 @@ function Spreadsheet:render(dc) self.shared.cache = {} end +function Spreadsheet:get_num_visible_cols() + local count = 0 + for _,col in ipairs(self.cols.subviews) do + if col.visible then + count = count + 1 + end + end + return count +end + function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then self.left_col = math.max(1, self.left_col - 1) self:updateLayout() elseif keys.KEYBOARD_CURSOR_LEFT_FAST then - self.left_col = math.max(1, self.left_col - 10) + self.left_col = math.max(1, self.left_col - self:get_num_visible_cols()) self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT then self.left_col = math.min(#self.cols.subviews, self.left_col + 1) self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then - self.left_col = math.min(#self.cols.subviews, self.left_col + 10) + self.left_col = math.min(#self.cols.subviews, self.left_col + self:get_num_visible_cols()) self:updateLayout() end return Spreadsheet.super.onInput(self, keys) From 76777c18129bf6fadddb9ef2881efe4507d6ea72 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 May 2024 23:29:12 -0700 Subject: [PATCH 07/26] get sorting working (i think) --- gui/manipulator.lua | 150 +++++++++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 37 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 6237f21d5d..ba4d33f25e 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -108,7 +108,7 @@ function Column:init() view_id='col_label', frame={l=self.label_inset, t=4}, label=self.label, - on_activate=self:callback('sort'), + on_activate=self:callback('sort', true), }, }, }, @@ -127,24 +127,52 @@ function Column:init() } self.subviews.col_list.scrollbar.visible = false + self.col_data = {} self.dirty = true end -function Column:sort() +function Column:sort(make_primary) if self.dirty then self:refresh() end - if self.shared.sort_idx == self.idx then - self.shared.sort_rev = not self.shared.sort_rev - else - self.shared.sort_idx = self.idx - self.shared.sort_rev = false + local sort_stack = self.shared.sort_stack + if make_primary then + -- we are newly sorting by this column: reverse sort if we're already on top of the + -- stack; otherwise put us on top of the stack + local top = sort_stack[#sort_stack] + if top.col == self then + top.rev = not top.rev + else + for idx,sort_spec in ipairs(sort_stack) do + if sort_spec.col == self then + table.remove(sort_stack, idx) + break + end + end + table.insert(sort_stack, {col=self, rev=false}) + end end - local spec = {compare=self.cmp_fn, reverse=self.shared.sort_rev, key=function(choice) return choice.data end} - local ordered_col_data = {} - for i, ordered_i in ipairs(self.shared.sort_order) do + local compare = function(a, b) + for idx=#sort_stack,1,-1 do + local sort_spec = sort_stack[idx] + local col = sort_spec.col + local first, second + if sort_spec.rev then + first, second = col.col_data[b], col.col_data[a] + else + first, second = col.col_data[a], col.col_data[b] + end + if first == second then goto continue end + if not first then return 1 end + if not second then return -1 end + local ret = (col.cmp_fn or utils.compare)(first, second) + if ret ~= 0 then return ret end + ::continue:: + end + return 0 end - local sort_order = utils.make_sort_order(ordered_col_data, {spec}) + local spec = {compare=compare} + self.shared.sort_order = utils.make_sort_order(self.shared.sort_order, {spec}) end function Column:get_units() @@ -172,10 +200,12 @@ function Column:refresh() if unit.id == self.shared.filtered_unit_ids[next_id_idx] then table.insert(col_data, data) table.insert(choices, { - text=function() - local ordered_data = col_data[self.shared.sort_order[next_id_idx]] - return (not data or data == 0) and '-' or tostring(data) - end, + { + text=function() + local ordered_data = col_data[self.shared.sort_order[next_id_idx]] + return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) + end, + }, }) current = current + val next_id_idx = next_id_idx + 1 @@ -250,7 +280,13 @@ function Spreadsheet:init() self.left_col = 1 self.dirty = true - self.shared = {sort_idx=-1, sort_rev=false, cache={}, unit_ids={}, filtered_unit_ids={}, sort_order={}} + self.shared = { + unit_ids={}, + filtered_unit_ids={}, + sort_stack={}, + sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) + cache={}, -- cached pointers; reset at end of frame + } local cols = widgets.Panel{} self.cols = cols @@ -258,12 +294,17 @@ function Spreadsheet:init() cols:addviews{ ToggleColumn{ view_id='favorites', - idx=#cols.subviews+1, label='Favorites', data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, group='tags', shared=self.shared, }, + DataColumn{ + label='Stress', + data_fn=function(unit) return unit.status.current_soul.personality.stress end, + group='summary', + shared=self.shared, + } } for i in ipairs(df.job_skill) do @@ -271,7 +312,6 @@ function Spreadsheet:init() if caption then cols:addviews{ DataColumn{ - idx=#cols.subviews+1, label=caption, data_fn=function(unit) return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating @@ -286,7 +326,6 @@ function Spreadsheet:init() for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do cols:addviews{ ToggleColumn{ - idx=#cols.subviews+1, label=wd.name, data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false @@ -297,6 +336,31 @@ function Spreadsheet:init() } end + for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do + cols:addviews{ + ToggleColumn{ + label=workshop.name, + data_fn=function(unit) + return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + end, + group='workshops', + shared=self.shared, + } + } + end + for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do + cols:addviews{ + ToggleColumn{ + label=workshop.name, + data_fn=function(unit) + return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + end, + group='workshops', + shared=self.shared, + } + } + end + self:addviews{ widgets.TextButton{ view_id='left_group', @@ -319,7 +383,6 @@ function Spreadsheet:init() DataColumn{ view_id='name', frame={w=30}, - idx=0, label='Name', label_inset=8, data_fn=dfhack.units.getReadableName, @@ -329,6 +392,10 @@ function Spreadsheet:init() cols, } + -- set up initial sort: primary favorites, secondary name + self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} + self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} + self.list = self.subviews.name.subviews.col_list self:addviews{ widgets.Scrollbar{ @@ -342,13 +409,28 @@ function Spreadsheet:init() self:update_headers() end -function Spreadsheet:sort_by_current_row() +function Spreadsheet:sort_by_current_col() + -- TODO end function Spreadsheet:filter(search) + -- TODO end -function Spreadsheet:hide_current_row() +function Spreadsheet:zoom_to_prev_group() + -- TODO +end + +function Spreadsheet:zoom_to_next_group() + -- TODO +end + +function Spreadsheet:hide_current_col() + -- TODO +end + +function Spreadsheet:hide_current_col_group() + -- TODO end function Spreadsheet:jump_to_group(group) @@ -382,24 +464,18 @@ function Spreadsheet:get_visible_units(units) end function Spreadsheet:refresh() - self.shared.fault = false + local shared = self.shared + local cache = shared.cache + shared.fault = false self.subviews.name.dirty = true for _, col in ipairs(self.cols.subviews) do col.dirty = true end - local units = dfhack.units.getCitizens() - self.shared.cache.units = units - self.shared.cache.visible_units, self.shared.visible_unit_ids = self:get_visible_units(units) - local sort_idx = self.shared.sort_idx - self.shared.sort_rev = not self.shared.sort_rev - if sort_idx == -1 then - self.subviews.name:sort() - self.subviews.favorites:sort() - elseif sort_idx == 0 then - self.subviews.name:sort() - else - self.cols.subviews[sort_idx]:sort() - end + local units = self.get_units_fn() + cache.units = units + cache.visible_units, shared.filtered_unit_ids = self:get_visible_units(units) + shared.sort_order = utils.tabulate(function(i) return i end, 1, #shared.filtered_unit_ids) + shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end @@ -582,7 +658,7 @@ function Manipulator:init() on_activate=function() self.subviews.sheet:zoom_to_next_group() end, }, widgets.HotkeyLabel{ - frame={b=1, l=54}, + frame={b=1, l=57}, auto_width=true, label='Hide', key='CUSTOM_CTRL_H', From 8386b22bb8661bd54aa55d601923b41650a1f037 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 00:27:51 -0700 Subject: [PATCH 08/26] actually get sorting working --- gui/manipulator.lua | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index ba4d33f25e..fc97fd09d3 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -132,9 +132,6 @@ function Column:init() end function Column:sort(make_primary) - if self.dirty then - self:refresh() - end local sort_stack = self.shared.sort_stack if make_primary then -- we are newly sorting by this column: reverse sort if we're already on top of the @@ -152,6 +149,12 @@ function Column:sort(make_primary) table.insert(sort_stack, {col=self, rev=false}) end end + for _,sort_spec in ipairs(sort_stack) do + local col = sort_spec.col + if col.dirty then + col:refresh() + end + end local compare = function(a, b) for idx=#sort_stack,1,-1 do local sort_spec = sort_stack[idx] @@ -171,8 +174,9 @@ function Column:sort(make_primary) end return 0 end + local order = utils.tabulate(function(i) return i end, 1, #self.shared.filtered_unit_ids) local spec = {compare=compare} - self.shared.sort_order = utils.make_sort_order(self.shared.sort_order, {spec}) + self.shared.sort_order = utils.make_sort_order(order, {spec}) end function Column:get_units() @@ -198,13 +202,16 @@ function Column:refresh() local data = self.data_fn(unit) local val = self.count_fn(data) if unit.id == self.shared.filtered_unit_ids[next_id_idx] then + local idx = next_id_idx table.insert(col_data, data) table.insert(choices, { - { - text=function() - local ordered_data = col_data[self.shared.sort_order[next_id_idx]] - return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) - end, + text={ + { + text=function() + local ordered_data = col_data[self.shared.sort_order[idx]] + return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) + end, + }, }, }) current = current + val @@ -276,6 +283,18 @@ Spreadsheet.ATTRS{ get_units_fn=DEFAULT_NIL, } +local function get_workshop_label(workshop, type_enum, bld_defs) + if #workshop.name > 0 then + return workshop.name + end + local type_name = type_enum[workshop.type] + if type_name == 'Custom' then + local bld_def = bld_defs[workshop.custom_type] + if bld_def then return bld_def.code end + end + return type_name +end + function Spreadsheet:init() self.left_col = 1 self.dirty = true @@ -339,7 +358,7 @@ function Spreadsheet:init() for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do cols:addviews{ ToggleColumn{ - label=workshop.name, + label=get_workshop_label(workshop, df.furnace_type, df.global.world.raws.buildings.furnaces), data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, @@ -351,7 +370,7 @@ function Spreadsheet:init() for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do cols:addviews{ ToggleColumn{ - label=workshop.name, + label=get_workshop_label(workshop, df.workshop_type, df.global.world.raws.buildings.workshops), data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, @@ -474,7 +493,6 @@ function Spreadsheet:refresh() local units = self.get_units_fn() cache.units = units cache.visible_units, shared.filtered_unit_ids = self:get_visible_units(units) - shared.sort_order = utils.tabulate(function(i) return i end, 1, #shared.filtered_unit_ids) shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end @@ -671,7 +689,7 @@ function Manipulator:init() return self.needs_refresh and 'Refresh (unit list has changed)' or 'Refresh' end, text_pen=function() - return self.needs_refresh and COLOR_LIGHTRED or nil + return self.needs_refresh and COLOR_LIGHTRED or COLOR_GRAY end, key='CUSTOM_SHIFT_R', on_activate=function() From f570c74fc2ed0c5c6547aee78f457c24dca0973d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 00:51:25 -0700 Subject: [PATCH 09/26] fix rendering and sort order issues --- gui/manipulator.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index fc97fd09d3..8e0b010ff9 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -115,10 +115,12 @@ function Column:init() widgets.Label{ view_id='col_current', frame={t=7, l=1+self.label_inset, w=4}, + auto_height=false, }, widgets.Label{ view_id='col_total', frame={t=8, l=1+self.label_inset, w=4}, + auto_height=false, }, widgets.List{ view_id='col_list', @@ -257,6 +259,12 @@ function DataColumn:init() return 1 end end + if not self.cmp_fn then + self.cmp_fn = function(a, b) + if type(a) == 'number' then return -utils.compare(a, b) end + return utils.compare(a, b) + end + end end ------------------------ @@ -473,13 +481,13 @@ function Spreadsheet:update_headers() end -- TODO: apply search and filtering -function Spreadsheet:get_visible_units(units) - local visible_units, visible_unit_ids = {}, {} +function Spreadsheet:filter_units(units) + local unit_ids, filtered_unit_ids = {}, {} for _, unit in ipairs(units) do - table.insert(visible_units, unit) - table.insert(visible_unit_ids, unit.id) + table.insert(unit_ids, unit.id) + table.insert(filtered_unit_ids, unit.id) end - return visible_units, visible_unit_ids + return unit_ids, filtered_unit_ids end function Spreadsheet:refresh() @@ -492,7 +500,7 @@ function Spreadsheet:refresh() end local units = self.get_units_fn() cache.units = units - cache.visible_units, shared.filtered_unit_ids = self:get_visible_units(units) + shared.unit_ids, shared.filtered_unit_ids = self:filter_units(units) shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end From d846f4ac3f9fad02438d02a5f7345a619cea584d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 02:24:16 -0700 Subject: [PATCH 10/26] render toggle buttons for toggle columns and format stress so it fits in 4 characters --- gui/manipulator.lua | 153 +++++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 44 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 8e0b010ff9..2ca8d0ffe3 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -4,6 +4,7 @@ local gui = require("gui") local json = require('json') local overlay = require('plugins.overlay') local presets = reqscript('internal/manipulator/presets') +local textures = require('gui.textures') local utils = require('utils') local widgets = require("gui.widgets") @@ -73,7 +74,6 @@ end Column = defclass(Column, widgets.Panel) Column.ATTRS{ - idx=DEFAULT_NIL, label=DEFAULT_NIL, group='', label_inset=0, @@ -83,6 +83,8 @@ Column.ATTRS{ data_fn=DEFAULT_NIL, count_fn=DEFAULT_NIL, cmp_fn=DEFAULT_NIL, + choice_fn=DEFAULT_NIL, + on_select=DEFAULT_NIL, } function Column:init() @@ -125,6 +127,7 @@ function Column:init() widgets.List{ view_id='col_list', frame={t=10, l=0, w=self.data_width}, + on_select=self.on_select, }, } @@ -206,16 +209,7 @@ function Column:refresh() if unit.id == self.shared.filtered_unit_ids[next_id_idx] then local idx = next_id_idx table.insert(col_data, data) - table.insert(choices, { - text={ - { - text=function() - local ordered_data = col_data[self.shared.sort_order[idx]] - return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) - end, - }, - }, - }) + table.insert(choices, self.choice_fn(function() return col_data[self.shared.sort_order[idx]] end)) current = current + val next_id_idx = next_id_idx + 1 end @@ -247,41 +241,91 @@ end -- DataColumn -- +local function data_cmp(a, b) + if type(a) == 'number' then return -utils.compare(a, b) end + return utils.compare(a, b) +end + +local function data_count(data) + if not data then return 0 end + if type(data) == 'number' then return data > 0 and 1 or 0 end + return 1 +end + +local function data_choice(get_ordered_data_fn) + return { + text={ + { + text=function() + local ordered_data = get_ordered_data_fn() + return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) + end, + }, + }, + } +end + DataColumn = defclass(DataColumn, Column) DataColumn.ATTRS{ + cmp_fn=data_cmp, + count_fn=data_count, + choice_fn=data_choice, } -function DataColumn:init() - if not self.count_fn then - self.count_fn = function(data) - if not data then return 0 end - if type(data) == 'number' then return data > 0 and 1 or 0 end - return 1 - end - end - if not self.cmp_fn then - self.cmp_fn = function(a, b) - if type(a) == 'number' then return -utils.compare(a, b) end - return utils.compare(a, b) - end - end -end - ------------------------ -- ToggleColumn -- +local ENABLED_PEN_LEFT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} +local ENABLED_PEN_CENTER = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check +local ENABLED_PEN_RIGHT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} +local DISABLED_PEN_LEFT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} +local DISABLED_PEN_CENTER = dfhack.pen.parse{fg=COLOR_RED, + tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} +local DISABLED_PEN_RIGHT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} + +local function toggle_count(data) + return data and 1 or 0 +end + +local function toggle_choice(get_ordered_data_fn) + local function get_enabled_button_token(enabled_tile, disabled_tile) + return { + tile=function() return get_ordered_data_fn() and enabled_tile or disabled_tile end, + } + end + return { + text={ + get_enabled_button_token(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT), + get_enabled_button_token(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER), + get_enabled_button_token(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT), + }, + } +end + ToggleColumn = defclass(ToggleColumn, Column) ToggleColumn.ATTRS{ - on_toggle=DEFAULT_NIL, + count_fn=toggle_count, + choice_fn=toggle_choice, } -function ToggleColumn:init() - if not self.count_fn then - self.count_fn = function(data) return data and 1 or 0 end - end +------------------------ +-- TagColumn +-- + +local function tag_select(_, choice) end +TagColumn = defclass(TagColumn, ToggleColumn) +TagColumn.ATTRS{ + on_select=tag_select, +} + ------------------------ -- Spreadsheet -- @@ -321,16 +365,37 @@ function Spreadsheet:init() cols:addviews{ ToggleColumn{ view_id='favorites', - label='Favorites', - data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, group='tags', + label='Favorites', shared=self.shared, + data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, }, DataColumn{ - label='Stress', - data_fn=function(unit) return unit.status.current_soul.personality.stress end, group='summary', + label='Stress', shared=self.shared, + data_fn=function(unit) return unit.status.current_soul.personality.stress end, + choice_fn=function(get_ordered_data_fn) + return { + text={ + { + text=function() + local ordered_data = get_ordered_data_fn() + if ordered_data > 99999 then + return '>99k' + elseif ordered_data > 9999 then + return ('%3dk'):format(ordered_data // 1000) + elseif ordered_data < -99999 then + return ' -' .. string.char(236) -- -∞ + elseif ordered_data < -999 then + return ('%3dk'):format(-(-ordered_data // 1000)) + end + return tostring(ordered_data) + end, + }, + }, + } + end, } } @@ -339,12 +404,12 @@ function Spreadsheet:init() if caption then cols:addviews{ DataColumn{ + group='skills', label=caption, + shared=self.shared, data_fn=function(unit) return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating end, - group='skills', - shared=self.shared, } } end @@ -353,12 +418,12 @@ function Spreadsheet:init() for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do cols:addviews{ ToggleColumn{ + group='work details', label=wd.name, + shared=self.shared, data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false end, - group='work details', - shared=self.shared, } } end @@ -366,24 +431,24 @@ function Spreadsheet:init() for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do cols:addviews{ ToggleColumn{ + group='workshops', label=get_workshop_label(workshop, df.furnace_type, df.global.world.raws.buildings.furnaces), + shared=self.shared, data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, - group='workshops', - shared=self.shared, } } end for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do cols:addviews{ ToggleColumn{ + group='workshops', label=get_workshop_label(workshop, df.workshop_type, df.global.world.raws.buildings.workshops), + shared=self.shared, data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, - group='workshops', - shared=self.shared, } } end From 1af885788e3bb44741904efb1a5b5e05a279e3d3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 17:06:07 -0700 Subject: [PATCH 11/26] implement favorites toggling and stress colors --- gui/manipulator.lua | 61 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 2ca8d0ffe3..bad0a64cd5 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -84,7 +84,6 @@ Column.ATTRS{ count_fn=DEFAULT_NIL, cmp_fn=DEFAULT_NIL, choice_fn=DEFAULT_NIL, - on_select=DEFAULT_NIL, } function Column:init() @@ -127,7 +126,7 @@ function Column:init() widgets.List{ view_id='col_list', frame={t=10, l=0, w=self.data_width}, - on_select=self.on_select, + on_submit=self:callback('on_select'), }, } @@ -136,6 +135,10 @@ function Column:init() self.dirty = true end +-- overridden by subclasses +function Column:on_select(idx, choice) +end + function Column:sort(make_primary) local sort_stack = self.shared.sort_stack if make_primary then @@ -199,6 +202,14 @@ function Column:get_units() return units end +function Column:get_sorted_unit_id(idx) + return self.shared.filtered_unit_ids[self.shared.sort_order[idx]] +end + +function Column:get_sorted_data(idx) + return self.col_data[self.shared.sort_order[idx]] +end + function Column:refresh() local col_data, choices = {}, {} local current, total = 0, 0 @@ -209,7 +220,7 @@ function Column:refresh() if unit.id == self.shared.filtered_unit_ids[next_id_idx] then local idx = next_id_idx table.insert(col_data, data) - table.insert(choices, self.choice_fn(function() return col_data[self.shared.sort_order[idx]] end)) + table.insert(choices, self.choice_fn(function() return self:get_sorted_data(idx) end)) current = current + val next_id_idx = next_id_idx + 1 end @@ -312,20 +323,18 @@ ToggleColumn = defclass(ToggleColumn, Column) ToggleColumn.ATTRS{ count_fn=toggle_count, choice_fn=toggle_choice, + toggle_fn=DEFAULT_NIL, } ------------------------- --- TagColumn --- - -local function tag_select(_, choice) +function ToggleColumn:on_select(idx, choice) + if not self.toggle_fn then return end + local unit_id = self:get_sorted_unit_id(idx) + local prev_val = self:get_sorted_data(idx) + print(idx, unit_id, dfhack.units.getReadableName(df.unit.find(unit_id)), prev_val) + self.toggle_fn(unit_id, prev_val) + self.dirty = true end -TagColumn = defclass(TagColumn, ToggleColumn) -TagColumn.ATTRS{ - on_select=tag_select, -} - ------------------------ -- Spreadsheet -- @@ -368,7 +377,16 @@ function Spreadsheet:init() group='tags', label='Favorites', shared=self.shared, - data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, + data_fn=function(unit) return utils.binsearch(ensure_key(state, 'favorites'), unit.id) and true or false end, + toggle_fn=function(unit_id, prev_val) + local fav_vec = ensure_key(state, 'favorites') + if prev_val then + utils.erase_sorted(fav_vec, unit_id) + else + utils.insert_sorted(fav_vec, unit_id) + end + persist_state() + end, }, DataColumn{ group='summary', @@ -390,7 +408,20 @@ function Spreadsheet:init() elseif ordered_data < -999 then return ('%3dk'):format(-(-ordered_data // 1000)) end - return tostring(ordered_data) + return ('%4d'):format(ordered_data) + end, + pen=function() + local ordered_data = get_ordered_data_fn() + local level = dfhack.units.getStressCategoryRaw(ordered_data) + local is_graphics = dfhack.screen.inGraphicsMode() + -- match colors of stress faces depending on mode + if level == 0 then return COLOR_RED end + if level == 1 then return COLOR_LIGHTRED end + if level == 2 then return is_graphics and COLOR_BROWN or COLOR_YELLOW end + if level == 3 then return is_graphics and COLOR_YELLOW or COLOR_WHITE end + if level == 4 then return is_graphics and COLOR_CYAN or COLOR_GREEN end + if level == 5 then return is_graphics and COLOR_GREEN or COLOR_LIGHTGREEN end + return is_graphics and COLOR_LIGHTGREEN or COLOR_LIGHTCYAN end, }, }, From beef160837aa2c14af037334cfaf7d3cb6e187fd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 17:30:55 -0700 Subject: [PATCH 12/26] implement toggling for workshops and work details --- gui/manipulator.lua | 57 ++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index bad0a64cd5..81c5b2c369 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -330,7 +330,6 @@ function ToggleColumn:on_select(idx, choice) if not self.toggle_fn then return end local unit_id = self:get_sorted_unit_id(idx) local prev_val = self:get_sorted_data(idx) - print(idx, unit_id, dfhack.units.getReadableName(df.unit.find(unit_id)), prev_val) self.toggle_fn(unit_id, prev_val) self.dirty = true end @@ -446,7 +445,8 @@ function Spreadsheet:init() end end - for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do + local work_details = df.global.plotinfo.labor_info.work_details + for _, wd in ipairs(work_details) do cols:addviews{ ToggleColumn{ group='work details', @@ -455,34 +455,43 @@ function Spreadsheet:init() data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false end, - } - } - end - - for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do - cols:addviews{ - ToggleColumn{ - group='workshops', - label=get_workshop_label(workshop, df.furnace_type, df.global.world.raws.buildings.furnaces), - shared=self.shared, - data_fn=function(unit) - return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + toggle_fn=function(unit_id, prev_val) + -- TODO: poke DF to actually apply the work details to units + if prev_val then + utils.erase_sorted(wd.assigned_units, unit_id) + else + utils.insert_sorted(wd.assigned_units, unit_id) + end end, } } end - for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do - cols:addviews{ - ToggleColumn{ - group='workshops', - label=get_workshop_label(workshop, df.workshop_type, df.global.world.raws.buildings.workshops), - shared=self.shared, - data_fn=function(unit) - return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false - end, + + local function add_workshops(vec, type_enum, type_defs) + for _, workshop in ipairs(vec) do + cols:addviews{ + ToggleColumn{ + group='workshops', + label=get_workshop_label(workshop, type_enum, type_defs), + shared=self.shared, + data_fn=function(unit) + return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + end, + toggle_fn=function(unit_id, prev_val) + if prev_val then + utils.erase_sorted(workshop.profile.permitted_workers, unit_id) + else + -- there can be only one + workshop.profile.permitted_workers:resize(0) + workshop.profile.permitted_workers:insert('#', unit_id) + end + end, + } } - } + end end + add_workshops(df.global.world.buildings.other.FURNACE_ANY, df.furnace_type, df.global.world.raws.buildings.furnaces) + add_workshops(df.global.world.buildings.other.WORKSHOP_ANY, df.workshop_type, df.global.world.raws.buildings.workshops) self:addviews{ widgets.TextButton{ From b77babfdfaee73c204cb073127306647a277695b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 17:34:55 -0700 Subject: [PATCH 13/26] add skeleton docs --- docs/gui/manipulator.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/gui/manipulator.rst diff --git a/docs/gui/manipulator.rst b/docs/gui/manipulator.rst new file mode 100644 index 0000000000..0cdbd8990e --- /dev/null +++ b/docs/gui/manipulator.rst @@ -0,0 +1,15 @@ +gui/manipulator +=============== + +.. dfhack-tool:: + :summary: Multi-function unit management interface. + :tags: fort productivity units + +This spreadsheet-like UI allows you to see all your units, their skills, properties, assignments, etc. at a glance. You can search, sort, and filter to see exactly the information that you're looking for. Moreover, you can assign units to burrows, squads, work details, and workshops. + +Usage +----- + +:: + + gui/manipulator From cc79310063a2fca2aaeae875fd509ee3b6ee5168 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 18:00:36 -0700 Subject: [PATCH 14/26] fix list width, factor out sorted vector logic --- gui/manipulator.lua | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 81c5b2c369..3cff2d1812 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -125,7 +125,7 @@ function Column:init() }, widgets.List{ view_id='col_list', - frame={t=10, l=0, w=self.data_width}, + frame={t=10, l=0, w=self.data_width+2}, -- +2 for the invisible scrollbar on_submit=self:callback('on_select'), }, } @@ -319,6 +319,18 @@ local function toggle_choice(get_ordered_data_fn) } end +local function toggle_sorted_vec_data(vec, unit) + return utils.binsearch(vec, unit.id) and true or false +end + +local function toggle_sorted_vec(vec, unit_id, prev_val) + if prev_val then + utils.erase_sorted(vec, unit_id) + else + utils.insert_sorted(vec, unit_id) + end +end + ToggleColumn = defclass(ToggleColumn, Column) ToggleColumn.ATTRS{ count_fn=toggle_count, @@ -376,14 +388,9 @@ function Spreadsheet:init() group='tags', label='Favorites', shared=self.shared, - data_fn=function(unit) return utils.binsearch(ensure_key(state, 'favorites'), unit.id) and true or false end, + data_fn=curry(toggle_sorted_vec_data, state.favorites), toggle_fn=function(unit_id, prev_val) - local fav_vec = ensure_key(state, 'favorites') - if prev_val then - utils.erase_sorted(fav_vec, unit_id) - else - utils.insert_sorted(fav_vec, unit_id) - end + toggle_sorted_vec(state.favorites, unit_id, prev_val) persist_state() end, }, @@ -452,16 +459,10 @@ function Spreadsheet:init() group='work details', label=wd.name, shared=self.shared, - data_fn=function(unit) - return utils.binsearch(wd.assigned_units, unit.id) and true or false - end, + data_fn=curry(toggle_sorted_vec_data, wd.assigned_units), toggle_fn=function(unit_id, prev_val) + toggle_sorted_vec(wd.assigned_units, unit_id, prev_val) -- TODO: poke DF to actually apply the work details to units - if prev_val then - utils.erase_sorted(wd.assigned_units, unit_id) - else - utils.insert_sorted(wd.assigned_units, unit_id) - end end, } } @@ -474,17 +475,13 @@ function Spreadsheet:init() group='workshops', label=get_workshop_label(workshop, type_enum, type_defs), shared=self.shared, - data_fn=function(unit) - return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false - end, + data_fn=curry(toggle_sorted_vec_data, workshop.profile.permitted_workers), toggle_fn=function(unit_id, prev_val) - if prev_val then - utils.erase_sorted(workshop.profile.permitted_workers, unit_id) - else + if not prev_val then -- there can be only one workshop.profile.permitted_workers:resize(0) - workshop.profile.permitted_workers:insert('#', unit_id) end + toggle_sorted_vec(workshop.profile.permitted_workers, unit_id, prev_val) end, } } From 87507e0cb191cf2d3f8c58e224f2b7b6fcf6af8e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 18:47:36 -0700 Subject: [PATCH 15/26] implement name search --- gui/manipulator.lua | 49 ++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 3cff2d1812..5fc7d2fd6a 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -369,6 +369,7 @@ end function Spreadsheet:init() self.left_col = 1 + self.prev_filter = '' self.dirty = true self.shared = { @@ -542,10 +543,6 @@ function Spreadsheet:sort_by_current_col() -- TODO end -function Spreadsheet:filter(search) - -- TODO -end - function Spreadsheet:zoom_to_prev_group() -- TODO end @@ -582,27 +579,34 @@ function Spreadsheet:update_headers() end end --- TODO: apply search and filtering -function Spreadsheet:filter_units(units) - local unit_ids, filtered_unit_ids = {}, {} - for _, unit in ipairs(units) do - table.insert(unit_ids, unit.id) - table.insert(filtered_unit_ids, unit.id) - end - return unit_ids, filtered_unit_ids -end - -function Spreadsheet:refresh() +-- TODO: support column addressing for searching/filtering (e.g. "skills/Armoring:>10") +function Spreadsheet:refresh(filter, full_refresh) local shared = self.shared - local cache = shared.cache shared.fault = false self.subviews.name.dirty = true for _, col in ipairs(self.cols.subviews) do col.dirty = true end - local units = self.get_units_fn() - cache.units = units - shared.unit_ids, shared.filtered_unit_ids = self:filter_units(units) + local incremental = not full_refresh and self.prev_filter and filter:startswith(self.prev_filter) + if not incremental then + local units = self.get_units_fn() + shared.cache.units = units + shared.unit_ids = utils.tabulate(function(idx) return units[idx].id end, 1, #units) + end + shared.filtered_unit_ids = copyall(shared.unit_ids) + if #filter > 0 then + local col = self.subviews.name + col:refresh() + for idx=#col.col_data,1,-1 do + local data = col.col_data[idx] + if (not utils.search_text(data, filter)) then + table.remove(shared.filtered_unit_ids, idx) + end + end + if #col.col_data ~= #shared.filtered_unit_ids then + col.dirty = true + end + end shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end @@ -652,7 +656,7 @@ end function Spreadsheet:render(dc) if self.dirty or self.shared.fault then - self:refresh() + self:refresh(self.prev_filter, true) self:updateLayout() end local page_top = self.list.page_top @@ -723,7 +727,7 @@ function Manipulator:init() frame={l=0, t=0}, key='FILTER', label_text='Search: ', - on_change=function(val) self.subviews.sheet:filter(val) end, + on_change=function(text) self.subviews.sheet:refresh(text, false) end, }, widgets.Divider{ frame={l=0, r=0, t=2, h=1}, @@ -803,8 +807,7 @@ function Manipulator:init() end, key='CUSTOM_SHIFT_R', on_activate=function() - self.subviews.sheet:refresh() - self.subviews.sheet:filter(self.subviews.search.text) + self.subviews.sheet:refresh(self.subviews.search.text, true) end, }, }, From 529fd42e48e2272846c7e590a6cab354e07d1f67 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 19:34:09 -0700 Subject: [PATCH 16/26] synchronize list row selection and add sort indicator --- gui/manipulator.lua | 63 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 5fc7d2fd6a..98c2fab506 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -86,6 +86,10 @@ Column.ATTRS{ choice_fn=DEFAULT_NIL, } +local CH_DOT = string.char(15) +local CH_UP = string.char(30) +local CH_DN = string.char(31) + function Column:init() self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) @@ -105,11 +109,43 @@ function Column:init() frame_style=gui.FRAME_INTERIOR, frame_style_b=false, }, - widgets.HotkeyLabel{ + widgets.Panel{ view_id='col_label', frame={l=self.label_inset, t=4}, - label=self.label, - on_activate=self:callback('sort', true), + subviews={ + widgets.HotkeyLabel{ + frame={l=0, t=0, w=1}, + label=CH_DN, + text_pen=COLOR_LIGHTGREEN, + visible=function() + local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] + return sort_spec.col == self and not sort_spec.rev + end, + }, + widgets.HotkeyLabel{ + frame={l=0, t=0, w=1}, + label=CH_UP, + text_pen=COLOR_LIGHTGREEN, + visible=function() + local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] + return sort_spec.col == self and sort_spec.rev + end, + }, + widgets.HotkeyLabel{ + frame={l=0, t=0, w=1}, + label=CH_DOT, + text_pen=COLOR_GRAY, + visible=function() + local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] + return sort_spec.col ~= self + end, + }, + widgets.HotkeyLabel{ + frame={l=1, t=0}, + label=self.label, + on_activate=self:callback('sort', true), + }, + }, }, }, }, @@ -135,8 +171,14 @@ function Column:init() self.dirty = true end --- overridden by subclasses +-- extended by subclasses function Column:on_select(idx, choice) + -- conveniently, this will be nil for the namelist column itself, + -- avoiding an infinite loop + local namelist = self.parent_view.parent_view.namelist + if namelist then + namelist:setSelected(idx) + end end function Column:sort(make_primary) @@ -339,6 +381,7 @@ ToggleColumn.ATTRS{ } function ToggleColumn:on_select(idx, choice) + ToggleColumn.super.on_select(self, idx, choice) if not self.toggle_fn then return end local unit_id = self:get_sorted_unit_id(idx) local prev_val = self:get_sorted_data(idx) @@ -526,15 +569,16 @@ function Spreadsheet:init() self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} - self.list = self.subviews.name.subviews.col_list + self.namelist = self.subviews.name.subviews.col_list self:addviews{ widgets.Scrollbar{ view_id='scrollbar', frame={t=7, r=0}, - on_scroll=self.list:callback('on_scrollbar'), + on_scroll=self.namelist:callback('on_scrollbar'), } } - self.list.scrollbar = self.subviews.scrollbar + self.namelist.scrollbar = self.subviews.scrollbar + self.namelist:setFocus(true) self:update_headers() end @@ -659,9 +703,11 @@ function Spreadsheet:render(dc) self:refresh(self.prev_filter, true) self:updateLayout() end - local page_top = self.list.page_top + local page_top = self.namelist.page_top + local selected = self.namelist:getSelected() for _, col in ipairs(self.cols.subviews) do col.subviews.col_list.page_top = page_top + col.subviews.col_list:setSelected(selected) end Spreadsheet.super.render(self, dc) self.shared.cache = {} @@ -728,6 +774,7 @@ function Manipulator:init() key='FILTER', label_text='Search: ', on_change=function(text) self.subviews.sheet:refresh(text, false) end, + on_unfocus=function() self.subviews.sheet.namelist:setFocus(true) end, }, widgets.Divider{ frame={l=0, r=0, t=2, h=1}, From 3e81a8e2deda45aa921cbddb56f6c8b4410807ed Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 21:04:33 -0700 Subject: [PATCH 17/26] implement hiding columns --- gui/manipulator.lua | 99 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 98c2fab506..798b6e21d9 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -68,6 +68,24 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) end +------------------------ +-- ColumnMenu +-- + +ColumnMenu = defclass(ColumnMenu, widgets.Panel) +ColumnMenu.ATTRS{ + frame_style=gui.FRAME_INTERIOR, +} + +function ColumnMenu:init() +end + +function ColumnMenu:show() + self.prev_focus_owner = self.focus_group.cur + self.visible = true +end + + ------------------------ -- Column -- @@ -143,7 +161,7 @@ function Column:init() widgets.HotkeyLabel{ frame={l=1, t=0}, label=self.label, - on_activate=self:callback('sort', true), + on_activate=self:callback('on_click'), }, }, }, @@ -181,6 +199,23 @@ function Column:on_select(idx, choice) end end +function Column:on_click() + local modifiers = dfhack.internal.getModifiers() + if modifiers.shift then + for _,col in ipairs(self.parent_view.subviews) do + if col.group == self.group then + col.hidden = true + end + end + self.shared.refresh_headers = true + elseif modifiers.ctrl then + self.hidden = true + self.shared.refresh_headers = true + else + self:sort(true) + end +end + function Column:sort(make_primary) local sort_stack = self.shared.sort_stack if make_primary then @@ -413,7 +448,6 @@ end function Spreadsheet:init() self.left_col = 1 self.prev_filter = '' - self.dirty = true self.shared = { unit_ids={}, @@ -421,6 +455,8 @@ function Spreadsheet:init() sort_stack={}, sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) cache={}, -- cached pointers; reset at end of frame + refresh_units=true, + refresh_headers=true, } local cols = widgets.Panel{} @@ -579,8 +615,6 @@ function Spreadsheet:init() } self.namelist.scrollbar = self.subviews.scrollbar self.namelist:setFocus(true) - - self:update_headers() end function Spreadsheet:sort_by_current_col() @@ -621,6 +655,7 @@ function Spreadsheet:update_headers() ord = ord + 1 end end + self.shared.refresh_headers = false end -- TODO: support column addressing for searching/filtering (e.g. "skills/Armoring:>10") @@ -652,7 +687,7 @@ function Spreadsheet:refresh(filter, full_refresh) end end shared.sort_stack[#shared.sort_stack].col:sort() - self.dirty = false + self.shared.refresh_units = false end function Spreadsheet:update_col_layout(idx, col, width, group, max_width) @@ -676,6 +711,7 @@ function Spreadsheet:preUpdateLayout(parent_rect) for idx, col in ipairs(self.cols.subviews) do local prev_group = group width, group = self:update_col_layout(idx, col, width, group, parent_rect.width) + if col.hidden then goto continue end if not next_col_group and group ~= '' and not col.visible and col.group ~= cur_col_group then next_col_group = col.group local str = next_col_group .. string.char(26) -- right arrow @@ -695,12 +731,19 @@ function Spreadsheet:preUpdateLayout(parent_rect) left_group.label.on_activate=self:callback('jump_to_group', prev_col_group) left_group.visible = true end + ::continue:: end + self.shared.layout_changed = false end function Spreadsheet:render(dc) - if self.dirty or self.shared.fault then - self:refresh(self.prev_filter, true) + if self.shared.refresh_headers or self.shared.refresh_units then + if self.shared.refresh_units then + self:refresh(self.prev_filter, true) + end + if self.shared.refresh_headers then + self:update_headers() + end self:updateLayout() end local page_top = self.namelist.page_top @@ -725,16 +768,44 @@ end function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then - self.left_col = math.max(1, self.left_col - 1) - self:updateLayout() + for idx=self.left_col-1,1,-1 do + if not self.cols.subviews[idx].hidden then + self.left_col = idx + self:updateLayout() + break + end + end elseif keys.KEYBOARD_CURSOR_LEFT_FAST then - self.left_col = math.max(1, self.left_col - self:get_num_visible_cols()) + local remaining = self:get_num_visible_cols() + for idx=self.left_col-1,1,-1 do + if not self.cols.subviews[idx].hidden then + remaining = remaining - 1 + self.left_col = idx + if remaining == 0 then + break + end + end + end self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT then - self.left_col = math.min(#self.cols.subviews, self.left_col + 1) - self:updateLayout() + for idx=self.left_col+1,#self.cols.subviews do + if not self.cols.subviews[idx].hidden then + self.left_col = idx + self:updateLayout() + break + end + end elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then - self.left_col = math.min(#self.cols.subviews, self.left_col + self:get_num_visible_cols()) + local remaining = self:get_num_visible_cols() + for idx=self.left_col+1,#self.cols.subviews do + if not self.cols.subviews[idx].hidden then + remaining = remaining - 1 + self.left_col = idx + if remaining == 0 then + break + end + end + end self:updateLayout() end return Spreadsheet.super.onInput(self, keys) @@ -847,7 +918,7 @@ function Manipulator:init() frame={b=0, l=0}, auto_width=true, label=function() - return self.needs_refresh and 'Refresh (unit list has changed)' or 'Refresh' + return self.needs_refresh and 'Refresh units (new units have arrived)' or 'Refresh units' end, text_pen=function() return self.needs_refresh and COLOR_LIGHTRED or COLOR_GRAY From 13f8afe9b06d1dbb83451203035c2a977a8acbd6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 01:15:24 -0700 Subject: [PATCH 18/26] implement column popup menu --- gui/manipulator.lua | 195 +++++++++++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 65 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 798b6e21d9..a4534963e6 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -75,16 +75,62 @@ end ColumnMenu = defclass(ColumnMenu, widgets.Panel) ColumnMenu.ATTRS{ frame_style=gui.FRAME_INTERIOR, + frame_background=gui.CLEAR_PEN, + visible=false, + col=DEFAULT_NIL, } function ColumnMenu:init() + local choices = {} + + table.insert(choices, { + text='Sort', + fn=self.col:callback('sort', true), + }) + table.insert(choices, { + text='Hide column', + fn=self.col:callback('hide_column'), + }) + table.insert(choices, { + text='Hide group', + fn=self.col:callback('hide_group'), + }) + + self:addviews{ + widgets.List{ + choices=choices, + on_submit=function(_, choice) + choice.fn() + self:hide() + end, + }, + } end function ColumnMenu:show() self.prev_focus_owner = self.focus_group.cur self.visible = true + self:setFocus(true) end +function ColumnMenu:hide() + self.visible = false + if self.prev_focus_owner then + self.prev_focus_owner:setFocus(true) + end +end + +function ColumnMenu:onInput(keys) + if ColumnMenu.super.onInput(self, keys) then + return true + end + if keys._MOUSE_R then + self:hide() + elseif keys._MOUSE_L and not self:getMouseFramePos() then + self:hide() + end + return true +end ------------------------ -- Column @@ -111,6 +157,10 @@ local CH_DN = string.char(31) function Column:init() self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) + local function show_menu() + self.subviews.col_menu:show() + end + self:addviews{ widgets.TextButton{ view_id='col_group', @@ -118,8 +168,23 @@ function Column:init() label=self.group, visible=#self.group > 0, }, + widgets.Label{ + view_id='col_current', + frame={t=7, l=1+self.label_inset, w=4}, + auto_height=false, + }, + widgets.Label{ + view_id='col_total', + frame={t=8, l=1+self.label_inset, w=4}, + auto_height=false, + }, + widgets.List{ + view_id='col_list', + frame={t=10, l=0, w=self.data_width+2}, -- +2 for the invisible scrollbar + on_submit=self:callback('on_select'), + }, widgets.Panel{ - frame={t=2, l=0, h=5}, + frame={t=2, l=0, h=10}, subviews={ widgets.Divider{ view_id='col_stem', @@ -135,6 +200,7 @@ function Column:init() frame={l=0, t=0, w=1}, label=CH_DN, text_pen=COLOR_LIGHTGREEN, + on_activate=show_menu, visible=function() local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] return sort_spec.col == self and not sort_spec.rev @@ -144,6 +210,7 @@ function Column:init() frame={l=0, t=0, w=1}, label=CH_UP, text_pen=COLOR_LIGHTGREEN, + on_activate=show_menu, visible=function() local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] return sort_spec.col == self and sort_spec.rev @@ -153,6 +220,7 @@ function Column:init() frame={l=0, t=0, w=1}, label=CH_DOT, text_pen=COLOR_GRAY, + on_activate=show_menu, visible=function() local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] return sort_spec.col ~= self @@ -163,25 +231,15 @@ function Column:init() label=self.label, on_activate=self:callback('on_click'), }, + ColumnMenu{ + view_id='col_menu', + frame={l=0, t=1, h=5}, + col=self, + }, }, }, }, }, - widgets.Label{ - view_id='col_current', - frame={t=7, l=1+self.label_inset, w=4}, - auto_height=false, - }, - widgets.Label{ - view_id='col_total', - frame={t=8, l=1+self.label_inset, w=4}, - auto_height=false, - }, - widgets.List{ - view_id='col_list', - frame={t=10, l=0, w=self.data_width+2}, -- +2 for the invisible scrollbar - on_submit=self:callback('on_select'), - }, } self.subviews.col_list.scrollbar.visible = false @@ -199,18 +257,26 @@ function Column:on_select(idx, choice) end end +function Column:hide_column() + self.hidden = true + self.shared.refresh_headers = true +end + +function Column:hide_group() + for _,col in ipairs(self.parent_view.subviews) do + if col.group == self.group then + col.hidden = true + end + end + self.shared.refresh_headers = true +end + function Column:on_click() local modifiers = dfhack.internal.getModifiers() if modifiers.shift then - for _,col in ipairs(self.parent_view.subviews) do - if col.group == self.group then - col.hidden = true - end - end - self.shared.refresh_headers = true + self:hide_group() elseif modifiers.ctrl then - self.hidden = true - self.shared.refresh_headers = true + self:hide_column() else self:sort(true) end @@ -424,6 +490,33 @@ function ToggleColumn:on_select(idx, choice) self.dirty = true end +------------------------ +-- Cols +-- + +Cols = defclass(Cols, widgets.Panel) + +function Cols:renderSubviews(dc) + -- allow labels of columns to the left to overwrite the stems of columns on the right + for idx=#self.subviews,1,-1 do + local child = self.subviews[idx] + if utils.getval(child.visible) then + child:render(dc) + end + end + -- but group labels and popup menus on the right should overwrite long group names on the left + for _,child in ipairs(self.subviews) do + if utils.getval(child.visible) then + if utils.getval(child.subviews.col_group.visible) then + child.subviews.col_group:render(dc) + end + if utils.getval(child.subviews.col_menu.visible) then + child:render(dc) + end + end + end +end + ------------------------ -- Spreadsheet -- @@ -459,7 +552,7 @@ function Spreadsheet:init() refresh_headers=true, } - local cols = widgets.Panel{} + local cols = Cols{} self.cols = cols cols:addviews{ @@ -574,11 +667,13 @@ function Spreadsheet:init() widgets.TextButton{ view_id='left_group', frame={t=1, l=0, h=1}, + key='CUSTOM_CTRL_Y', visible=false, }, widgets.TextButton{ view_id='right_group', frame={t=1, r=0, h=1}, + key='CUSTOM_CTRL_T', visible=false, }, widgets.Label{ @@ -621,14 +716,6 @@ function Spreadsheet:sort_by_current_col() -- TODO end -function Spreadsheet:zoom_to_prev_group() - -- TODO -end - -function Spreadsheet:zoom_to_next_group() - -- TODO -end - function Spreadsheet:hide_current_col() -- TODO end @@ -715,10 +802,10 @@ function Spreadsheet:preUpdateLayout(parent_rect) if not next_col_group and group ~= '' and not col.visible and col.group ~= cur_col_group then next_col_group = col.group local str = next_col_group .. string.char(26) -- right arrow - right_group:setLabel(str) - right_group.frame.w = #str + 2 + right_group.frame.w = #str + 10 right_group.label.on_activate=self:callback('jump_to_group', next_col_group) right_group.visible = true + right_group:setLabel(str) end if cur_col_group ~= col.group then prev_col_group = cur_col_group @@ -726,10 +813,10 @@ function Spreadsheet:preUpdateLayout(parent_rect) cur_col_group = col.group if prev_group == '' and group ~= '' and prev_col_group and prev_col_group ~= '' then local str = string.char(27) .. prev_col_group -- left arrow - left_group:setLabel(str) - left_group.frame.w = #str + 2 + left_group.frame.w = #str + 10 left_group.label.on_activate=self:callback('jump_to_group', prev_col_group) left_group.visible = true + left_group:setLabel(str) end ::continue:: end @@ -869,48 +956,26 @@ function Manipulator:init() subviews={ widgets.WrappedLabel{ frame={t=0, l=0}, - text_to_wrap='Use arrow keys or middle click drag to navigate cells. Left click or ENTER to toggle current cell.', - }, - widgets.Label{ - frame={b=2, l=0}, - text='Current column:', + text_to_wrap='Use arrow keys or middle click drag to navigate cells. Left click or ENTER to toggle cell.', }, widgets.HotkeyLabel{ - frame={b=2, l=17}, + frame={b=2, l=0}, auto_width=true, label='Sort/reverse sort', key='CUSTOM_SHIFT_S', on_activate=function() self.subviews.sheet:sort_by_current_col() end, }, widgets.HotkeyLabel{ - frame={b=2, l=39}, + frame={b=2, l=22}, auto_width=true, - label='Hide', + label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, }, - widgets.Label{ - frame={b=1, l=0}, - text='Current group:', - }, - widgets.HotkeyLabel{ - frame={b=1, l=17}, - auto_width=true, - label='Prev group', - key='CUSTOM_CTRL_Y', - on_activate=function() self.subviews.sheet:zoom_to_prev_group() end, - }, - widgets.HotkeyLabel{ - frame={b=1, l=37}, - auto_width=true, - label='Next group', - key='CUSTOM_CTRL_T', - on_activate=function() self.subviews.sheet:zoom_to_next_group() end, - }, widgets.HotkeyLabel{ - frame={b=1, l=57}, + frame={b=2, l=38}, auto_width=true, - label='Hide', + label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, }, From 7232ba845f8143c363e6a05401abafa3dc6d3cad Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 02:11:28 -0700 Subject: [PATCH 19/26] popup menus for jump to col and unhide col --- gui/manipulator.lua | 164 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 154 insertions(+), 10 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index a4534963e6..9b233dae99 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -262,6 +262,11 @@ function Column:hide_column() self.shared.refresh_headers = true end +function Column:unhide_column() + self.hidden = false + self.shared.refresh_headers = true +end + function Column:hide_group() for _,col in ipairs(self.parent_view.subviews) do if col.group == self.group then @@ -727,10 +732,14 @@ end function Spreadsheet:jump_to_group(group) for i, col in ipairs(self.cols.subviews) do if not col.hidden and col.group == group then - self.left_col = i + self:jump_to_col(i) break end end +end + +function Spreadsheet:jump_to_col(idx) + self.left_col = idx self:updateLayout() end @@ -857,47 +866,133 @@ function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then for idx=self.left_col-1,1,-1 do if not self.cols.subviews[idx].hidden then - self.left_col = idx - self:updateLayout() + self:jump_to_col(idx) break end end elseif keys.KEYBOARD_CURSOR_LEFT_FAST then local remaining = self:get_num_visible_cols() + local target_col = self.left_col for idx=self.left_col-1,1,-1 do if not self.cols.subviews[idx].hidden then remaining = remaining - 1 - self.left_col = idx + target_col = idx if remaining == 0 then break end end end - self:updateLayout() + self:jump_to_col(target_col) elseif keys.KEYBOARD_CURSOR_RIGHT then for idx=self.left_col+1,#self.cols.subviews do if not self.cols.subviews[idx].hidden then - self.left_col = idx - self:updateLayout() + self:jump_to_col(idx) break end end elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then local remaining = self:get_num_visible_cols() + local target_col = self.left_col for idx=self.left_col+1,#self.cols.subviews do if not self.cols.subviews[idx].hidden then remaining = remaining - 1 - self.left_col = idx + target_col = idx if remaining == 0 then break end end end - self:updateLayout() + self:jump_to_col(target_col) end return Spreadsheet.super.onInput(self, keys) end +------------------------ +-- QuickMenu +-- + +QuickMenu = defclass(QuickMenu, widgets.Panel) +QuickMenu.ATTRS{ + frame_style=gui.FRAME_INTERIOR, + frame_background=gui.CLEAR_PEN, + visible=false, + multiselect=false, + label=DEFAULT_NIL, + choices_fn=DEFAULT_NIL, +} + +function QuickMenu:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text=self.label, + }, + widgets.FilteredList{ + view_id='list', + frame={t=2, l=0, b=self.multiselect and 3 or 0}, + on_submit=function(_, choice) + choice.fn() + self:hide() + end, + on_submit2=self.multiselect and function(_, choice) + choice.fn() + local list = self.subviews.list + local filter = list:getFilter() + list:setChoices(self.choices_fn(), list:getSelected()) + list:setFilter(filter) + end or nil, + }, + widgets.Label{ + frame={b=1, l=0}, + text='Shift click to select multiple.', + visible=self.multiselect, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='CUSTOM_CTRL_A', + label='Select all', + visible=self.multiselect, + on_activate=function() + local list = self.subviews.list + for _,choice in ipairs(list:getVisibleChoices()) do + choice.fn() + end + local filter = list:getFilter() + list:setChoices(self.choices_fn(), list:getSelected()) + list:setFilter(filter) + end, + }, + } +end + +function QuickMenu:show() + self.prev_focus_owner = self.focus_group.cur + self.visible = true + local list = self.subviews.list + list.edit:setText('') + list.edit:setFocus(true) + list:setChoices(self.choices_fn()) +end + +function QuickMenu:hide() + self.visible = false + if self.prev_focus_owner then + self.prev_focus_owner:setFocus(true) + end +end + +function QuickMenu:onInput(keys) + if ColumnMenu.super.onInput(self, keys) then + return true + end + if keys._MOUSE_R then + self:hide() + elseif keys._MOUSE_L and not self:getMouseFramePos() then + self:hide() + end + return true +end + ------------------------ -- Manipulator -- @@ -968,17 +1063,31 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=2, l=22}, auto_width=true, + label='Jump to column', + key='CUSTOM_CTRL_G', + on_activate=function() self.subviews.quick_jump_menu:show() end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + auto_width=true, label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, }, widgets.HotkeyLabel{ - frame={b=2, l=38}, + frame={b=1, l=22}, auto_width=true, label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, }, + widgets.HotkeyLabel{ + frame={b=1, l=46}, + auto_width=true, + label='Unhide column', + key='CUSTOM_SHIFT_U', + on_activate=function() self.subviews.unhide_menu:show() end, + }, widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, @@ -995,6 +1104,41 @@ function Manipulator:init() }, }, }, + QuickMenu{ + view_id='quick_jump_menu', + frame={b=0, w=35, h=25}, + label='Jump to column:', + choices_fn=function() + local choices = {} + for idx,col in ipairs(self.subviews.sheet.cols.subviews) do + if col.hidden then goto continue end + table.insert(choices, { + text=('%s/%s'):format(col.group, col.label), + fn=function() self.subviews.sheet:jump_to_col(idx) end, + }) + ::continue:: + end + return choices + end, + }, + QuickMenu{ + view_id='unhide_menu', + frame={b=0, w=35, h=25}, + multiselect=true, + label='Unhide column:', + choices_fn=function() + local choices = {} + for idx,col in ipairs(self.subviews.sheet.cols.subviews) do + if not col.hidden then goto continue end + table.insert(choices, { + text=('%s/%s'):format(col.group, col.label), + fn=function() self.subviews.sheet.cols.subviews[idx]:unhide_column() end, + }) + ::continue:: + end + return choices + end, + }, } end From 03030cdf0c60b92dbcc48efc52daa382823c9c13 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 02:35:38 -0700 Subject: [PATCH 20/26] implement zooming to unit and buildings --- gui/manipulator.lua | 50 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 9b233dae99..2932ee2e75 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -95,6 +95,12 @@ function ColumnMenu:init() text='Hide group', fn=self.col:callback('hide_group'), }) + if self.col.zoom_fn then + table.insert(choices, { + text='Zoom to', + fn=self.col.zoom_fn, + }) + end self:addviews{ widgets.List{ @@ -148,6 +154,7 @@ Column.ATTRS{ count_fn=DEFAULT_NIL, cmp_fn=DEFAULT_NIL, choice_fn=DEFAULT_NIL, + zoom_fn=DEFAULT_NIL, } local CH_DOT = string.char(15) @@ -233,7 +240,7 @@ function Column:init() }, ColumnMenu{ view_id='col_menu', - frame={l=0, t=1, h=5}, + frame={l=0, t=1, h=self.zoom_fn and 6 or 5}, col=self, }, }, @@ -647,19 +654,23 @@ function Spreadsheet:init() end local function add_workshops(vec, type_enum, type_defs) - for _, workshop in ipairs(vec) do + for _, bld in ipairs(vec) do cols:addviews{ ToggleColumn{ group='workshops', - label=get_workshop_label(workshop, type_enum, type_defs), + label=get_workshop_label(bld, type_enum, type_defs), shared=self.shared, - data_fn=curry(toggle_sorted_vec_data, workshop.profile.permitted_workers), + data_fn=curry(toggle_sorted_vec_data, bld.profile.permitted_workers), toggle_fn=function(unit_id, prev_val) if not prev_val then -- there can be only one - workshop.profile.permitted_workers:resize(0) + bld.profile.permitted_workers:resize(0) end - toggle_sorted_vec(workshop.profile.permitted_workers, unit_id, prev_val) + toggle_sorted_vec(bld.profile.permitted_workers, unit_id, prev_val) + end, + zoom_fn=function() + dfhack.gui.revealInDwarfmodeMap( + xyz2pos(bld.centerx, bld.centery, bld.z), true, true) end, } } @@ -717,6 +728,19 @@ function Spreadsheet:init() self.namelist:setFocus(true) end +function Spreadsheet:zoom_to_unit() + local idx = self.namelist:getSelected() + if not idx then return end + local unit = df.unit.find(self.subviews.name:get_sorted_unit_id(idx)) + if not unit then return end + dfhack.gui.revealInDwarfmodeMap( + xyz2pos(dfhack.units.getPosition(unit)), true, true) +end + +function Spreadsheet:zoom_to_col_source() + -- TODO +end + function Spreadsheet:sort_by_current_col() -- TODO end @@ -1091,6 +1115,20 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, + label='Zoom to unit', + key='CUSTOM_SHIFT_Z', + on_activate=function() self.subviews.sheet:zoom_to_unit() end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=22}, + auto_width=true, + label='Zoom to col source', + key='CUSTOM_CTRL_Z', + on_activate=function() self.subviews.sheet:zoom_to_col_source() end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=46}, + auto_width=true, label=function() return self.needs_refresh and 'Refresh units (new units have arrived)' or 'Refresh units' end, From 3ecf6e039b08578e9cc77f26118acb544823cc70 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 02:54:09 -0700 Subject: [PATCH 21/26] disable non-functional hotkey buttons --- gui/manipulator.lua | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 2932ee2e75..0fffdb1079 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -34,8 +34,7 @@ end -- preset schema local function get_default_preset() return { - hidden_groups={}, - hidden_cols={}, + cols={}, pinned={}, } end @@ -737,6 +736,7 @@ function Spreadsheet:zoom_to_unit() xyz2pos(dfhack.units.getPosition(unit)), true, true) end +-- TODO these are dependent on having a column cursor function Spreadsheet:zoom_to_col_source() -- TODO end @@ -753,6 +753,10 @@ function Spreadsheet:hide_current_col_group() -- TODO end +function Spreadsheet:export() + -- TODO +end + function Spreadsheet:jump_to_group(group) for i, col in ipairs(self.cols.subviews) do if not col.hidden and col.group == group then @@ -1083,6 +1087,7 @@ function Manipulator:init() label='Sort/reverse sort', key='CUSTOM_SHIFT_S', on_activate=function() self.subviews.sheet:sort_by_current_col() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=2, l=22}, @@ -1091,12 +1096,21 @@ function Manipulator:init() key='CUSTOM_CTRL_G', on_activate=function() self.subviews.quick_jump_menu:show() end, }, + widgets.HotkeyLabel{ + frame={b=2, l=46}, + auto_width=true, + label='Export to csv', + key='CUSTOM_SHIFT_E', + on_activate=function() self.subviews.sheet:export() end, + enabled=false, + }, widgets.HotkeyLabel{ frame={b=1, l=0}, auto_width=true, label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=22}, @@ -1104,6 +1118,7 @@ function Manipulator:init() label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=46}, @@ -1122,9 +1137,10 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=0, l=22}, auto_width=true, - label='Zoom to col source', + label='Zoom to source', key='CUSTOM_CTRL_Z', on_activate=function() self.subviews.sheet:zoom_to_col_source() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=0, l=46}, @@ -1133,7 +1149,7 @@ function Manipulator:init() return self.needs_refresh and 'Refresh units (new units have arrived)' or 'Refresh units' end, text_pen=function() - return self.needs_refresh and COLOR_LIGHTRED or COLOR_GRAY + return self.needs_refresh and COLOR_LIGHTRED or COLOR_WHITE end, key='CUSTOM_SHIFT_R', on_activate=function() From 97e7f8c8d86d9f7d07801f090724be1cd2e5bc3f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 09:55:50 -0700 Subject: [PATCH 22/26] shift displayed columns when all columns on screen are hidden --- gui/manipulator.lua | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 0fffdb1079..5cc4ed666f 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -141,12 +141,15 @@ end -- Column -- +local DEFAULT_DATA_WIDTH = 4 +local DEFAULT_COL_OVERSCAN = 14 + Column = defclass(Column, widgets.Panel) Column.ATTRS{ label=DEFAULT_NIL, group='', label_inset=0, - data_width=4, + data_width=DEFAULT_DATA_WIDTH, hidden=DEFAULT_NIL, shared=DEFAULT_NIL, data_fn=DEFAULT_NIL, @@ -161,7 +164,7 @@ local CH_UP = string.char(30) local CH_DN = string.char(31) function Column:init() - self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) + self.frame = utils.assign({t=0, b=0, l=0, w=DEFAULT_COL_OVERSCAN}, self.frame or {}) local function show_menu() self.subviews.col_menu:show() @@ -767,7 +770,27 @@ function Spreadsheet:jump_to_group(group) end function Spreadsheet:jump_to_col(idx) + idx = math.min(idx, #self.cols.subviews) + idx = math.max(idx, 1) self.left_col = idx + if self.cols.subviews[idx].hidden then + local found = false + for shifted_idx=self.left_col-1,1,-1 do + if not self.cols.subviews[shifted_idx].hidden then + self.left_col = shifted_idx + found = true + break + end + end + if not found then + for shifted_idx=self.left_col+1,#self.cols.subviews do + if not self.cols.subviews[shifted_idx].hidden then + self.left_col = shifted_idx + break + end + end + end + end self:updateLayout() end @@ -868,7 +891,11 @@ function Spreadsheet:render(dc) if self.shared.refresh_headers then self:update_headers() end - self:updateLayout() + if self.cols.subviews[self.left_col].hidden then + self:jump_to_col(self.left_col) + else + self:updateLayout() + end end local page_top = self.namelist.page_top local selected = self.namelist:getSelected() @@ -881,23 +908,16 @@ function Spreadsheet:render(dc) end function Spreadsheet:get_num_visible_cols() - local count = 0 - for _,col in ipairs(self.cols.subviews) do - if col.visible then - count = count + 1 - end - end - return count + local rect = self.frame_rect + if not rect then return 1 end + local other_width = self.subviews.name.data_width + (DEFAULT_COL_OVERSCAN - DEFAULT_DATA_WIDTH) + local width = rect.width - other_width + return width // (DEFAULT_DATA_WIDTH + 1) end function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then - for idx=self.left_col-1,1,-1 do - if not self.cols.subviews[idx].hidden then - self:jump_to_col(idx) - break - end - end + self:jump_to_col(self.left_col-1) elseif keys.KEYBOARD_CURSOR_LEFT_FAST then local remaining = self:get_num_visible_cols() local target_col = self.left_col @@ -1253,14 +1273,14 @@ ManipulatorOverlay.ATTRS{ default_pos={x=50, y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/CREATURES/CITIZEN', - frame={w=34, h=1}, + frame={w=35, h=1}, } function ManipulatorOverlay:init() self:addviews{ widgets.TextButton{ frame={t=0, l=0}, - label='DFHack citizen interface', + label='DFHack citizen management', key='CUSTOM_CTRL_N', on_activate=function() dfhack.run_script('gui/manipulator') end, }, From ce186331c5200b93426202215ee823ec0255bcec Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 11:03:19 -0700 Subject: [PATCH 23/26] keyboard cursor prototype and export logic --- gui/manipulator.lua | 69 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 5cc4ed666f..722b69ccff 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -472,6 +472,7 @@ local function toggle_choice(get_ordered_data_fn) get_enabled_button_token(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT), get_enabled_button_token(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER), get_enabled_button_token(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT), + ' ', }, } end @@ -562,6 +563,7 @@ function Spreadsheet:init() sort_stack={}, sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) cache={}, -- cached pointers; reset at end of frame + cur_col=nil, refresh_units=true, refresh_headers=true, } @@ -718,6 +720,9 @@ function Spreadsheet:init() self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} + -- set initial selection + self:set_cur_col(self.subviews.favorites) + self.namelist = self.subviews.name.subviews.col_list self:addviews{ widgets.Scrollbar{ @@ -730,6 +735,16 @@ function Spreadsheet:init() self.namelist:setFocus(true) end +local CURSOR_PEN = dfhack.pen.parse{fg=COLOR_GREY, bg=COLOR_CYAN} + +function Spreadsheet:set_cur_col(col) + if self.shared.cur_col then + self.shared.cur_col.subviews.col_list.cursor_pen = COLOR_LIGHTCYAN + end + self.shared.cur_col = col + col.subviews.col_list.cursor_pen = CURSOR_PEN +end + function Spreadsheet:zoom_to_unit() local idx = self.namelist:getSelected() if not idx then return end @@ -739,25 +754,62 @@ function Spreadsheet:zoom_to_unit() xyz2pos(dfhack.units.getPosition(unit)), true, true) end --- TODO these are dependent on having a column cursor function Spreadsheet:zoom_to_col_source() - -- TODO + if not self.shared.cur_col or not self.shared.cur_col.zoom_fn then return end + self.shared.cur_col.zoom_fn() end function Spreadsheet:sort_by_current_col() - -- TODO + if not self.shared.cur_col then return end + self.shared.cur_col:sort(true) end function Spreadsheet:hide_current_col() - -- TODO + if not self.shared.cur_col then return end + self.shared.cur_col:hide_column() end function Spreadsheet:hide_current_col_group() - -- TODO + if not self.shared.cur_col then return end + self.shared.cur_col:hide_group() end +-- utf8-ize and, if needed, quote and escape +local function make_csv_cell(fmt, ...) + local str = fmt:format(...) + str = dfhack.df2utf(str) + if str:find('[,"]') then + str = str:gsub('"', '""') + str = ('"%s"'):format(str) + end + return str +end + +-- exports visible data, in the current sort, to a .csv file function Spreadsheet:export() - -- TODO + local file = io.open('manipulator.csv', 'a+') + if not file then + dfhack.printerr('could not open export file: manipulator.csv') + return + end + file:write(make_csv_cell('%s,', self.subviews.name.label)) + for _, col in ipairs(self.cols.subviews) do + if col.hidden then goto continue end + file:write(make_csv_cell('%s/%s,', col.group, col.label)) + ::continue:: + end + file:write(NEWLINE) + for row=1,#self.shared.filtered_unit_ids do + file:write(make_csv_cell('%s', self.subviews.name:get_sorted_data(row))) + file:write(',') + for _, col in ipairs(self.cols.subviews) do + if col.hidden then goto continue end + file:write(make_csv_cell('%s,', col:get_sorted_data(row) or '')) + ::continue:: + end + file:write(NEWLINE) + end + file:close() end function Spreadsheet:jump_to_group(group) @@ -1107,7 +1159,6 @@ function Manipulator:init() label='Sort/reverse sort', key='CUSTOM_SHIFT_S', on_activate=function() self.subviews.sheet:sort_by_current_col() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=2, l=22}, @@ -1122,7 +1173,6 @@ function Manipulator:init() label='Export to csv', key='CUSTOM_SHIFT_E', on_activate=function() self.subviews.sheet:export() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=0}, @@ -1130,7 +1180,6 @@ function Manipulator:init() label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=22}, @@ -1138,7 +1187,6 @@ function Manipulator:init() label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=46}, @@ -1160,7 +1208,6 @@ function Manipulator:init() label='Zoom to source', key='CUSTOM_CTRL_Z', on_activate=function() self.subviews.sheet:zoom_to_col_source() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=0, l=46}, From c47f79de920ce70cee03bfdc2cd51167da662bbd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 May 2024 07:18:24 -0700 Subject: [PATCH 24/26] progress towards keyboard cursor --- gui/manipulator.lua | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 722b69ccff..2e2c901d03 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -262,6 +262,7 @@ function Column:on_select(idx, choice) -- avoiding an infinite loop local namelist = self.parent_view.parent_view.namelist if namelist then + self.shared.set_cursor_col_fn(self) namelist:setSelected(idx) end end @@ -563,7 +564,8 @@ function Spreadsheet:init() sort_stack={}, sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) cache={}, -- cached pointers; reset at end of frame - cur_col=nil, + cursor_col=nil, + set_cursor_col_fn=self:callback('set_cursor_col'), refresh_units=true, refresh_headers=true, } @@ -716,12 +718,17 @@ function Spreadsheet:init() cols, } + -- teach each column about its relative position so we can track cursor movement + for idx, col in ipairs(cols) do + col.idx = idx + end + -- set up initial sort: primary favorites, secondary name self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} - -- set initial selection - self:set_cur_col(self.subviews.favorites) + -- set initial keyboard cursor position + self:set_cursor_col(self.subviews.favorites) self.namelist = self.subviews.name.subviews.col_list self:addviews{ @@ -737,11 +744,11 @@ end local CURSOR_PEN = dfhack.pen.parse{fg=COLOR_GREY, bg=COLOR_CYAN} -function Spreadsheet:set_cur_col(col) - if self.shared.cur_col then - self.shared.cur_col.subviews.col_list.cursor_pen = COLOR_LIGHTCYAN +function Spreadsheet:set_cursor_col(col) + if self.shared.cursor_col then + self.shared.cursor_col.subviews.col_list.cursor_pen = COLOR_LIGHTCYAN end - self.shared.cur_col = col + self.shared.cursor_col = col col.subviews.col_list.cursor_pen = CURSOR_PEN end @@ -755,23 +762,23 @@ function Spreadsheet:zoom_to_unit() end function Spreadsheet:zoom_to_col_source() - if not self.shared.cur_col or not self.shared.cur_col.zoom_fn then return end - self.shared.cur_col.zoom_fn() + if not self.shared.cursor_col or not self.shared.cursor_col.zoom_fn then return end + self.shared.cursor_col.zoom_fn() end function Spreadsheet:sort_by_current_col() - if not self.shared.cur_col then return end - self.shared.cur_col:sort(true) + if not self.shared.cursor_col then return end + self.shared.cursor_col:sort(true) end function Spreadsheet:hide_current_col() - if not self.shared.cur_col then return end - self.shared.cur_col:hide_column() + if not self.shared.cursor_col then return end + self.shared.cursor_col:hide_column() end function Spreadsheet:hide_current_col_group() - if not self.shared.cur_col then return end - self.shared.cur_col:hide_group() + if not self.shared.cursor_col then return end + self.shared.cursor_col:hide_group() end -- utf8-ize and, if needed, quote and escape From 01ee53f5e5c0ff6cc16ade038f0f1408f7158b80 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 5 Oct 2024 19:30:07 -0700 Subject: [PATCH 25/26] mark as unavailable for dark launch --- docs/gui/manipulator.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/gui/manipulator.rst b/docs/gui/manipulator.rst index 0cdbd8990e..353d894c77 100644 --- a/docs/gui/manipulator.rst +++ b/docs/gui/manipulator.rst @@ -3,9 +3,12 @@ gui/manipulator .. dfhack-tool:: :summary: Multi-function unit management interface. - :tags: fort productivity units + :tags: unavailable -This spreadsheet-like UI allows you to see all your units, their skills, properties, assignments, etc. at a glance. You can search, sort, and filter to see exactly the information that you're looking for. Moreover, you can assign units to burrows, squads, work details, and workshops. +This spreadsheet-like UI allows you to see all your units, their skills, +properties, assignments, etc. at a glance. You can search, sort, and filter to +see exactly the information that you're looking for. Moreover, you can assign +units to burrows, squads, work details, and workshops. Usage ----- From b9433de652e485586c2a0effde16be77f12ce554 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 02:35:00 +0000 Subject: [PATCH 26/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- internal/manipulator/presets.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/manipulator/presets.lua b/internal/manipulator/presets.lua index 757e625cda..259d4a43ea 100644 --- a/internal/manipulator/presets.lua +++ b/internal/manipulator/presets.lua @@ -7,4 +7,4 @@ PRESETS = { }, }, -} \ No newline at end of file +}