From 348ac55f4cb5160122f073814de88dfb579e28d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 21:17:03 -0800 Subject: [PATCH] allow singleton selection for items --- plugins/lua/buildingplan.lua | 238 +++++++++++++++++++++++++---------- 1 file changed, 171 insertions(+), 67 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index fcd7b83fd..cb5926723 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -131,6 +131,15 @@ local function get_selected_item_pen() return SELECTED_ITEM_PEN end +BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) +BuildingplanScreen.ATTRS { + force_pause=true, + pass_pause=false, + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, +} + -------------------------------- -- ItemSelection -- @@ -154,15 +163,16 @@ function ItemSelection:init() self.quantity = get_quantity(filter) self.num_selected = 0 self.selected_set = {} + local plural = self.quantity == 1 and '' or 's' self:addviews{ widgets.Label{ frame={t=0, l=0, r=10}, text={ get_desc(filter), - self.quantity == 1 and '' or 's', + plural, NEWLINE, - ('Select up to %d items ('):format(self.quantity), + ('Select up to %d item%s ('):format(self.quantity, plural), {text=function() return self.num_selected end}, ' selected)', }, @@ -179,11 +189,52 @@ function ItemSelection:init() on_click=self:callback('submit'), }, widgets.FilteredList{ - frame={t=3, l=0, r=0, b=0}, + view_id='flist', + frame={t=3, l=0, r=0, b=4}, case_sensitive=false, choices=self:get_choices(), icon_width=2, - on_submit=self:callback('toggle_item'), + on_submit=self:callback('toggle_group'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=2}, + key='SELECT', + label='Use all/none selected', + auto_width=true, + on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.HotkeyLabel{ + frame={l=32, b=2}, + key='LEAVESCREEN', + label='Cancel build', + auto_width=true, + on_activate=function() self.on_cancel() end, + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, + key='KEYBOARD_CURSOR_RIGHT_FAST', + key_sep=' : ', + label='Use one selected', + auto_width=true, + on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=1, w=5}, + text_pen=COLOR_LIGHTGREEN, + text='Right', + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + key='KEYBOARD_CURSOR_LEFT_FAST', + key_sep=' : ', + label='Use one fewer selected', + auto_width=true, + on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=0, w=4}, + text_pen=COLOR_LIGHTGREEN, + text='Left', }, } end @@ -199,67 +250,79 @@ end function ItemSelection:get_choices() local item_ids = getAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index - 1) - local buckets, selected_buckets = {}, {} + local buckets = {} for _,item_id in ipairs(item_ids) do local item = df.item.find(item_id) if not item then goto continue end local desc = dfhack.items.getDescription(item, 0, true) if buckets[desc] then local bucket = buckets[desc] - table.insert(bucket.item_ids, item_id) - bucket.quantity = bucket.quantity + 1 + table.insert(bucket.data.item_ids, item_id) + bucket.data.quantity = bucket.data.quantity + 1 else local entry = { - text=desc, search_key=make_search_key(desc), icon=self:callback('get_entry_icon', item_id), - item_ids={item_id}, - item_type=item:getType(), - item_subtype=item:getSubtype(), - quantity=1, - selected=false, + data={ + item_ids={item_id}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + quality=item:getQuality(), + selected=0, + }, } buckets[desc] = entry end ::continue:: end - local selected_qty = 0 - for bucket in pairs(selected_buckets) do - for _,item_id in ipairs(bucket.item_ids) do - self.selected_set[item_id] = true - end - selected_qty = selected_qty + bucket.quantity - bucket.selected = true - if selected_qty >= self.quantity then break end - end - self.num_selected = selected_qty local choices = {} - for _,choice in pairs(buckets) do - choice.text = ('(%d) %s'):format(choice.quantity, choice.text) + for desc,choice in pairs(buckets) do + local data = choice.data + choice.text = { + {width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end}, + {gap=2, text=desc}, + } table.insert(choices, choice) end local function choice_sort(a, b) - return a.item_type < b.item_type or - (a.item_type == b.item_type and a.item_subtype < b.item_subtype) or - (a.item_type == b.item_type and a.item_subtype == b.item_subtype and a.search_key < b.search_key) + local ad, bd = a.data, b.data + return ad.item_type < bd.item_type or + (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) end table.sort(choices, choice_sort) return choices end -function ItemSelection:toggle_item(_, choice) - if choice.selected then - for _,item_id in ipairs(choice.item_ids) do - self.selected_set[item_id] = nil - end - self.num_selected = self.num_selected - choice.quantity - choice.selected = false - elseif self.quantity > self.num_selected then - for _,item_id in ipairs(choice.item_ids) do - self.selected_set[item_id] = true - end - self.num_selected = self.num_selected + choice.quantity - choice.selected = true +function ItemSelection:increment_group(idx, choice) + local data = choice.data + if self.quantity <= self.num_selected then return false end + if data.selected >= data.quantity then return false end + data.selected = data.selected + 1 + self.num_selected = self.num_selected + 1 + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = true + return true +end + +function ItemSelection:decrement_group(idx, choice) + local data = choice.data + if data.selected <= 0 then return false end + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = nil + self.num_selected = self.num_selected - 1 + data.selected = data.selected - 1 + return true +end + +function ItemSelection:toggle_group(idx, choice) + local data = choice.data + if data.selected > 0 then + while self:decrement_group(idx, choice) do end + else + while self:increment_group(idx, choice) do end end end @@ -279,19 +342,26 @@ function ItemSelection:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then self.on_cancel() return true + elseif keys._MOUSE_L_DOWN then + local list = self.subviews.flist.list + local idx = list:getIdxUnderMouse() + if idx then + list:setSelected(idx) + local modstate = dfhack.internal.getModstate() + if modstate & 2 > 0 then -- ctrl + local choice = list:getChoices()[idx] + if modstate & 1 > 0 then -- shift + self:decrement_group(idx, choice) + else + self:increment_group(idx, choice) + end + return true + end + end end return ItemSelection.super.onInput(self, keys) end -BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) -BuildingplanScreen.ATTRS { - force_pause=true, - pass_pause=false, - pass_movement_keys=true, - pass_mouse_clicks=false, - defocusable=false, -} - ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { focus_path='buildingplan/itemselection', @@ -311,7 +381,48 @@ function ItemSelectionScreen:init() end -------------------------------- --- PlannerOverlay +-- FilterSelection +-- + +-- returns whether the items matched by the specified filter can have a quality +-- rating. This also conveniently indicates whether an item can be decorated. +local function can_be_improved(idx) + local filter = get_cur_filters()[idx] + if filter.flags2 and filter.flags2.building_material then + return false; + end + return filter.item_type ~= df.item_type.WOOD and + filter.item_type ~= df.item_type.BLOCKS and + filter.item_type ~= df.item_type.BAR and + filter.item_type ~= df.item_type.BOULDER +end + +FilterSelection = defclass(FilterSelection, widgets.Window) +FilterSelection.ATTRS{ + frame_title='Choose filters', + frame={w=60, h=40, l=4, t=8}, + draggable=false, + resizable=true, + index=DEFAULT_NIL, +} + +function FilterSelection:init() +end + +FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) +FilterSelectionScreen.ATTRS { + focus_path='buildingplan/filterselection', + index=DEFAULT_NIL, +} + +function FilterSelectionScreen:init() + self:addviews{ + FilterSelection{index=self.index} + } +end + +-------------------------------- +-- ItemLine -- local function cur_building_has_no_area() @@ -512,6 +623,10 @@ local function get_placement_errors() return out end +-------------------------------- +-- PlannerOverlay +-- + PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ default_pos={x=5,y=9}, @@ -932,7 +1047,7 @@ function PlannerOverlay:place_building(pos, chosen_items) end -------------------------------- --- InspectorOverlay +-- InspectorLine -- local function get_building_filters() @@ -981,6 +1096,10 @@ function InspectorLine:reset() self.status = nil end +-------------------------------- +-- InspectorOverlay +-- + InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, @@ -1053,19 +1172,4 @@ OVERLAY_WIDGETS = { inspector=InspectorOverlay, } --- returns whether the items matched by the specified filter can have a quality --- rating. This also conveniently indicates whether an item can be decorated. --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function item_can_be_improved(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) - if filter.flags2 and filter.flags2.building_material then - return false; - end - return filter.item_type ~= df.item_type.WOOD and - filter.item_type ~= df.item_type.BLOCKS and - filter.item_type ~= df.item_type.BAR and - filter.item_type ~= df.item_type.BOULDER -end - return _ENV