diff --git a/spec/System/TestCustomModControl_spec.lua b/spec/System/TestCustomModControl_spec.lua new file mode 100644 index 00000000000..0ec3f33c4e4 --- /dev/null +++ b/spec/System/TestCustomModControl_spec.lua @@ -0,0 +1,143 @@ +describe("Custom modifier controls", function() + local initialPopupCount + + before_each(function() + newBuild() + main:SelectControl() + initialPopupCount = #main.popups + end) + + after_each(function() + main:SelectControl() + while #main.popups > initialPopupCount do + main:ClosePopup() + end + end) + + local function openModBrowser() + local configTab = build.configTab + local blockData = configTab.configSets[configTab.activeConfigSetId].customModsList[1] + configTab:OpenAddModPopup(blockData) + return main.popups[1], blockData + end + + it("does not retain the modifier list focus after adding a mod", function() + local popup, blockData = openModBrowser() + local listControl = popup.controls.listControl + local selectedMod = listControl.list[1] + + listControl:OnSelClick(1, selectedMod, false) + popup.controls.save.onClick() + + assert.is_nil(main.selControl) + assert.are_not.equal(popup, main.popups[1]) + assert.are.equal(selectedMod, blockData.text) + end) + + it("collapses numeric tiers and imports the first range value", function() + local popup, blockData = openModBrowser() + local minionDamageEntries = { } + local minionDamageIndex + for index, modText in ipairs(popup.controls.listControl.list) do + if modText:match("^Minions deal .-%% increased Damage$") then + table.insert(minionDamageEntries, modText) + minionDamageIndex = index + end + end + + assert.are.same({ "Minions deal 10% increased Damage" }, minionDamageEntries) + popup.controls.listControl.selIndex = minionDamageIndex + popup.controls.save.onClick() + assert.are.equal("Minions deal 10% increased Damage", blockData.text) + end) + + it("orders modifiers alphabetically while ignoring numeric values", function() + local popup = openModBrowser() + local previousSortKey + for _, modText in ipairs(popup.controls.listControl.list) do + local sortKey = modText + :gsub("([%+-]?)%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)", "%1#") + :gsub("%d+%.?%d*", "#") + :lower() + :gsub("#", " ") + :gsub("[^%a]+", " ") + :match("^%s*(.-)%s*$") + assert.is_true(not previousSortKey or previousSortKey <= sortKey, + tostring(previousSortKey) .. " should be ordered before " .. sortKey) + previousSortKey = sortKey + end + end) + + it("clears the modifier search with its clear button", function() + local popup = openModBrowser() + local controls = popup.controls + local unfilteredCount = #controls.listControl.list + assert.is_false(controls.search.controls.buttonClear:IsShown()) + controls.search:SetText("minion damage", true) + + assert.is_true(#controls.listControl.list < unfilteredCount) + assert.are.equal("minion damage", controls.search.buf) + assert.is_true(controls.search.controls.buttonClear:IsShown()) + + controls.search.controls.buttonClear.onClick() + + assert.are.equal("", controls.search.buf) + assert.are.equal(unfilteredCount, #controls.listControl.list) + assert.is_false(controls.search.controls.buttonClear:IsShown()) + end) + + it("uses the expanded browser dimensions", function() + local popup = openModBrowser() + local listWidth, listHeight = popup.controls.listControl:GetSize() + local searchWidth = popup.controls.search:GetSize() + + assert.are.equal(720, popup.width) + assert.are.equal(540, popup.height) + assert.are.equal(700, listWidth) + assert.are.equal(454, listHeight) + assert.are.equal(640, searchWidth) + end) + + it("opens with the search field ready for typing", function() + local popup = openModBrowser() + local inputEvents = { { type = "Char", key = "m" } } + + assert.are.equal(popup.controls.search, popup.selControl) + assert.is_true(popup.controls.search.hasFocus) + assert.is_nil(popup.controls.listControl.hasFocus) + + popup:ProcessInput(inputEvents, { x = 0, y = 0, width = 1920, height = 1080 }) + + assert.are.equal("m", popup.controls.search.buf) + end) + + it("uses the mod group title as the calculation source name", function() + local configTab = build.configTab + local blockData = configTab.configSets[configTab.activeConfigSetId].customModsList[1] + blockData.text = "+100 to maximum Life" + configTab:BuildModList() + + configTab.customModsBlockControls[1].controls.titleEdit:SetText("Bossing", true) + + local customMods = configTab.modList:Tabulate("BASE", nil, "Life") + assert.are.equal(1, #customMods) + assert.are.equal("Custom:Bossing", customMods[1].mod.source) + + build.buildFlag = true + runCallback("OnFrame") + local breakdownControl = build.calcsTab.controls.breakdown + breakdownControl.sectionList = { } + breakdownControl:AddModSection({ modName = "Life", modType = "BASE" }) + local customRow + for _, row in ipairs(breakdownControl.sectionList[1].rowList) do + if row.mod.source == "Custom:Bossing" then + customRow = row + break + end + end + + assert.is_not_nil(customRow) + assert.are.equal("Custom", customRow.source) + assert.are.equal("Bossing", customRow.sourceName) + end) +end) diff --git a/spec/System/TestItemDBControl_spec.lua b/spec/System/TestItemDBControl_spec.lua index 85dc8efa4b8..3a77a067e92 100644 --- a/spec/System/TestItemDBControl_spec.lua +++ b/spec/System/TestItemDBControl_spec.lua @@ -1,4 +1,14 @@ describe("ItemDBControl", function() + local originalGetCursorPos + + before_each(function() + originalGetCursorPos = GetCursorPos + end) + + after_each(function() + GetCursorPos = originalGetCursorPos + end) + it("sorts lower-is-better stats below zero", function() local function makeItem(name) return { @@ -75,4 +85,33 @@ describe("ItemDBControl", function() assert.is_true(control:DoesItemMatchFilters(item)) end) + + it("releases focus after opening an item with a double click", function() + local item = { + raw = "Rarity: Unique\nTest Item\nLeather Belt", + } + local itemsTab + itemsTab = { + CreateDisplayItemFromRaw = function(_, raw, isUnique) + itemsTab.displayRaw = raw + itemsTab.displayIsUnique = isUnique + end, + } + local control = new("ItemDBControl", nil, { 0, 0, 100, 100 }, itemsTab, { + list = { item }, + }, "UNIQUE") + control.list = { item } + GetCursorPos = function() + return 3, 3 + end + control.GetRowRegion = function() + return { x = 0, y = 0, width = 100, height = 100 } + end + + local selectedControl = control:OnKeyDown("LEFTBUTTON", true) + + assert.is_nil(selectedControl) + assert.are.equal(item.raw, itemsTab.displayRaw) + assert.is_true(itemsTab.displayIsUnique) + end) end) diff --git a/spec/System/TestItemListControl_spec.lua b/spec/System/TestItemListControl_spec.lua index 17982f5a310..9b27e5ec0e0 100644 --- a/spec/System/TestItemListControl_spec.lua +++ b/spec/System/TestItemListControl_spec.lua @@ -1,5 +1,6 @@ describe("ItemListControl", function() local originalOpenConfirmPopup + local originalGetCursorPos local function newItemListControl() local activeItemSet = { @@ -57,10 +58,12 @@ describe("ItemListControl", function() before_each(function() originalOpenConfirmPopup = main.OpenConfirmPopup + originalGetCursorPos = GetCursorPos end) after_each(function() main.OpenConfirmPopup = originalOpenConfirmPopup + GetCursorPos = originalGetCursorPos end) it("only shows items from the active item set and passive tree", function() @@ -172,4 +175,29 @@ describe("ItemListControl", function() assert.are.same({ }, itemsTab.itemOrderList) assert.are.same({ }, itemsTab.items) end) + + it("releases focus after opening an item with a double click", function() + local control, itemsTab = newItemListControl() + local item = new("Item", [[ +Rarity: Rare +Test Belt +Leather Belt +]]) + item.id = 1 + itemsTab.items[1] = item + itemsTab.SetDisplayItem = function(_, displayItem) + itemsTab.displayItem = displayItem + end + GetCursorPos = function() + return 3, 3 + end + control.GetRowRegion = function() + return { x = 0, y = 0, width = 360, height = 308 } + end + + local selectedControl = control:OnKeyDown("LEFTBUTTON", true) + + assert.is_nil(selectedControl) + assert.are.equal(1, itemsTab.displayItem.id) + end) end) diff --git a/src/Classes/CalcBreakdownControl.lua b/src/Classes/CalcBreakdownControl.lua index 1255ae9856c..6983b4b9418 100644 --- a/src/Classes/CalcBreakdownControl.lua +++ b/src/Classes/CalcBreakdownControl.lua @@ -421,6 +421,8 @@ function CalcBreakdownClass:AddModSection(sectionData, modList) row.sourceName = row.mod.source:match("Pantheon:(.+)") elseif sourceType == "Spectre" then row.sourceName = row.mod.source:match("Spectre:(.+)") + elseif sourceType == "Custom" then + row.sourceName = row.mod.source:match("Custom:(.+)") end if row.mod.flags ~= 0 or row.mod.keywordFlags ~= 0 then diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 47fe38cb865..3085f50fe7d 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -32,12 +32,14 @@ local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "Contro configTab.build.buildFlag = true end) - self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.deleteBtn, "RIGHT"}, {6, 0, 232, 18}, blockData.title or "", nil, nil, nil, function(buf) + self.controls.titleEdit = new("EditControl", {"LEFT", self.controls.deleteBtn, "RIGHT"}, {6, 0, 222, 18}, blockData.title or "", nil, nil, nil, function(buf) blockData.title = buf configTab:AddUndoState() + configTab:BuildModList() + configTab.build.buildFlag = true end) - self.controls.addModBtn = new("ButtonControl", {"LEFT", self.controls.titleEdit, "RIGHT"}, {6, 0, 48, 18}, "^7+ Mod", function() + self.controls.addModBtn = new("ButtonControl", {"LEFT", self.controls.titleEdit, "RIGHT"}, {6, 0, 58, 18}, "^7Add Mod", function() configTab:OpenAddModPopup(blockData) end) @@ -59,7 +61,7 @@ local CustomModBlockClass = newClass("CustomModBlockControl", "Control", "Contro local output = calcFunc() blockData.enabled = curState configTab:BuildModList() - configTab.build:AddStatComparesToTooltip(tooltip, calcBase, output, curState and "^7Disabling this block will give you:" or "^7Enabling this block will give you:") + configTab.build:AddStatComparesToTooltip(tooltip, calcBase, output, curState and "^7Disabling this group will give you:" or "^7Enabling this group will give you:") end end end @@ -770,9 +772,9 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont end self.controls.scrollBar = new("ScrollBarControl", {"TOPRIGHT",self,"TOPRIGHT"}, {0, 0, 18, 0}, 50, "VERTICAL", true) if self.customSection then - self.controls.customModsAddBlock = new("ButtonControl", {"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, 75, 20}, "^7+ Block", function() + self.controls.customModsAddBlock = new("ButtonControl", {"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, 120, 20}, "^7Add Mod Group", function() local customModsList = self.configSets[self.activeConfigSetId].customModsList - t_insert(customModsList, { title = "Block " .. (#customModsList + 1), enabled = true, text = "" }) + t_insert(customModsList, { title = "Group " .. (#customModsList + 1), enabled = true, text = "" }) self:UpdateCustomModsControls() self:AddUndoState() self:BuildModList() @@ -1101,7 +1103,7 @@ function ConfigTabClass:BuildModList() end end - -- Apply Custom Modifier Blocks + -- Apply Custom Modifier groups local customModsList = self.configSets[self.activeConfigSetId].customModsList local hasBlockText = false if customModsList then @@ -1112,7 +1114,7 @@ function ConfigTabClass:BuildModList() local strippedLine = StripEscapes(line):match("^%s*(.-)%s*$") local mods, extra = modLib.parseMod(strippedLine) if mods and not extra then - local source = "Custom" + local source = "Custom:" .. (block.title or "Default") for i = 1, #mods do local mod = mods[i] if mod then @@ -1352,7 +1354,60 @@ function ConfigTabClass:OpenAddModPopup(blockData) end end - table.sort(allModsList) + local modTemplateCache = { } + local function getModTemplate(modText) + if not modTemplateCache[modText] then + modTemplateCache[modText] = modText + :gsub("([%+-]?)%((%-?%d+%.?%d*)%-(%-?%d+%.?%d*)%)", "%1#") + :gsub("%d+%.?%d*", "#") + :lower() + end + return modTemplateCache[modText] + end + local alphabeticalSortKeyCache = { } + local function getAlphabeticalSortKey(modText) + if not alphabeticalSortKeyCache[modText] then + alphabeticalSortKeyCache[modText] = getModTemplate(modText) + :gsub("#", " ") + :gsub("[^%a]+", " ") + :match("^%s*(.-)%s*$") + end + return alphabeticalSortKeyCache[modText] + end + local function sortAlphabeticallyIgnoringValues(a, b) + local aSortKey = getAlphabeticalSortKey(a) + local bSortKey = getAlphabeticalSortKey(b) + if aSortKey ~= bSortKey then + return aSortKey < bSortKey + end + local aTemplate = getModTemplate(a) + local bTemplate = getModTemplate(b) + if aTemplate ~= bTemplate then + return aTemplate < bTemplate + end + local aLower = a:lower() + local bLower = b:lower() + if aLower ~= bLower then + return aLower < bLower + end + return a < b + end + + table.sort(allModsList, sortAlphabeticallyIgnoringValues) + + -- Collapse affix tiers that only differ by their numeric values. The list is + -- sorted first so the retained representative is deterministic. + wipeTable(seen) + local deduplicatedModsList = { } + for _, modText in ipairs(allModsList) do + local modTemplate = getModTemplate(modText) + if not seen[modTemplate] then + seen[modTemplate] = true + t_insert(deduplicatedModsList, itemLib.applyRange(modText, 0)) + end + end + table.sort(deduplicatedModsList, sortAlphabeticallyIgnoringValues) + allModsList = deduplicatedModsList local displayList = { } local controls = { } @@ -1416,7 +1471,7 @@ function ConfigTabClass:OpenAddModPopup(blockData) if a.rank ~= b.rank then return a.rank < b.rank end - return a.text < b.text + return sortAlphabeticallyIgnoringValues(a.text, b.text) end) for _, match in ipairs(matches) do t_insert(displayList, match.text) @@ -1433,17 +1488,15 @@ function ConfigTabClass:OpenAddModPopup(blockData) end end - controls.listControl = new("ListControl", {"TOPLEFT", nil, "TOPLEFT"}, {10, 20, 580, 184}, 16, "VERTICAL", false, displayList) + controls.listControl = new("ListControl", {"TOPLEFT", nil, "TOPLEFT"}, {10, 20, 700, 454}, 16, "VERTICAL", false, displayList) controls.listControl.font = "VAR" - controls.listControl.hasFocus = true controls.listControl.GetRowValue = function(self, column, index, value) return value or "" end controls.listControl.AddValueTooltip = function(self, tooltip, index, value) tooltip:Clear(true) if value and #value > 0 and value ~= "No matching modifiers found" then - local cleanText = itemLib.applyRange(value, 0.5) - local mods, extra = modLib.parseMod(cleanText) + local mods, extra = modLib.parseMod(value) if mods and not extra then tooltip:AddLine(14, "^7Supported: ^2Yes") else @@ -1452,31 +1505,30 @@ function ConfigTabClass:OpenAddModPopup(blockData) end end controls.listControl.OnSelClick = function(self, index, value, doubleClick) - if main.SelectControl then - main:SelectControl(self) - end self:SelectIndex(index) if doubleClick and controls.save:IsEnabled() then controls.save.onClick() end end - controls.searchLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {65, 212, 0, 16}, "^7Search:") - controls.search = new("EditControl", {"TOPLEFT", nil, "TOPLEFT"}, {70, 212, 520, 18}, "", nil, "%c", 100, function() + controls.searchLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {65, 482, 0, 16}, "^7Search:") + controls.search = new("EditControl", {"TOPLEFT", nil, "TOPLEFT"}, {70, 482, 640, 18}, "", nil, "%c", 100, function() updateDisplayList() - end) + end, nil, nil, true) + controls.search.controls.buttonClear.shown = function() + return #controls.search.buf > 0 + end updateDisplayList() - controls.save = new("ButtonControl", nil, {-45, 242, 80, 20}, "Add", function() + controls.save = new("ButtonControl", nil, {-45, 512, 80, 20}, "Add", function() local selIndex = controls.listControl.selIndex or 1 local selected = displayList[selIndex] if selected and selected ~= "No matching modifiers found" then - local textToAdd = itemLib.applyRange(selected, 0.5) if blockData.text and #blockData.text > 0 and not blockData.text:match("\n$") then blockData.text = blockData.text .. "\n" end - blockData.text = (blockData.text or "") .. textToAdd + blockData.text = (blockData.text or "") .. selected self:UpdateCustomModsControls() self:AddUndoState() self:BuildModList() @@ -1490,9 +1542,9 @@ function ConfigTabClass:OpenAddModPopup(blockData) return selected ~= nil and selected ~= "No matching modifiers found" end - controls.close = new("ButtonControl", nil, {45, 242, 80, 20}, "Cancel", function() + controls.close = new("ButtonControl", nil, {45, 512, 80, 20}, "Cancel", function() main:ClosePopup() end) - main:OpenPopup(600, 270, "Mod Browser", controls, "save", nil, "close") + main:OpenPopup(720, 540, "Mod Browser", controls, "save", "search", "close") end diff --git a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua index f8a56af8282..0995654528e 100644 --- a/src/Classes/ItemDBControl.lua +++ b/src/Classes/ItemDBControl.lua @@ -360,6 +360,7 @@ function ItemDBClass:OnSelClick(index, item, doubleClick) self.itemsTab.build.buildFlag = true elseif doubleClick then self.itemsTab:CreateDisplayItemFromRaw(item.raw, true) + return false end end diff --git a/src/Classes/ItemListControl.lua b/src/Classes/ItemListControl.lua index 11ae71487b7..acb48e8e13d 100644 --- a/src/Classes/ItemListControl.lua +++ b/src/Classes/ItemListControl.lua @@ -316,6 +316,7 @@ function ItemListClass:OnSelClick(index, itemId, doubleClick) local newItem = new("Item", item:BuildRaw()) newItem.id = item.id self.itemsTab:SetDisplayItem(newItem) + return false end end diff --git a/src/Classes/ListControl.lua b/src/Classes/ListControl.lua index 34dd0e7f413..c37608e20f4 100644 --- a/src/Classes/ListControl.lua +++ b/src/Classes/ListControl.lua @@ -16,7 +16,7 @@ -- :OnDragSend(index, value, target) [Called after a drag event] -- :OnOrderChange() [Called after list order is changed through dragging] -- :OnSelect(index, value) [Called when a list value is selected] --- :OnSelClick(index, value, doubleClick) [Called when a list value is clicked] +-- :OnSelClick(index, value, doubleClick) [Called when a list value is clicked; return false to release focus] -- :OnSelCopy(index, value) [Called when Ctrl+C is pressed while a list value is selected] -- :OnSelDelete(index, value) [Called when backspace or delete is pressed while a list value is selected] -- :OnSelKeyDown(index, value) [Called when any other key is pressed while a list value is selected] @@ -366,7 +366,9 @@ function ListClass:OnKeyDown(key, doubleClick) self.selDragActive = false end if self.OnSelClick then - self:OnSelClick(self.selIndex, self.selValue, doubleClick) + if self:OnSelClick(self.selIndex, self.selValue, doubleClick) == false then + return + end end end elseif #self.list > 0 and not self.selDragActive then