From ad873cfe83813e487ebca0300516c5008e14c5c7 Mon Sep 17 00:00:00 2001 From: AdamZ Date: Tue, 25 Aug 2026 16:57:59 -0700 Subject: [PATCH 1/3] Improve Items tab filtering and sorting Expand the Items tab layout and add non-destructive filtering and sorting views while preserving the custom item order. Keep newly added items visible and selected across each item-add path. --- spec/System/TestItemDBControl_spec.lua | 62 ++++ spec/System/TestItemListControl_spec.lua | 227 ++++++++++++++- src/Classes/ItemDBControl.lua | 39 ++- src/Classes/ItemListControl.lua | 349 ++++++++++++++++++----- src/Classes/ItemSlotControl.lua | 10 +- src/Classes/ItemsTab.lua | 103 +++---- src/Classes/SharedItemListControl.lua | 4 +- 7 files changed, 633 insertions(+), 161 deletions(-) diff --git a/spec/System/TestItemDBControl_spec.lua b/spec/System/TestItemDBControl_spec.lua index 98d4605c4a2..9e7efd18619 100644 --- a/spec/System/TestItemDBControl_spec.lua +++ b/spec/System/TestItemDBControl_spec.lua @@ -1,12 +1,15 @@ describe("ItemDBControl", function() local originalGetCursorPos + local originalIsKeyDown before_each(function() originalGetCursorPos = GetCursorPos + originalIsKeyDown = _G.IsKeyDown end) after_each(function() GetCursorPos = originalGetCursorPos + _G.IsKeyDown = originalIsKeyDown end) it("sorts lower-is-better stats below zero", function() @@ -114,4 +117,63 @@ describe("ItemDBControl", function() assert.are.equal(item.raw, itemsTab.displayRaw) assert.is_true(itemsTab.displayIsUnique) end) + + it("resizes its filter controls with the item list", function() + local width = 360 + local control = new("ItemDBControl"):ItemDBControl(nil, { 0, 0, function() return width end, 100 }, { + build = { outputRevision = 1 }, + }, { + list = { }, + }, "RARE") + local viewPort = { x = 0, y = 0, width = 1920, height = 1080 } + + control:Draw(viewPort) + assert.equal(179, control.controls.slot.width) + assert.equal(258, control.controls.search.width) + + width = 450 + control:Draw(viewPort) + assert.equal(224, control.controls.slot.width) + assert.equal(348, control.controls.search.width) + end) + + it("selects a newly added item after equipping it", function() + local equipped + local counterpartAdded + local slotsPopulated + local selectedItemId + local item = { raw = "Rarity: Normal\nPlate Vest" } + local itemsTab = { + activeItemSet = { useSecondWeaponSet = false }, + slots = { + ["Body Armour"] = { + weaponSet = 0, + SetSelItemId = function() equipped = true end, + }, + }, + controls = { + itemList = { + SelectItem = function(_, itemId) + assert.is_true(equipped) + assert.is_true(counterpartAdded) + assert.is_true(slotsPopulated) + selectedItemId = itemId + end, + }, + }, + build = { }, + AddItem = function(_, newItem) + newItem.id = 1 + end, + AddForbiddenJewelCounterpart = function() counterpartAdded = true end, + PopulateSlots = function() slotsPopulated = true end, + AddUndoState = function() end, + } + local control = new("ItemDBControl"):ItemDBControl(nil, { 0, 0, 100, 100 }, itemsTab, { list = { item } }, "RARE") + _G.IsKeyDown = function(key) return key == "CTRL" end + + control:OnSelClick(1, item, false) + + assert.equal(1, selectedItemId) + end) end) diff --git a/spec/System/TestItemListControl_spec.lua b/spec/System/TestItemListControl_spec.lua index 2d690a6eb86..c41ce97a62c 100644 --- a/spec/System/TestItemListControl_spec.lua +++ b/spec/System/TestItemListControl_spec.lua @@ -1,8 +1,21 @@ describe("ItemListControl", function() local originalOpenConfirmPopup local originalGetCursorPos + local originalPortraitMode local function newItemListControl() + local function newItem(id, name, type, primarySlot, rarity) + return { + id = id, + name = name, + type = type, + rarity = rarity or "NORMAL", + base = { subType = "" }, + GetPrimarySlot = function() + return primarySlot + end, + } + end local activeItemSet = { id = 1, title = "Boss", @@ -30,11 +43,12 @@ describe("ItemListControl", function() } local itemsTab = { itemOrderList = { 1, 2, 3, 4 }, + slotOrder = { ["Body Armour"] = 1, Jewel = 2 }, items = { - [1] = { id = 1, type = "Body Armour", base = { subType = "" } }, - [2] = { id = 2, type = "Body Armour", base = { subType = "" } }, - [3] = { id = 3, type = "Jewel", base = { subType = "" } }, - [4] = { id = 4, type = "Jewel", base = { subType = "" } }, + [1] = newItem(1, "Armour One", "Body Armour", "Body Armour"), + [2] = newItem(2, "Armour Two", "Body Armour", "Body Armour"), + [3] = newItem(3, "Jewel One", "Jewel", "Jewel"), + [4] = newItem(4, "Jewel Two", "Jewel", "Jewel"), }, itemSetOrderList = { 1, 2 }, itemSets = { activeItemSet, otherItemSet }, @@ -51,6 +65,13 @@ describe("ItemListControl", function() }, PopulateSlots = function() end, AddUndoState = function() end, + GetEquippedSlotForItem = function(_, item) + if item.id == 1 then + return { slotName = "Body Armour" } + elseif item.id == 2 then + return { slotName = "Body Armour" }, otherItemSet + end + end, } local control = new("ItemListControl"):ItemListControl(nil, { 0, 0, 360, 308 }, itemsTab, true) return control, itemsTab, treeTab @@ -59,11 +80,13 @@ describe("ItemListControl", function() before_each(function() originalOpenConfirmPopup = main.OpenConfirmPopup originalGetCursorPos = GetCursorPos + originalPortraitMode = main.portraitMode end) after_each(function() main.OpenConfirmPopup = originalOpenConfirmPopup GetCursorPos = originalGetCursorPos + main.portraitMode = originalPortraitMode end) it("only shows items from the active item set and passive tree", function() @@ -126,12 +149,15 @@ describe("ItemListControl", function() assert.are.equal(3, control.selValue) end) - it("only allows internal reordering in the unfiltered item list", function() + it("uses the mutable custom order by default", function() local control = newItemListControl() control:UpdateLoadoutList() control:UpdateList() + assert.equal("Custom Order", control.controls.sortMode:GetSelValue()) assert.is_true(control.isMutable) + assert.is_true(rawequal(control.list, control.itemsTab.itemOrderList)) + assert.are.same({ 1, 2, 3, 4 }, control.itemsTab.itemOrderList) control.controls.loadoutFilter.selIndex = 2 control:UpdateList() @@ -142,6 +168,197 @@ describe("ItemListControl", function() control:UpdateList() assert.is_true(control.isMutable) + assert.is_true(rawequal(control.list, control.itemsTab.itemOrderList)) + assert.are.same({ 1, 2, 3, 4 }, control.itemsTab.itemOrderList) + end) + + it("keeps sorted views separate from the custom order", function() + local control, itemsTab = newItemListControl() + itemsTab.items[1].name = "D Item" + itemsTab.items[2].name = "A Item" + itemsTab.items[3].name = "B Item" + itemsTab.items[4].name = "C Item" + control.controls.sortMode:SelByValue("Sort by Name") + + control:UpdateList() + + assert.is_false(control.isMutable) + assert.are.same({ 2, 3, 4, 1 }, control.list) + assert.are.same({ 1, 2, 3, 4 }, control.itemsTab.itemOrderList) + end) + + it("appends drops from sorted views to the custom order", function() + local control, itemsTab = newItemListControl() + local insertionIndex + itemsTab.AddItem = function(self, item, _, index) + insertionIndex = index + item.id = 5 + self.items[item.id] = item + table.insert(self.itemOrderList, index or #self.itemOrderList + 1, item.id) + end + itemsTab.AddForbiddenJewelCounterpart = function() end + control.controls.sortMode:SelByValue("Sort by Name") + control:UpdateList() + control.selDragIndex = 2 + + control:ReceiveDrag("Item", { raw = "Rarity: Normal\nPlate Vest" }) + + assert.is_nil(insertionIndex) + assert.are.same({ 1, 2, 3, 4, 5 }, itemsTab.itemOrderList) + end) + + it("filters by primary item slot and item name", function() + local control, itemsTab = newItemListControl() + itemsTab.items[1].name = "Matching Body Armour" + itemsTab.items[2].name = "Other Body Armour" + itemsTab.items[3].name = "Matching Jewel" + control.controls.slotFilter.selIndex = isValueInArray(control.controls.slotFilter.list, "Body Armour") + control.controls.search.buf = "matching" + + control:UpdateList() + + assert.are.same({ 1 }, control.list) + end) + + it("sorts by item-slot order and then by name", function() + local control, itemsTab = newItemListControl() + itemsTab.items[1].name = "Later Body Armour" + itemsTab.items[2].name = "Earlier Body Armour" + itemsTab.items[3].name = "Later Jewel" + itemsTab.items[4].name = "Earlier Jewel" + control.controls.sortMode:SelByValue("Sort by Item Slot") + + control:UpdateList() + + assert.are.same({ 2, 1, 4, 3 }, control.list) + end) + + it("sorts unique, rare, magic, and normal items in rarity order", function() + local control, itemsTab = newItemListControl() + itemsTab.items[1].rarity = "MAGIC" + itemsTab.items[2].rarity = "NORMAL" + itemsTab.items[3].rarity = "UNIQUE" + itemsTab.items[4].rarity = "RARE" + control.controls.sortMode:SelByValue("Sort by Rarity") + + control:UpdateList() + + assert.are.same({ 3, 4, 1, 2 }, control.list) + end) + + it("sorts loadout groups in menu order while preferring the current loadout", function() + local control, itemsTab = newItemListControl() + itemsTab.items[1].name = "Later Boss Armour" + itemsTab.items[2].name = "Mapping Armour" + itemsTab.items[3].name = "Boss Jewel" + itemsTab.items[4].name = "Mapping Jewel" + itemsTab.itemSets[2].Gloves = { selItemId = 1 } + itemsTab.build.controls.buildLoadouts.list = { "Mapping", "Boss" } + control.controls.sortMode:SelByValue("Sort by Loadout") + + control:UpdateList() + + assert.are.same({ + { groupHeader = "Mapping" }, 2, 4, + { groupHeader = "Boss" }, 3, 1, + }, control.list) + end) + + it("does not label items used outside complete loadouts as unused", function() + local control, itemsTab, treeTab = newItemListControl() + itemsTab.items[5] = { + id = 5, + name = "Experimental Jewel", + type = "Jewel", + rarity = "RARE", + base = { subType = "" }, + GetPrimarySlot = function() return "Jewel" end, + } + itemsTab.items[6] = { + id = 6, + name = "Unused Jewel", + type = "Jewel", + rarity = "RARE", + base = { subType = "" }, + GetPrimarySlot = function() return "Jewel" end, + } + table.insert(itemsTab.itemOrderList, 5) + table.insert(itemsTab.itemOrderList, 6) + table.insert(treeTab.specList, { + title = "Experimental", + jewels = { [300] = 5 }, + nodes = { [300] = { alloc = true } }, + }) + control.controls.sortMode:SelByValue("Sort by Loadout") + + control:UpdateList() + + assert.are.same({ + { groupHeader = "Boss" }, 1, 3, + { groupHeader = "Mapping" }, 2, 4, + { groupHeader = "Other Used Items" }, 5, + { groupHeader = "Unused Items" }, 6, + }, control.list) + end) + + it("does not allow group headers to become selected", function() + local control = newItemListControl() + control.controls.sortMode:SelByValue("Sort by Loadout") + control:UpdateList() + + assert.is_false(control:SelectIndex(1)) + assert.is_nil(control.selIndex) + assert.is_nil(control.selValue) + end) + + it("clears filters that hide a selected item", function() + local control = newItemListControl() + control.controls.slotFilter.selIndex = 4 + control.controls.loadoutFilter.selIndex = 2 + control.controls.search.buf = "missing" + control.controls.sortMode:SelByValue("Sort by Item Slot") + + control:SelectItem(4) + + assert.equal(1, control.controls.slotFilter.selIndex) + assert.equal(1, control.controls.loadoutFilter.selIndex) + assert.equal("", control.controls.search.buf) + assert.equal(4, control.selValue) + assert.equal(4, control.selIndex) + end) + + it("skips group headers during keyboard navigation", function() + local control = newItemListControl() + control.controls.sortMode:SelByValue("Sort by Loadout") + control:UpdateList() + + control:OnKeyDown("HOME") + assert.equal(1, control.selValue) + control:OnKeyDown("DOWN") + assert.equal(3, control.selValue) + control:OnKeyDown("DOWN") + assert.equal(2, control.selValue) + control:OnKeyDown("UP") + assert.equal(3, control.selValue) + control:OnKeyDown("END") + assert.equal(4, control.selValue) + end) + + it("resizes list controls when the orientation changes", function() + local control = newItemListControl() + control.width = function() return main.portraitMode and 360 or 450 end + local viewPort = { x = 0, y = 0, width = 1920, height = 1080 } + + main.portraitMode = false + control:Draw(viewPort) + assert.equal((450 - 8) / 3, control.controls.slotFilter.width) + assert.equal(450, control.controls.search.width) + + main.portraitMode = true + control:Draw(viewPort) + assert.equal((360 - 8) / 3, control.controls.slotFilter.width) + assert.equal(360, control.controls.search.width) + assert.equal(-44, control.controls.deleteUnused.y) end) it("refreshes filter options when loadouts are renamed without a new output revision", function() diff --git a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua index 37b3ecee3db..fb2eecf018a 100644 --- a/src/Classes/ItemDBControl.lua +++ b/src/Classes/ItemDBControl.lua @@ -31,27 +31,29 @@ function ItemDBClass:ItemDBControl(anchor, rect, itemsTab, db, dbType) self.typeList = { "Any type", "Armour", "Jewellery", "One Handed Melee", "Two Handed Melee" } self.slotList = { "Any slot", "Weapon 1", "Weapon 2", "Helmet", "Body Armour", "Gloves", "Boots", "Amulet", "Ring", "Belt", "Jewel", "Flask", "Graft 1", "Graft 2" } local baseY = dbType == "RARE" and -22 or -62 - self.controls.slot = new("DropDownControl"):DropDownControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, baseY, 179, 18}, self.slotList, function(index, value) + local width = self:GetProperty("width") + local filterWidth = (width - 2) / 2 + self.controls.slot = new("DropDownControl"):DropDownControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, baseY, filterWidth, 18}, self.slotList, function(index, value) self.listBuildFlag = true end) - self.controls.type = new("DropDownControl"):DropDownControl({"LEFT",self.controls.slot,"RIGHT"}, {2, 0, 179, 18}, self.typeList, function(index, value) + self.controls.type = new("DropDownControl"):DropDownControl({"LEFT",self.controls.slot,"RIGHT"}, {2, 0, filterWidth, 18}, self.typeList, function(index, value) self.listBuildFlag = true end) if dbType == "UNIQUE" then - self.controls.sort = new("DropDownControl"):DropDownControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, baseY + 20, 179, 18}, self.sortDropList, function(index, value) + self.controls.sort = new("DropDownControl"):DropDownControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, baseY + 20, filterWidth, 18}, self.sortDropList, function(index, value) self:SetSortMode(value.sortMode) end) - self.controls.league = new("DropDownControl"):DropDownControl({"LEFT",self.controls.sort,"RIGHT"}, {2, 0, 179, 18}, self.leagueList, function(index, value) + self.controls.league = new("DropDownControl"):DropDownControl({"LEFT",self.controls.sort,"RIGHT"}, {2, 0, filterWidth, 18}, self.leagueList, function(index, value) self.listBuildFlag = true end) - self.controls.requirement = new("DropDownControl"):DropDownControl({"LEFT",self.controls.sort,"BOTTOMLEFT"}, {0, 11, 179, 18}, { "Any requirements", "Current level", "Current attributes", "Current useable" }, function(index, value) + self.controls.requirement = new("DropDownControl"):DropDownControl({"LEFT",self.controls.sort,"BOTTOMLEFT"}, {0, 11, filterWidth, 18}, { "Any requirements", "Current level", "Current attributes", "Current useable" }, function(index, value) self.listBuildFlag = true end) - self.controls.obtainable = new("DropDownControl"):DropDownControl({"LEFT",self.controls.requirement,"RIGHT"}, {2, 0, 179, 18}, { "Obtainable", "Any source", "Unobtainable", "Vendor Recipe", "Upgraded", "Boss Item", "Corruption", "Core Drop Pool"}, function(index, value) + self.controls.obtainable = new("DropDownControl"):DropDownControl({"LEFT",self.controls.requirement,"RIGHT"}, {2, 0, filterWidth, 18}, { "Obtainable", "Any source", "Unobtainable", "Vendor Recipe", "Upgraded", "Boss Item", "Corruption", "Core Drop Pool"}, function(index, value) self.listBuildFlag = true end) end - self.controls.search = new("EditControl"):EditControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -2, 258, 18}, "", "Search", "%c", 100, function() + self.controls.search = new("EditControl"):EditControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -2, m_max(width - 102, 0), 18}, "", "Search", "%c", 100, function() self.listBuildFlag = true end, nil, nil, true) self.controls.searchMode = new("DropDownControl"):DropDownControl({"LEFT",self.controls.search,"RIGHT"}, {2, 0, 100, 18}, { "Anywhere", "Names", "Modifiers" }, function(index, value) @@ -291,6 +293,28 @@ function ItemDBClass:ListBuilder() end function ItemDBClass:Draw(viewPort) + local width = self:GetProperty("width") + local filterWidth = (width - 2) / 2 + local widthChanged = self.controls.slot.width ~= filterWidth + self.controls.slot.width = filterWidth + self.controls.type.width = filterWidth + if self.dbType == "UNIQUE" then + self.controls.sort.width = filterWidth + self.controls.league.width = filterWidth + self.controls.requirement.width = filterWidth + self.controls.obtainable.width = filterWidth + end + self.controls.search.width = m_max(width - 102, 0) + if widthChanged then + self.controls.slot:CheckDroppedWidth(false) + self.controls.type:CheckDroppedWidth(false) + if self.dbType == "UNIQUE" then + self.controls.sort:CheckDroppedWidth(false) + self.controls.league:CheckDroppedWidth(false) + self.controls.requirement:CheckDroppedWidth(false) + self.controls.obtainable:CheckDroppedWidth(false) + end + end if self.itemsTab.build.outputRevision ~= self.listOutputRevision then self.listBuildFlag = true end @@ -364,6 +388,7 @@ function ItemDBClass:OnSelClick(index, item, doubleClick) self.itemsTab:AddForbiddenJewelCounterpart(newItem) self.itemsTab:PopulateSlots() + self.itemsTab.controls.itemList:SelectItem(newItem.id) self.itemsTab:AddUndoState() self.itemsTab.build.buildFlag = true elseif doubleClick then diff --git a/src/Classes/ItemListControl.lua b/src/Classes/ItemListControl.lua index ee50d6371b5..7611f8ae21a 100644 --- a/src/Classes/ItemListControl.lua +++ b/src/Classes/ItemListControl.lua @@ -6,6 +6,18 @@ local pairs = pairs local ipairs = ipairs local t_insert = table.insert +local t_sort = table.sort + +local slotFilterList = { "Any Slot", "Weapon 1", "Weapon 2", "Helmet", "Body Armour", "Gloves", "Boots", "Amulet", "Ring", "Belt", "Graft", "Flask", "Jewel" } +local raritySortOrder = { UNIQUE = 1, RELIC = 1, RARE = 2, MAGIC = 3, NORMAL = 4 } + +local function isGroupHeader(value) + return type(value) == "table" and value.groupHeader +end + +local function getItemName(item) + return (item.name or item.title or ""):lower() +end ---@class ItemListControl: ListControl local ItemListClass = newClass("ItemListControl", "ListControl") @@ -15,15 +27,22 @@ function ItemListClass:ItemListControl(anchor, rect, itemsTab, forceTooltip) self.itemsTab = itemsTab self.defaultText = "^x7F7F7FThis is the list of items that have been added to this build.\nYou can add items to this list by dragging them from\none of the other lists, or by clicking 'Add to build' when\nviewing an item." self.dragTargetList = { } - self.controls.loadoutFilter = new("DropDownControl"):DropDownControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -2, 110, 18}, nil, function() + local width = self:GetProperty("width") + local rowControlWidth = (width - 8) / 3 + self.controls.slotFilter = new("DropDownControl"):DropDownControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -22, rowControlWidth, 18}, slotFilterList, function() self:UpdateList() end) - self.controls.loadoutFilter.enableDroppedWidth = true - self.controls.sort = new("ButtonControl"):ButtonControl({"LEFT",self.controls.loadoutFilter,"RIGHT"}, {4, 0, 42, 18}, "Sort", function() - itemsTab:SortItemList() + self.controls.sortMode = new("DropDownControl"):DropDownControl({"LEFT",self.controls.slotFilter,"RIGHT"}, {4, 0, rowControlWidth, 18}, { "Custom Order", "Sort by Item Slot", "Sort by Name", "Sort by Rarity", "Sort by Loadout" }, function() + self:UpdateList() + end) + self.controls.loadoutFilter = new("DropDownControl"):DropDownControl({"LEFT",self.controls.sortMode,"RIGHT"}, {4, 0, rowControlWidth, 18}, nil, function() self:UpdateList() end) - self.controls.deleteUnused = new("ButtonControl"):ButtonControl({"LEFT",self.controls.sort,"RIGHT"}, {4, 0, 84, 18}, "Del Unused", function() + self.controls.loadoutFilter.enableDroppedWidth = true + self.controls.search = new("EditControl"):EditControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -2, width, 18}, "", "Search", "%c", 100, function() + self:UpdateList() + end, nil, nil, true) + self.controls.deleteUnused = new("ButtonControl"):ButtonControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -50, rowControlWidth, 20}, "Delete Unused", function() local delList = {} for _, itemId in pairs(itemsTab.itemOrderList) do if not itemsTab:GetEquippedSlotForItem(itemsTab.items[itemId]) and not self:FindEquippedAbyssJewel(itemId, false) and not self:FindSocketedJewel(itemId, false) then @@ -44,9 +63,9 @@ function ItemListClass:ItemListControl(anchor, rect, itemsTab, forceTooltip) self:UpdateList() end) self.controls.deleteUnused.enabled = function() - return #self.list > 0 + return #itemsTab.itemOrderList > 0 end - self.controls.deleteAll = new("ButtonControl"):ButtonControl({"LEFT",self.controls.deleteUnused,"RIGHT"}, {4, 0, 58, 18}, "Del All", function() + self.controls.deleteAll = new("ButtonControl"):ButtonControl({"LEFT",self.controls.deleteUnused,"RIGHT"}, {4, 0, rowControlWidth, 20}, "Delete All", function() main:OpenConfirmPopup("Delete All", "Are you sure you want to delete all items in this build?", "Delete", function() for _, slot in pairs(itemsTab.slots) do slot:SetSelItemId(0) @@ -67,13 +86,13 @@ function ItemListClass:ItemListControl(anchor, rect, itemsTab, forceTooltip) end) end) self.controls.deleteAll.enabled = function() - return #self.list > 0 + return #itemsTab.itemOrderList > 0 end - self.controls.delete = new("ButtonControl"):ButtonControl({"LEFT",self.controls.deleteAll,"RIGHT"}, {4, 0, 50, 18}, "Delete", function() + self.controls.delete = new("ButtonControl"):ButtonControl({"LEFT",self.controls.deleteAll,"RIGHT"}, {4, 0, rowControlWidth, 20}, "Delete", function() self:OnSelDelete(self.selIndex, self.selValue) end) self.controls.delete.enabled = function() - return self.selValue ~= nil + return type(self.selValue) == "number" end return self end @@ -84,7 +103,7 @@ function ItemListClass:UpdateLoadoutList() local build = self.itemsTab.build if build and build.controls and build.controls.buildLoadouts then for _, val in ipairs(build.controls.buildLoadouts.list) do - if val ~= "^7^7Loadouts:" and val ~= "^7^7-----" and val ~= "^7^7New Loadout" and val ~= "^7^7Sync" and val ~= "^7^7Help >>" then + if val ~= "No Loadouts" and val ~= "^7^7Loadouts:" and val ~= "^7^7-----" and val ~= "^7^7New Loadout" and val ~= "^7^7Sync" and val ~= "^7^7Help >>" then if not listValues[val] then t_insert(list, val) listValues[val] = true @@ -114,82 +133,208 @@ function ItemListClass:UpdateLoadoutList() return true end -function ItemListClass:UpdateList() - self:UpdateLoadoutList() - local selFilter = self.controls.loadoutFilter.selIndex or 1 - local filterVal = self.controls.loadoutFilter.list[selFilter] or "Any Loadout" - local selectedItemId = self.selValue +function ItemListClass:GetLoadoutSetAndSpec(loadoutName) + local itemSet + local spec + local filterTitle = loadoutName:gsub("^%[[^%]]+%]%s*", "") + for _, itemSetId in ipairs(self.itemsTab.itemSetOrderList) do + local candidate = self.itemsTab.itemSets[itemSetId] + if (candidate.title or "Default") == filterTitle then + itemSet = candidate + break + end + end + local treeTab = self.itemsTab.build.treeTab + for _, candidate in ipairs(treeTab.specList) do + if (candidate.title or "Default") == filterTitle then + spec = candidate + break + end + end + local linkId = loadoutName:match("%{(%w+)%}") + local itemLink = linkId and self.itemsTab.build.itemListSpecialLinks and self.itemsTab.build.itemListSpecialLinks[linkId] + local treeLink = linkId and self.itemsTab.build.treeListSpecialLinks and self.itemsTab.build.treeListSpecialLinks[linkId] + itemSet = itemSet or #self.itemsTab.itemSetOrderList == 1 and self.itemsTab.itemSets[self.itemsTab.itemSetOrderList[1]] or itemLink and self.itemsTab.itemSets[itemLink.setId] + spec = spec or #treeTab.specList == 1 and treeTab.specList[1] or treeLink and treeTab.specList[treeLink.setId] + return itemSet or { }, spec +end - if selFilter == 1 or filterVal == "Any Loadout" then - self.list = self.itemsTab.itemOrderList - self.isMutable = true - else - self.isMutable = false - local filterItemSet - local filterSpec - if selFilter == 2 or filterVal == "Current Loadout" then - filterItemSet = self.itemsTab.activeItemSet - filterSpec = self.itemsTab.build.treeTab.specList[self.itemsTab.build.treeTab.activeSpec] - elseif selFilter ~= 3 and filterVal ~= "Unused Items" then - local filterTitle = filterVal:gsub("^%[[^%]]+%]%s*", "") - for _, itemSetId in ipairs(self.itemsTab.itemSetOrderList) do - local itemSet = self.itemsTab.itemSets[itemSetId] - if (itemSet.title or "Default") == filterTitle then - filterItemSet = itemSet - break - end +function ItemListClass:IsItemInLoadout(itemId, itemSet, spec) + for _, slot in pairs(itemSet) do + if type(slot) == "table" and slot.selItemId == itemId then + return true + end + end + if spec and spec.jewels then + for nodeId, jewelId in pairs(spec.jewels) do + if jewelId == itemId and spec.nodes[nodeId] and spec.nodes[nodeId].alloc then + return true + end + end + end + return false +end + +function ItemListClass:SortItems(itemList, canonicalOrder, sortMode) + t_sort(itemList, function(a, b) + local itemA = self.itemsTab.items[a] + local itemB = self.itemsTab.items[b] + if sortMode == "Sort by Item Slot" then + local orderA = self.itemsTab.slotOrder[itemA:GetPrimarySlot()] or math.huge + local orderB = self.itemsTab.slotOrder[itemB:GetPrimarySlot()] or math.huge + if orderA ~= orderB then + return orderA < orderB + end + elseif sortMode == "Sort by Rarity" then + local orderA = raritySortOrder[itemA.rarity] or math.huge + local orderB = raritySortOrder[itemB.rarity] or math.huge + if orderA ~= orderB then + return orderA < orderB end - local treeTab = self.itemsTab.build.treeTab - for _, spec in ipairs(treeTab.specList) do - if (spec.title or "Default") == filterTitle then - filterSpec = spec + end + local nameA = getItemName(itemA) + local nameB = getItemName(itemB) + return nameA == nameB and canonicalOrder[a] < canonicalOrder[b] or nameA < nameB + end) +end + +function ItemListClass:BuildLoadoutSort(itemList, canonicalOrder) + local groups = { } + local currentGroup + local currentSpec = self.itemsTab.build.treeTab.specList[self.itemsTab.build.treeTab.activeSpec] + for index = 4, #self.controls.loadoutFilter.list do + local loadoutName = self.controls.loadoutFilter.list[index] + local itemSet, spec = self:GetLoadoutSetAndSpec(loadoutName) + local group = { label = loadoutName, itemSet = itemSet, spec = spec, items = { } } + t_insert(groups, group) + if itemSet == self.itemsTab.activeItemSet and spec == currentSpec then + currentGroup = group + end + end + local otherUsed = { } + local unused = { } + for _, itemId in ipairs(itemList) do + local assigned + if currentGroup and self:IsItemInLoadout(itemId, currentGroup.itemSet, currentGroup.spec) then + t_insert(currentGroup.items, itemId) + assigned = true + else + for _, group in ipairs(groups) do + if self:IsItemInLoadout(itemId, group.itemSet, group.spec) then + t_insert(group.items, itemId) + assigned = true break end end - local linkId = filterVal:match("%{(%w+)%}") - local itemLink = linkId and self.itemsTab.build.itemListSpecialLinks and self.itemsTab.build.itemListSpecialLinks[linkId] - local treeLink = linkId and self.itemsTab.build.treeListSpecialLinks and self.itemsTab.build.treeListSpecialLinks[linkId] - filterItemSet = filterItemSet or #self.itemsTab.itemSetOrderList == 1 and self.itemsTab.itemSets[self.itemsTab.itemSetOrderList[1]] or itemLink and self.itemsTab.itemSets[itemLink.setId] - filterSpec = filterSpec or #treeTab.specList == 1 and treeTab.specList[1] or treeLink and treeTab.specList[treeLink.setId] end - filterItemSet = filterItemSet or { } - local newList = {} - for _, itemId in ipairs(self.itemsTab.itemOrderList) do + if not assigned then local item = self.itemsTab.items[itemId] - if item then - if selFilter == 3 or filterVal == "Unused Items" then - if not self.itemsTab:GetEquippedSlotForItem(item) and not self:FindEquippedAbyssJewel(itemId, false) and not self:FindSocketedJewel(itemId, false) then - t_insert(newList, itemId) - end - else - local inLoadout = false - for _, slot in pairs(filterItemSet) do - if type(slot) == "table" and slot.selItemId == itemId then - inLoadout = true - break - end - end - if not inLoadout and filterSpec then - for nodeId, jewelId in pairs(filterSpec.jewels) do - if jewelId == itemId and filterSpec.nodes[nodeId] and filterSpec.nodes[nodeId].alloc then - inLoadout = true - break - end - end - end - if inLoadout then - t_insert(newList, itemId) - end - end + if not self.itemsTab:GetEquippedSlotForItem(item) and not self:FindEquippedAbyssJewel(itemId, false) and not self:FindSocketedJewel(itemId, false) then + t_insert(unused, itemId) + else + t_insert(otherUsed, itemId) end end - self.list = newList + end + local list = { } + local function addGroup(label, items) + if #items > 0 then + self:SortItems(items, canonicalOrder, "Sort by Name") + t_insert(list, { groupHeader = label }) + for _, itemId in ipairs(items) do + t_insert(list, itemId) + end + end + end + for _, group in ipairs(groups) do + addGroup(group.label, group.items) + end + addGroup("Other Used Items", otherUsed) + addGroup("Unused Items", unused) + return list +end + +function ItemListClass:UpdateList() + self:UpdateLoadoutList() + local loadoutFilterIndex = self.controls.loadoutFilter.selIndex or 1 + local loadoutFilter = self.controls.loadoutFilter.list[loadoutFilterIndex] or "Any Loadout" + local slotFilter = self.controls.slotFilter.list[self.controls.slotFilter.selIndex or 1] or "Any Slot" + local searchText = self.controls.search.buf:lower() + local selectedItemId = type(self.selValue) == "number" and self.selValue + local filterItemSet + local filterSpec + if loadoutFilterIndex == 2 then + filterItemSet = self.itemsTab.activeItemSet + filterSpec = self.itemsTab.build.treeTab.specList[self.itemsTab.build.treeTab.activeSpec] + elseif loadoutFilterIndex > 3 then + filterItemSet, filterSpec = self:GetLoadoutSetAndSpec(loadoutFilter) + end + local itemList = { } + local canonicalOrder = { } + for index, itemId in ipairs(self.itemsTab.itemOrderList) do + canonicalOrder[itemId] = index + local item = self.itemsTab.items[itemId] + if item then + local matchesLoadout = loadoutFilterIndex == 1 + or loadoutFilterIndex == 3 and not self.itemsTab:GetEquippedSlotForItem(item) and not self:FindEquippedAbyssJewel(itemId, false) and not self:FindSocketedJewel(itemId, false) + or filterItemSet and self:IsItemInLoadout(itemId, filterItemSet, filterSpec) + local primarySlot = item:GetPrimarySlot() + local matchesSlot = slotFilter == "Any Slot" or primarySlot == slotFilter or primarySlot:gsub(" %d$", "") == slotFilter + local matchesSearch = searchText == "" or getItemName(item):find(searchText, 1, true) + if matchesLoadout and matchesSlot and matchesSearch then + t_insert(itemList, itemId) + end + end + end + local sortMode = self.controls.sortMode.list[self.controls.sortMode.selIndex or 1] or "Custom Order" + if sortMode == "Custom Order" then + local unfiltered = loadoutFilterIndex == 1 and slotFilter == "Any Slot" and searchText == "" + self.list = unfiltered and self.itemsTab.itemOrderList or itemList + self.isMutable = unfiltered + elseif sortMode == "Sort by Loadout" then + self.list = self:BuildLoadoutSort(itemList, canonicalOrder) + self.isMutable = false + else + self:SortItems(itemList, canonicalOrder, sortMode) + self.list = itemList + self.isMutable = false end self.selIndex = selectedItemId and isValueInArray(self.list, selectedItemId) or nil self.selValue = self.selIndex and self.list[self.selIndex] or nil end +function ItemListClass:SelectItem(itemId) + self:UpdateList() + local index = isValueInArray(self.list, itemId) + if not index then + self.controls.slotFilter.selIndex = 1 + self.controls.loadoutFilter.selIndex = 1 + self.controls.search.buf = "" + self:UpdateList() + index = isValueInArray(self.list, itemId) + end + if index then + self:SelectIndex(index) + end +end + function ItemListClass:Draw(viewPort) + local width = self:GetProperty("width") + local rowControlWidth = (width - 8) / 3 + local widthChanged = self.controls.slotFilter.width ~= rowControlWidth + self.controls.slotFilter.width = rowControlWidth + self.controls.sortMode.width = rowControlWidth + self.controls.loadoutFilter.width = rowControlWidth + self.controls.search.width = width + self.controls.deleteUnused.y = main.portraitMode and -44 or -50 + self.controls.deleteUnused.width = rowControlWidth + self.controls.deleteAll.width = rowControlWidth + self.controls.delete.width = rowControlWidth + if widthChanged then + self.controls.slotFilter:CheckDroppedWidth(false) + self.controls.sortMode:CheckDroppedWidth(false) + self.controls.loadoutFilter:CheckDroppedWidth(true) + end local loadoutListChanged = self:UpdateLoadoutList() local outputRevision = self.itemsTab.build and self.itemsTab.build.outputRevision if loadoutListChanged or outputRevision ~= self.lastOutputRevision then @@ -243,9 +388,57 @@ function ItemListClass:FindEquippedAbyssJewel(jewelId, excludeActiveSet) return equipSet end +function ItemListClass:OverrideSelectIndex(index) + if isGroupHeader(self.list[index]) then + self.selIndex = nil + self.selValue = nil + return true + end + return false +end + +function ItemListClass:OnKeyDown(key, doubleClick) + if not self:IsShown() or not self:IsEnabled() then + return + end + local mouseOverControl = self:GetMouseOverControl() + if mouseOverControl and mouseOverControl.OnKeyDown then + return mouseOverControl:OnKeyDown(key) + end + if not self.selDragActive and #self.list > 0 and (key == "UP" or key == "DOWN" or key == "HOME" or key == "END") then + local step = (key == "UP" or key == "END") and -1 or 1 + local index + if key == "HOME" then + index = 1 + elseif key == "END" then + index = #self.list + elseif key == "UP" then + index = (self.selIndex or #self.list + 1) - 1 + else + index = (self.selIndex or 0) + 1 + end + for _ = 1, #self.list do + if index < 1 then + index = #self.list + elseif index > #self.list then + index = 1 + end + if not isGroupHeader(self.list[index]) then + self:SelectIndex(index) + return self + end + index = index + step + end + end + return self.ListControl.OnKeyDown(self, key, doubleClick) +end + function ItemListClass:GetRowValue(column, index, itemId) - local item = self.itemsTab.items[itemId] if column == 1 then + if isGroupHeader(itemId) then + return "^7" .. itemId.groupHeader + end + local item = self.itemsTab.items[itemId] local used = self:FindEquippedAbyssJewel(itemId, true) or self:FindSocketedJewel(itemId, true) or "" if used == "" then local slot, itemSet = self.itemsTab:GetEquippedSlotForItem(item) @@ -262,7 +455,7 @@ function ItemListClass:GetRowValue(column, index, itemId) end function ItemListClass:AddValueTooltip(tooltip, index, itemId) - if main.popups[1] then + if main.popups[1] or isGroupHeader(itemId) then tooltip:Clear() return end @@ -280,11 +473,11 @@ function ItemListClass:ReceiveDrag(type, value, source) if type == "Item" then local newItem = new("Item"):Item(value.raw) newItem:NormaliseQuality() - self.itemsTab:AddItem(newItem, true, self.selDragIndex) + self.itemsTab:AddItem(newItem, true, self.isMutable and self.selDragIndex or nil) self.itemsTab:AddForbiddenJewelCounterpart(newItem) self.itemsTab:PopulateSlots() self.itemsTab:AddUndoState() - self:UpdateList() + self:SelectItem(newItem.id) end end @@ -376,9 +569,9 @@ end function ItemListClass:OnHoverKeyUp(key) if itemLib.wiki.matchesKey(key) then local itemId = self.ListControl:GetHoverValue() - if itemId then + if itemId and not isGroupHeader(itemId) then local item = self.itemsTab.items[itemId] itemLib.wiki.openItem(item) end end -end \ No newline at end of file +end diff --git a/src/Classes/ItemSlotControl.lua b/src/Classes/ItemSlotControl.lua index bdf73ad7505..912dd4e213c 100644 --- a/src/Classes/ItemSlotControl.lua +++ b/src/Classes/ItemSlotControl.lua @@ -12,7 +12,7 @@ local itemSlotHelper = require("Modules.ItemSlotHelper") local ItemSlotClass = newClass("ItemSlotControl", "DropDownControl") function ItemSlotClass:ItemSlotControl(anchor, x, y, itemsTab, slotName, slotLabel, nodeId) - self:DropDownControl(anchor, { x, y, 310, 20 }, {}, function(index, value) + self:DropDownControl(anchor, { x, y, 329, 20 }, {}, function(index, value) if self.items[index] ~= self.selItemId then self:SetSelItemId(self.items[index]) itemsTab:PopulateSlots() @@ -120,16 +120,20 @@ function ItemSlotClass:CanReceiveDrag(type, value) end function ItemSlotClass:ReceiveDrag(type, value, source) + local newItem if value.id and self.itemsTab.items[value.id] then self:SetSelItemId(value.id) else - local newItem = new("Item"):Item(value.raw) + newItem = new("Item"):Item(value.raw) newItem:NormaliseQuality() self.itemsTab:AddItem(newItem, true) self:SetSelItemId(newItem.id) self.itemsTab:AddForbiddenJewelCounterpart(newItem) end self.itemsTab:PopulateSlots() + if newItem then + self.itemsTab.controls.itemList:SelectItem(newItem.id) + end self.itemsTab:AddUndoState() self.itemsTab.build.buildFlag = true end @@ -176,4 +180,4 @@ function ItemSlotClass:OnHoverKeyUp(key) end end end -end \ No newline at end of file +end diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index e6500220202..eb7bd611895 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -108,7 +108,7 @@ function ItemsTabClass:ItemsTab(build) self.tradeQuery = new("TradeQuery"):TradeQuery(self) -- Set selector - self.controls.setSelect = new("DropDownControl"):DropDownControl({"TOPLEFT",self,"TOPLEFT"}, {96, 8, 216, 20}, nil, function(index, value) + self.controls.setSelect = new("DropDownControl"):DropDownControl({"TOPLEFT",self,"TOPLEFT"}, {96, 8, 235, 20}, nil, function(index, value) self:SetActiveItemSet(self.itemSetOrderList[index]) self:AddUndoState() end) @@ -128,7 +128,7 @@ function ItemsTabClass:ItemsTab(build) end) -- Price Items - self.controls.priceDisplayItem = new("ButtonControl"):ButtonControl({"TOPLEFT",self,"TOPLEFT"}, {96, 32, 310, 20}, "Trade for these items", function() + self.controls.priceDisplayItem = new("ButtonControl"):ButtonControl({"TOPLEFT",self,"TOPLEFT"}, {96, 32, 329, 20}, "Trade for these items", function() self.tradeQuery:PriceItem() end) self.controls.priceDisplayItem.tooltipFunc = function(tooltip) @@ -141,7 +141,7 @@ function ItemsTabClass:ItemsTab(build) self.slots = { } self.orderedSlots = { } self.slotOrder = { } - self.slotAnchor = new("Control"):Control({"TOPLEFT",self,"TOPLEFT"}, {96, 76, 310, 0}) + self.slotAnchor = new("Control"):Control({"TOPLEFT",self,"TOPLEFT"}, {96, 76, 329, 0}) local prevSlot = self.slotAnchor local function addSlot(slot) prevSlot = slot @@ -202,7 +202,7 @@ function ItemsTabClass:ItemsTab(build) end -- Passive tree dropdown controls - self.controls.specSelect = new("DropDownControl"):DropDownControl({"TOPLEFT",prevSlot,"BOTTOMLEFT"}, {0, 8, 216, 20}, nil, function(index, value) + self.controls.specSelect = new("DropDownControl"):DropDownControl({"TOPLEFT",prevSlot,"BOTTOMLEFT"}, {0, 8, 235, 20}, nil, function(index, value) if self.build.treeTab.specList[index] then self.build.modFlag = true self.build.treeTab:SetActiveSpec(index) @@ -276,10 +276,12 @@ function ItemsTabClass:ItemsTab(build) self.controls.weaponSwapLabel = new("LabelControl"):LabelControl({"RIGHT",self.controls.weaponSwap1,"LEFT"}, {-4, 0, 0, 14}, "^7Weapon Set:") -- All items list + local function itemListWidth() return main.portraitMode and 360 or 450 end + local function itemListHeight() return main.portraitMode and 244 or 324 end if main.portraitMode then - self.controls.itemList = new("ItemListControl"):ItemListControl({"TOPRIGHT",self.lastSlot,"BOTTOMRIGHT"}, {0, 0, 360, 308}, self, true) + self.controls.itemList = new("ItemListControl"):ItemListControl({"TOPRIGHT",self.lastSlot,"BOTTOMRIGHT"}, {0, 0, itemListWidth, itemListHeight}, self, true) else - self.controls.itemList = new("ItemListControl"):ItemListControl({"TOPLEFT",self.controls.setManage,"TOPRIGHT"}, {20, 20, 360, 308}, self, true) + self.controls.itemList = new("ItemListControl"):ItemListControl({"TOPLEFT",self.controls.setManage,"TOPRIGHT"}, {20, 70, itemListWidth, itemListHeight}, self, true) end -- Database selector @@ -290,24 +292,24 @@ function ItemsTabClass:ItemsTab(build) self.controls.selectDB = new("DropDownControl"):DropDownControl({"LEFT",self.controls.selectDBLabel,"RIGHT"}, {4, 0, 150, 18}, { "Uniques", "Rare Templates" }) -- Unique database - self.controls.uniqueDB = new("ItemDBControl"):ItemDBControl({"TOPLEFT",self.controls.itemList,"BOTTOMLEFT"}, {0, 76, 360, function(c) return m_min(244, self.maxY - select(2, c:GetPos())) end}, self, main.uniqueDB, "UNIQUE") + self.controls.uniqueDB = new("ItemDBControl"):ItemDBControl({"TOPLEFT",self.controls.itemList,"BOTTOMLEFT"}, {0, 76, itemListWidth, function(c) return m_min(196, self.maxY - select(2, c:GetPos())) end}, self, main.uniqueDB, "UNIQUE") self.controls.uniqueDB.y = function() - return self.controls.selectDBLabel:IsShown() and 118 or 96 + return self.controls.selectDBLabel:IsShown() and 122 or 100 end self.controls.uniqueDB.shown = function() return not self.controls.selectDBLabel:IsShown() or self.controls.selectDB.selIndex == 1 end -- Rare template database - self.controls.rareDB = new("ItemDBControl"):ItemDBControl({"TOPLEFT",self.controls.itemList,"BOTTOMLEFT"}, {0, 76, 360, function(c) return m_min(260, self.maxY - select(2, c:GetPos())) end}, self, main.rareDB, "RARE") + self.controls.rareDB = new("ItemDBControl"):ItemDBControl({"TOPLEFT",self.controls.itemList,"BOTTOMLEFT"}, {0, 76, itemListWidth, function(c) return m_min(196, self.maxY - select(2, c:GetPos())) end}, self, main.rareDB, "RARE") self.controls.rareDB.y = function() - return self.controls.selectDBLabel:IsShown() and 78 or 396 + return self.controls.selectDBLabel:IsShown() and 82 or 356 end self.controls.rareDB.shown = function() return not self.controls.selectDBLabel:IsShown() or self.controls.selectDB.selIndex == 2 end -- Create/import item - self.controls.craftDisplayItem = new("ButtonControl"):ButtonControl({"TOPLEFT",main.portraitMode and self.controls.setManage or self.controls.itemList,"TOPRIGHT"}, {20, main.portraitMode and 0 or -20, 120, 20}, "Craft item...", function() + self.controls.craftDisplayItem = new("ButtonControl"):ButtonControl({"TOPLEFT",main.portraitMode and self.controls.setManage or self.controls.itemList,"TOPRIGHT"}, {20, main.portraitMode and 0 or -70, 120, 20}, "Craft item...", function() self:CraftItem() end) self.controls.craftDisplayItem.shown = function() @@ -317,24 +319,21 @@ function ItemsTabClass:ItemsTab(build) self:EditDisplayItemText() end) self.controls.displayItemTip = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.craftDisplayItem,"BOTTOMLEFT"}, {0, 8, 100, 16}, -[[^7Double-click an item from one of the lists, -or copy and paste an item from in game -(hover over the item and Ctrl+C) to view or edit -the item and add it to your build. You can -also clone an item within Path of Building by -copying and pasting it with Ctrl+C and Ctrl+V. - -You can Control + Click an item to equip it, or -drag it onto the slot. This will also add it to -your build if it's from the unique/template list. -If there's 2 slots an item can go in, -holding Shift will put it in the second.]]) - self.controls.sharedItemList = new("SharedItemListControl"):SharedItemListControl({"TOPLEFT",self.controls.craftDisplayItem, "BOTTOMLEFT"}, {0, 232, 340, 308}, self, true) +[[^7Double-click an item from one of the lists, or copy and paste an +item from in game (hover over the item and Ctrl+C) to view or edit +the item and add it to your build. You can also clone an item within +Path of Building by copying and pasting it with Ctrl+C and Ctrl+V. + +You can Control + Click an item to equip it, or drag it onto the slot. +This will also add it to your build if it's from the unique/template +list. If there are 2 slots an item can go in, holding Shift will +put it in the second.]]) + self.controls.sharedItemList = new("SharedItemListControl"):SharedItemListControl({"TOPLEFT",self.controls.craftDisplayItem, "BOTTOMLEFT"}, {0, 232, 425, 308}, self, true) -- Display item self.displayItemTooltip = new("Tooltip"):Tooltip() self.displayItemTooltip.maxWidth = 458 - self.anchorDisplayItem = new("Control"):Control({"TOPLEFT",main.portraitMode and self.controls.setManage or self.controls.itemList,"TOPRIGHT"}, {20, main.portraitMode and 0 or -20, 0, 0}) + self.anchorDisplayItem = new("Control"):Control({"TOPLEFT",main.portraitMode and self.controls.setManage or self.controls.itemList,"TOPRIGHT"}, {20, main.portraitMode and 0 or -70, 0, 0}) self.anchorDisplayItem.shown = function() return self.displayItem ~= nil end @@ -1551,12 +1550,12 @@ function ItemsTabClass:Draw(viewPort, inputEvents) self:UpdateSockets() if main.portraitMode then - self.controls.itemList:SetAnchor("TOPRIGHT", self.lastSlot, "BOTTOMRIGHT", 0, 40) + self.controls.itemList:SetAnchor("TOPRIGHT", self.lastSlot, "BOTTOMRIGHT", 0, 84) else - self.controls.itemList:SetAnchor("TOPLEFT", self.controls.setManage, "TOPRIGHT", 20, 20) + self.controls.itemList:SetAnchor("TOPLEFT", self.controls.setManage, "TOPRIGHT", 20, 70) end - self.controls.craftDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0 or -20) - self.anchorDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0) + self.controls.craftDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0 or -70) + self.anchorDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0 or -70) self:DrawControls(viewPort) if self.controls.scrollBarH:IsShown() then @@ -1620,6 +1619,7 @@ end function ItemsTabClass:EquipItemInSet(item, itemSetId) local itemSet = self.itemSets[itemSetId] local slotName = item:GetPrimarySlot() + local itemAdded if self.slots[slotName].weaponSet == 1 and itemSet.useSecondWeaponSet then -- Redirect to second weapon set slotName = slotName .. " Swap" @@ -1627,6 +1627,7 @@ function ItemsTabClass:EquipItemInSet(item, itemSetId) if not item.id or not self.items[item.id] then item = new("Item"):Item(item.raw) self:AddItem(item, true) + itemAdded = true end local altSlot = slotName:gsub("1","2") if IsKeyDown("SHIFT") then @@ -1644,6 +1645,9 @@ function ItemsTabClass:EquipItemInSet(item, itemSetId) end end self:PopulateSlots() + if itemAdded then + self.controls.itemList:SelectItem(item.id) + end self:AddUndoState() self.build.buildFlag = true end @@ -1803,6 +1807,7 @@ end -- Adds the current display item to the build's item list function ItemsTabClass:AddDisplayItem(noAutoEquip) local item = self.displayItem + local itemAdded = item and not item.id local oldItem = item and item.id and self.items[item.id] -- Add it to the list and clear the current display item self:AddItem(item, noAutoEquip) @@ -1812,47 +1817,13 @@ function ItemsTabClass:AddDisplayItem(noAutoEquip) self:AddForbiddenJewelCounterpart(item) self:PopulateSlots() + if itemAdded then + self.controls.itemList:SelectItem(item.id) + end self:AddUndoState() self.build.buildFlag = true end --- Sorts the build's item list -function ItemsTabClass:SortItemList() - table.sort(self.itemOrderList, function(a, b) - local itemA = self.items[a] - local itemB = self.items[b] - local primSlotA = itemA:GetPrimarySlot() - local primSlotB = itemB:GetPrimarySlot() - if primSlotA ~= primSlotB then - if not self.slotOrder[primSlotA] then - return false - elseif not self.slotOrder[primSlotB] then - return true - end - return self.slotOrder[primSlotA] < self.slotOrder[primSlotB] - end - local equipSlotA, equipSetA = self:GetEquippedSlotForItem(itemA) - local equipSlotB, equipSetB = self:GetEquippedSlotForItem(itemB) - if equipSlotA and equipSlotB then - if equipSlotA ~= equipSlotB then - return self.slotOrder[equipSlotA.slotName] < self.slotOrder[equipSlotB.slotName] - elseif equipSetA and not equipSetB then - return false - elseif not equipSetA and equipSetB then - return true - elseif equipSetA and equipSetB then - return isValueInArray(self.itemSetOrderList, equipSetA.id) < isValueInArray(self.itemSetOrderList, equipSetB.id) - end - elseif equipSlotA then - return true - elseif equipSlotB then - return false - end - return itemA.name < itemB.name - end) - self:AddUndoState() -end - -- Deletes an item function ItemsTabClass:DeleteItem(item, deferUndoState) for slotName, slot in pairs(self.slots) do diff --git a/src/Classes/SharedItemListControl.lua b/src/Classes/SharedItemListControl.lua index a4891b368ff..6e41fac75cd 100644 --- a/src/Classes/SharedItemListControl.lua +++ b/src/Classes/SharedItemListControl.lua @@ -14,9 +14,9 @@ function SharedItemListClass:SharedItemListControl(anchor, rect, itemsTab, force self:ListControl(anchor, rect, 16, "VERTICAL", true, main.sharedItemList, forceTooltip) self.itemsTab = itemsTab self.label = "^7Shared items:" - self.defaultText = "^x7F7F7FThis is a list of items that will be shared between all of\nyour builds.\nYou can add items to this list by dragging them from\none of the other lists." + self.defaultText = "^x7F7F7FThis is a list of items that will be shared between all of your builds.\n\nYou can add items to this list by dragging them from another list." self.dragTargetList = { } - self.controls.delete = new("ButtonControl"):ButtonControl({"BOTTOMRIGHT",self,"TOPRIGHT"}, {0, -2, 60, 18}, "Delete", function() + self.controls.delete = new("ButtonControl"):ButtonControl({"BOTTOMRIGHT",self,"TOPRIGHT"}, {0, -2, 60, 20}, "Delete", function() self:OnSelDelete(self.selIndex, self.selValue) end) self.controls.delete.enabled = function() From 6eda4d07b6fed79dc122bcc04cc446f3b9ee817a Mon Sep 17 00:00:00 2001 From: AdamZ Date: Tue, 25 Aug 2026 16:58:13 -0700 Subject: [PATCH 2/3] Add item slot icons to Items tab lists Forward-port the Items tab icon work from PathOfBuildingCommunity/PathOfBuilding#7265 by Simone. Reuse a shared slot icon module across the Skills, Item Database, and All Items lists, with support for loadout group headers. Co-authored-by: Simone --- spec/System/TestItemListControl_spec.lua | 7 ++++ src/Assets/icon_flask.png | Bin 0 -> 174 bytes src/Assets/icon_jewel.png | Bin 0 -> 196 bytes src/Classes/ItemDBControl.lua | 5 +++ src/Classes/ItemListControl.lua | 7 ++++ src/Classes/SkillListControl.lua | 25 ++------------ src/Modules/ItemSlotIcons.lua | 40 +++++++++++++++++++++++ 7 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 src/Assets/icon_flask.png create mode 100644 src/Assets/icon_jewel.png create mode 100644 src/Modules/ItemSlotIcons.lua diff --git a/spec/System/TestItemListControl_spec.lua b/spec/System/TestItemListControl_spec.lua index c41ce97a62c..19c14fd9df9 100644 --- a/spec/System/TestItemListControl_spec.lua +++ b/spec/System/TestItemListControl_spec.lua @@ -311,6 +311,13 @@ describe("ItemListControl", function() assert.is_nil(control.selValue) end) + it("shows slot icons for items but not loadout group headers", function() + local control = newItemListControl() + + assert.is_not_nil(control:GetRowIcon(1, 1, 1)) + assert.is_nil(control:GetRowIcon(1, 1, { groupHeader = "Boss" })) + end) + it("clears filters that hide a selected item", function() local control = newItemListControl() control.controls.slotFilter.selIndex = 4 diff --git a/src/Assets/icon_flask.png b/src/Assets/icon_flask.png new file mode 100644 index 0000000000000000000000000000000000000000..18668370ca7c031efa71256eaccae2c8e529198f GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|GCW-zLo9le z6BejO7}lTqFZy};pX1Bh1ZO{JzMZ}?D=5J9*s6IN%mtFCTzV9DmO5T>Lo9le z6C^SYba+2H_;&OEi9fkk9gDbXB)Gcr=pW7h8&@7(B`IlaU|?{-MA(ES_H*;?^#6;q zk7aV0o!t@Bki&7!z=2Ksng37ui8Bs&_(b1`cvQw{c+QnSMZ__QiDxzA4U=SJmgQ+N sZ4;JA&R5)F^4RCpi-o<~n*26E>pK^2|IWC^0q7D2Pgg&ebxsLQ0K Date: Tue, 25 Aug 2026 17:20:59 -0700 Subject: [PATCH 3/3] Clarify custom order sort label --- spec/System/TestItemListControl_spec.lua | 2 +- src/Classes/ItemListControl.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/System/TestItemListControl_spec.lua b/spec/System/TestItemListControl_spec.lua index 19c14fd9df9..5f2315ee151 100644 --- a/spec/System/TestItemListControl_spec.lua +++ b/spec/System/TestItemListControl_spec.lua @@ -154,7 +154,7 @@ describe("ItemListControl", function() control:UpdateLoadoutList() control:UpdateList() - assert.equal("Custom Order", control.controls.sortMode:GetSelValue()) + assert.equal("Sort by Custom Order", control.controls.sortMode:GetSelValue()) assert.is_true(control.isMutable) assert.is_true(rawequal(control.list, control.itemsTab.itemOrderList)) assert.are.same({ 1, 2, 3, 4 }, control.itemsTab.itemOrderList) diff --git a/src/Classes/ItemListControl.lua b/src/Classes/ItemListControl.lua index a9becfe5173..dfd25088ad2 100644 --- a/src/Classes/ItemListControl.lua +++ b/src/Classes/ItemListControl.lua @@ -33,7 +33,7 @@ function ItemListClass:ItemListControl(anchor, rect, itemsTab, forceTooltip) self.controls.slotFilter = new("DropDownControl"):DropDownControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -22, rowControlWidth, 18}, slotFilterList, function() self:UpdateList() end) - self.controls.sortMode = new("DropDownControl"):DropDownControl({"LEFT",self.controls.slotFilter,"RIGHT"}, {4, 0, rowControlWidth, 18}, { "Custom Order", "Sort by Item Slot", "Sort by Name", "Sort by Rarity", "Sort by Loadout" }, function() + self.controls.sortMode = new("DropDownControl"):DropDownControl({"LEFT",self.controls.slotFilter,"RIGHT"}, {4, 0, rowControlWidth, 18}, { "Sort by Custom Order", "Sort by Item Slot", "Sort by Name", "Sort by Rarity", "Sort by Loadout" }, function() self:UpdateList() end) self.controls.loadoutFilter = new("DropDownControl"):DropDownControl({"LEFT",self.controls.sortMode,"RIGHT"}, {4, 0, rowControlWidth, 18}, nil, function() @@ -287,8 +287,8 @@ function ItemListClass:UpdateList() end end end - local sortMode = self.controls.sortMode.list[self.controls.sortMode.selIndex or 1] or "Custom Order" - if sortMode == "Custom Order" then + local sortMode = self.controls.sortMode.list[self.controls.sortMode.selIndex or 1] or "Sort by Custom Order" + if sortMode == "Sort by Custom Order" then local unfiltered = loadoutFilterIndex == 1 and slotFilter == "Any Slot" and searchText == "" self.list = unfiltered and self.itemsTab.itemOrderList or itemList self.isMutable = unfiltered