diff --git a/docs/changelog.txt b/docs/changelog.txt index 78e8a56cc3..2dc3e49474 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: ## New Features - `autodump`: new ``undestroy`` option reverts pending item destruction while the game is still paused +- `stocks`: the overlay's ``collapse all`` hotkey now toggles, expanding all categories again when everything is collapsed ## Fixes - Fix broken weather lookup in ``World::ReadCurrentWeather`` @@ -69,6 +70,7 @@ Template for new versions: - `aquifer`: make ``--skip-top`` and top-relative ``--levels`` take effect for the ``drain``, ``convert``, and ``add`` actions instead of being silently ignored - `autodump`: ``destroy`` no longer leaves the contents of destroyed containers in limbo or crashes when destroying unit-held items - `timestream`: deal properly with units who have breathing difficulties +- `stocks`: overlay now resets scroll position when collapsing categories so the item list is no longer left blank and unscrollable ## Misc Improvements - Added ``Coord2d`` and ``Coord3d`` C++ templates, providing a standard set operations for 2-tuples and 3-tuples of any numeric type diff --git a/plugins/lua/stocks.lua b/plugins/lua/stocks.lua index c1a7ad81de..a854311b84 100644 --- a/plugins/lua/stocks.lua +++ b/plugins/lua/stocks.lua @@ -6,24 +6,45 @@ local widgets = require('gui.widgets') local stocks = df.global.game.main_interface.stocks +-- these are only safe to call while the stocks page is open; guard against +-- stray activations as the page closes local function collapse_all() + if not stocks.open then return end local num_sections = #stocks.current_type_a_expanded for idx=0,num_sections-1 do stocks.current_type_a_expanded[idx] = false end stocks.i_height = num_sections * 3 + -- the collapsed list is much shorter; reset the scroll position so the + -- view isn't left pointing past the end of the list + stocks.scroll_position_item = 0 end local function expand_all() + if not stocks.open then return end local num_sections = #stocks.current_type_a_expanded for idx=0,num_sections-1 do stocks.current_type_a_expanded[idx] = true end - num_items = #stocks.current_type_i_list + local num_items = #stocks.current_type_i_list stocks.i_height = (num_items + num_sections) * 3 end +local function all_collapsed() + if not stocks.open then return true end + for idx=0,#stocks.current_type_a_expanded-1 do + if stocks.current_type_a_expanded[idx] then return false end + end + return true +end + +local function toggle_all() + if not stocks.open then return end + if all_collapsed() then expand_all() else collapse_all() end +end + local function remove_empty() + if not stocks.open then return end local empties = {} for itype,v in ipairs(stocks.storeamount) do if v == 0 and stocks.badamount[itype] == 0 then @@ -36,6 +57,10 @@ local function remove_empty() for idx=#stocks.filtered_type_list-1,0,-1 do if empties[stocks.filtered_type_list[idx]] then stocks.filtered_type_list:erase(idx) end end + -- removing types shortens the type list; keep the scroll position in bounds + if stocks.scroll_position_type >= #stocks.filtered_type_list then + stocks.scroll_position_type = math.max(0, #stocks.filtered_type_list - 1) + end end -- ------------------- @@ -57,9 +82,11 @@ function StocksOverlay:init() self:addviews{ widgets.HotkeyLabel{ frame={t=0, l=0}, - label='collapse all', + label=function() + return all_collapsed() and 'expand all' or 'collapse all' + end, key='CUSTOM_CTRL_X', - on_activate=collapse_all, + on_activate=toggle_all, }, widgets.HotkeyLabel{ frame={t=1, l=0}, diff --git a/test/plugins/stocks.lua b/test/plugins/stocks.lua new file mode 100644 index 0000000000..0dba35c00a --- /dev/null +++ b/test/plugins/stocks.lua @@ -0,0 +1,87 @@ +config.mode = 'fortress' +config.target = 'overlay' + +local gui = require('gui') +local overlay = require('plugins.overlay') + +local stocks = df.global.game.main_interface.stocks + +-- feed a key to the stocks overlay widget's input handler. This is the same +-- code path the viewscreen's interposed feed() takes; we invoke it directly +-- because the focus string cache only refreshes on game frames, which tests +-- cannot wait for. +local function send_key(key) + overlay.get_state().db['stocks.overlay'].widget:onInput({[key]=true}) +end + +local function set_stocks_open(open) + if stocks.open ~= open then + gui.simulateInput(dfhack.gui.getDFViewscreen(true), 'D_STOCKS') + end +end + +local function expanded_count() + local n = 0 + for i=0,#stocks.current_type_a_expanded-1 do + if stocks.current_type_a_expanded[i] then n = n + 1 end + end + return n +end + +local saved_open + +config.wrapper = function(test_fn) + saved_open = stocks.open + -- the widget db can be empty if a rescan is still in progress + overlay.rescan() + return dfhack.with_finalize(function() + -- close and reopen so the game repopulates any lists the test mutated + set_stocks_open(false) + set_stocks_open(saved_open) + end, test_fn) +end + +function test.toggle_collapses_then_expands() + set_stocks_open(true) + local num_sections = #stocks.current_type_a_expanded + expect.true_(num_sections > 0) + for i=0,num_sections-1 do + stocks.current_type_a_expanded[i] = true + end + stocks.scroll_position_item = 5 + send_key('CUSTOM_CTRL_X') + expect.eq(0, expanded_count()) + -- collapsing resets the item scroll position so it stays in bounds + expect.eq(0, stocks.scroll_position_item) + -- with everything collapsed, the same hotkey expands + send_key('CUSTOM_CTRL_X') + expect.eq(num_sections, expanded_count()) +end + +function test.expand_hotkey_expands_all() + set_stocks_open(true) + for i=0,#stocks.current_type_a_expanded-1 do + stocks.current_type_a_expanded[i] = false + end + send_key('CUSTOM_CTRL_Z') + expect.eq(#stocks.current_type_a_expanded, expanded_count()) +end + +function test.remove_empty_keeps_type_scroll_in_bounds() + set_stocks_open(true) + stocks.scroll_position_type = #stocks.filtered_type_list + 10 + send_key('CUSTOM_CTRL_E') + expect.true_(stocks.scroll_position_type >= 0) + expect.true_(stocks.scroll_position_type < #stocks.filtered_type_list) +end + +function test.closed_stocks_ignores_overlay_keys() + set_stocks_open(false) + -- the stocks.open guard must make every hotkey a no-op while the page is + -- closed, even if an activation slips through as the page closes + local n = expanded_count() + send_key('CUSTOM_CTRL_X') + send_key('CUSTOM_CTRL_Z') + send_key('CUSTOM_CTRL_E') + expect.eq(n, expanded_count()) +end