allow singleton selection for items

develop
Myk Taylor 2023-02-19 21:17:03 -08:00
parent e9555c29be
commit 348ac55f4c
No known key found for this signature in database
1 changed files with 171 additions and 67 deletions

@ -131,6 +131,15 @@ local function get_selected_item_pen()
return SELECTED_ITEM_PEN return SELECTED_ITEM_PEN
end 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 -- ItemSelection
-- --
@ -154,15 +163,16 @@ function ItemSelection:init()
self.quantity = get_quantity(filter) self.quantity = get_quantity(filter)
self.num_selected = 0 self.num_selected = 0
self.selected_set = {} self.selected_set = {}
local plural = self.quantity == 1 and '' or 's'
self:addviews{ self:addviews{
widgets.Label{ widgets.Label{
frame={t=0, l=0, r=10}, frame={t=0, l=0, r=10},
text={ text={
get_desc(filter), get_desc(filter),
self.quantity == 1 and '' or 's', plural,
NEWLINE, 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}, {text=function() return self.num_selected end},
' selected)', ' selected)',
}, },
@ -179,11 +189,52 @@ function ItemSelection:init()
on_click=self:callback('submit'), on_click=self:callback('submit'),
}, },
widgets.FilteredList{ 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, case_sensitive=false,
choices=self:get_choices(), choices=self:get_choices(),
icon_width=2, 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 end
@ -199,67 +250,79 @@ end
function ItemSelection:get_choices() function ItemSelection:get_choices()
local item_ids = getAvailableItems(uibs.building_type, local item_ids = getAvailableItems(uibs.building_type,
uibs.building_subtype, uibs.custom_type, self.index - 1) uibs.building_subtype, uibs.custom_type, self.index - 1)
local buckets, selected_buckets = {}, {} local buckets = {}
for _,item_id in ipairs(item_ids) do for _,item_id in ipairs(item_ids) do
local item = df.item.find(item_id) local item = df.item.find(item_id)
if not item then goto continue end if not item then goto continue end
local desc = dfhack.items.getDescription(item, 0, true) local desc = dfhack.items.getDescription(item, 0, true)
if buckets[desc] then if buckets[desc] then
local bucket = buckets[desc] local bucket = buckets[desc]
table.insert(bucket.item_ids, item_id) table.insert(bucket.data.item_ids, item_id)
bucket.quantity = bucket.quantity + 1 bucket.data.quantity = bucket.data.quantity + 1
else else
local entry = { local entry = {
text=desc,
search_key=make_search_key(desc), search_key=make_search_key(desc),
icon=self:callback('get_entry_icon', item_id), icon=self:callback('get_entry_icon', item_id),
data={
item_ids={item_id}, item_ids={item_id},
item_type=item:getType(), item_type=item:getType(),
item_subtype=item:getSubtype(), item_subtype=item:getSubtype(),
quantity=1, quantity=1,
selected=false, quality=item:getQuality(),
selected=0,
},
} }
buckets[desc] = entry buckets[desc] = entry
end end
::continue:: ::continue::
end 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 = {} local choices = {}
for _,choice in pairs(buckets) do for desc,choice in pairs(buckets) do
choice.text = ('(%d) %s'):format(choice.quantity, choice.text) 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) table.insert(choices, choice)
end end
local function choice_sort(a, b) local function choice_sort(a, b)
return a.item_type < b.item_type or local ad, bd = a.data, b.data
(a.item_type == b.item_type and a.item_subtype < b.item_subtype) or return ad.item_type < bd.item_type or
(a.item_type == b.item_type and a.item_subtype == b.item_subtype and a.search_key < b.search_key) (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 end
table.sort(choices, choice_sort) table.sort(choices, choice_sort)
return choices return choices
end end
function ItemSelection:toggle_item(_, choice) function ItemSelection:increment_group(idx, choice)
if choice.selected then local data = choice.data
for _,item_id in ipairs(choice.item_ids) do if self.quantity <= self.num_selected then return false end
self.selected_set[item_id] = nil if data.selected >= data.quantity then return false end
end data.selected = data.selected + 1
self.num_selected = self.num_selected - choice.quantity self.num_selected = self.num_selected + 1
choice.selected = false local item_id = data.item_ids[data.selected]
elseif self.quantity > self.num_selected then
for _,item_id in ipairs(choice.item_ids) do
self.selected_set[item_id] = true self.selected_set[item_id] = true
end return true
self.num_selected = self.num_selected + choice.quantity end
choice.selected = true
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
end end
@ -279,19 +342,26 @@ function ItemSelection:onInput(keys)
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self.on_cancel() self.on_cancel()
return true 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 end
return ItemSelection.super.onInput(self, keys) return ItemSelection.super.onInput(self, keys)
end 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 = defclass(ItemSelectionScreen, BuildingplanScreen)
ItemSelectionScreen.ATTRS { ItemSelectionScreen.ATTRS {
focus_path='buildingplan/itemselection', focus_path='buildingplan/itemselection',
@ -311,7 +381,48 @@ function ItemSelectionScreen:init()
end 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() local function cur_building_has_no_area()
@ -512,6 +623,10 @@ local function get_placement_errors()
return out return out
end end
--------------------------------
-- PlannerOverlay
--
PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget)
PlannerOverlay.ATTRS{ PlannerOverlay.ATTRS{
default_pos={x=5,y=9}, default_pos={x=5,y=9},
@ -932,7 +1047,7 @@ function PlannerOverlay:place_building(pos, chosen_items)
end end
-------------------------------- --------------------------------
-- InspectorOverlay -- InspectorLine
-- --
local function get_building_filters() local function get_building_filters()
@ -981,6 +1096,10 @@ function InspectorLine:reset()
self.status = nil self.status = nil
end end
--------------------------------
-- InspectorOverlay
--
InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget)
InspectorOverlay.ATTRS{ InspectorOverlay.ATTRS{
default_pos={x=-41,y=14}, default_pos={x=-41,y=14},
@ -1053,19 +1172,4 @@ OVERLAY_WIDGETS = {
inspector=InspectorOverlay, 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 return _ENV