From b26d2ddb3a3a2b952fb8b47bfc7cf2be710c1343 Mon Sep 17 00:00:00 2001 From: AdamZ Date: Mon, 21 Sep 2026 14:12:11 -0700 Subject: [PATCH 1/2] Improve UI layout, item management, and socket editing --- spec/System/TestGemSocketQuality_spec.lua | 14 + spec/System/TestImport_spec.lua | 84 ++++ spec/System/TestItemListControl_spec.lua | 46 ++ spec/System/TestItemMods_spec.lua | 127 +++++ spec/System/TestSkills_spec.lua | 117 +++++ src/Classes/CalcSectionControl.lua | 29 +- src/Classes/CalcsTab.lua | 41 +- src/Classes/CheckBoxControl.lua | 64 ++- src/Classes/ConfigTab.lua | 92 +++- src/Classes/DropDownControl.lua | 62 ++- src/Classes/EditControl.lua | 12 +- src/Classes/GemSelectControl.lua | 2 +- src/Classes/ImportTab.lua | 348 ++++++++++---- src/Classes/ItemDBControl.lua | 119 +++-- src/Classes/ItemListControl.lua | 373 +++++++++++---- src/Classes/ItemSlotControl.lua | 8 +- src/Classes/ItemsTab.lua | 552 +++++++++++----------- src/Classes/ListControl.lua | 15 +- src/Classes/SectionControl.lua | 6 +- src/Classes/SharedItemListControl.lua | 4 +- src/Classes/SkillListControl.lua | 15 +- src/Classes/SkillsTab.lua | 288 ++++++++--- src/Classes/Tooltip.lua | 101 +++- src/Classes/TreeTab.lua | 2 +- src/Modules/AddImplicitPopup.lua | 65 +-- src/Modules/Build.lua | 269 ++++++++--- src/Modules/CalcSections.lua | 10 +- src/Modules/CalcSetup.lua | 14 +- src/Modules/ConfigModBrowser.lua | 12 +- src/Modules/Data.lua | 10 +- src/Modules/ItemSocketControls.lua | 162 +++++++ src/Modules/Main.lua | 10 +- 32 files changed, 2265 insertions(+), 808 deletions(-) create mode 100644 src/Modules/ItemSocketControls.lua diff --git a/spec/System/TestGemSocketQuality_spec.lua b/spec/System/TestGemSocketQuality_spec.lua index 719004b1767..3f54c876494 100644 --- a/spec/System/TestGemSocketQuality_spec.lua +++ b/spec/System/TestGemSocketQuality_spec.lua @@ -23,6 +23,20 @@ describe("TestGemSocketQuality", function() end end + it("requires a physical socket for Dialla's 'always matches' mod", function() + equipBody("R", "Gems Socketed always have the Quality bonus from Socket Colour\n") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\nFireball 20/0 1\n") + runCallback("OnFrame") + + local group = groupForSlot("Body Armour") + assert.is_true(group.gemList[1].matchesSocket) + assert.is_false(group.gemList[2].matchesSocket) + group.mainActiveSkill = 2 + build.buildFlag = true + runCallback("OnFrame") + assert.are.equals(0, build.calcsTab.mainOutput.GemQuality) + end) + it("grants +10% quality to a gem in a matching colour socket", function() equipBody("B-B-B") build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") diff --git a/spec/System/TestImport_spec.lua b/spec/System/TestImport_spec.lua index 185e4a482ef..fdb3083fd39 100644 --- a/spec/System/TestImport_spec.lua +++ b/spec/System/TestImport_spec.lua @@ -12,6 +12,90 @@ describe("TestImport", function() newBuild() end) + describe("account-name overwrite import", function() + local downloadPage, requests, imports, tab + local realm = { hostName = "https://example.invalid/", realmCode = "pc" } + + before_each(function() + tab = build.importTab + tab.controls.siteAccountName:SetText("First#0001") + tab.controls.siteCharSelect.list = { { char = { name = "FirstCharacter", league = "Standard" } } } + tab.controls.siteCharSelect.selIndex = 1 + tab.lastLeague = "Standard" + requests, imports = { }, { } + downloadPage = launch.DownloadPage + launch.DownloadPage = function(_, url, callback) + table.insert(requests, { url = url, callback = callback }) + end + tab.ImportItemsAndSkills = function(_, character) + table.insert(imports, { "items", character.name }) + end + tab.ImportPassiveTreeAndJewels = function(_, character) + table.insert(imports, { "tree", character.name }) + end + end) + + after_each(function() + launch.DownloadPage = downloadPage + end) + + it("keeps both requests tied to the original account and character", function() + tab:DownloadItems(realm, true) + tab.controls.siteAccountName:SetText("Second#0002") + tab.controls.siteCharSelect.list = { { char = { name = "SecondCharacter", league = "Standard" } } } + requests[1].callback({ body = '{"items":[]}' }) + assert.matches("accountName=First%%230001&character=FirstCharacter&realm=pc", requests[2].url) + assert.same({ }, imports) + requests[2].callback({ body = '{"items":[]}' }) + assert.same({ { "items", "FirstCharacter" }, { "tree", "FirstCharacter" } }, imports) + end) + + for _, phase in ipairs({ "items", "tree" }) do + it("ignores a cancelled " .. phase .. " response after reopening import", function() + tab:DownloadItems(realm, true) + if phase == "tree" then + requests[1].callback({ body = '{"items":[]}' }) + end + local pending = requests[#requests] + tab.controls.siteCharClose.onClick() + tab.charImportMode = "SELECTCHAR" + tab.charImportStatus = "New character selection" + pending.callback({ body = '{"items":[]}' }) + assert.same({ }, imports) + assert.equal("New character selection", tab.charImportStatus) + assert.equal(pending, requests[#requests]) + end) + end + + it("ignores an older request without interrupting a newer import", function() + tab:DownloadItems(realm, true) + tab:DownloadItems(realm, true) + requests[1].callback({ body = '{"items":[]}' }) + assert.equal(2, #requests) + assert.equal("IMPORTING", tab.charImportMode) + requests[2].callback({ body = '{"items":[]}' }) + requests[3].callback({ body = '{"items":[]}' }) + assert.same({ { "items", "FirstCharacter" }, { "tree", "FirstCharacter" } }, imports) + end) + + it("leaves the build untouched if the second download fails", function() + tab:DownloadItems(realm, true) + requests[1].callback({ body = '{"items":[]}' }) + requests[2].callback(nil, "Download failed") + assert.same({ }, imports) + assert.equal("SELECTCHAR", tab.charImportMode) + assert.matches("Download failed", tab.charImportStatus) + end) + + it("ignores responses after switching to another build", function() + tab:DownloadItems(realm, true) + newBuild() + requests[1].callback({ body = '{"items":[]}' }) + assert.same({ }, imports) + assert.equal(1, #requests) + end) + end) + it("imports with correct tree", function() build.importTab:ImportPassiveTreeAndJewels(sampleData, true) runCallback("OnFrame") diff --git a/spec/System/TestItemListControl_spec.lua b/spec/System/TestItemListControl_spec.lua index 2d690a6eb86..0362a5f8645 100644 --- a/spec/System/TestItemListControl_spec.lua +++ b/spec/System/TestItemListControl_spec.lua @@ -52,6 +52,10 @@ describe("ItemListControl", function() PopulateSlots = function() end, AddUndoState = function() end, } + for id, item in pairs(itemsTab.items) do + item.name = item.type .. " " .. id + item.GetPrimarySlot = function(self) return self.type end + end local control = new("ItemListControl"):ItemListControl(nil, { 0, 0, 360, 308 }, itemsTab, true) return control, itemsTab, treeTab end @@ -66,6 +70,48 @@ describe("ItemListControl", function() GetCursorPos = originalGetCursorPos end) + it("sorts and filters without overwriting custom order or using sorted drop positions", function() + local control, itemsTab = newItemListControl() + itemsTab.items[1].name = "Z Armour" + control.controls.sortMode:SelByValue("Sort by Name") + control:UpdateList() + assert.are.same({ 2, 3, 4, 1 }, control.list) + assert.is_false(control.isMutable) + control.controls.search.buf = "armour" + control:UpdateList() + assert.are.same({ 2, 1 }, control.list) + assert.are.same({ 1, 2, 3, 4 }, itemsTab.itemOrderList) + + itemsTab.AddItem = function(self, item, _, index) + assert.is_nil(index) + item.id = 5 + self.items[item.id] = item + table.insert(self.itemOrderList, item.id) + end + itemsTab.AddForbiddenJewelCounterpart = function() end + control.selDragIndex = 2 + control:ReceiveDrag("Item", { raw = "Rarity: Normal\nPlate Vest" }) + control.controls.search.buf = "" + control.controls.sortMode:SelByValue("Custom Order") + control:UpdateList() + assert.are.same({ 1, 2, 3, 4, 5 }, control.list) + assert.equal(itemsTab.itemOrderList, control.list) + end) + + it("skips non-item group headers during selection and keyboard navigation", function() + local control = newItemListControl() + control.controls.sortMode:SelByValue("Sort by Loadout") + control:UpdateList() + assert.is_false(control:SelectIndex(1)) + assert.is_nil(control.selValue) + control:OnKeyDown("HOME") + assert.equal(1, control.selValue) + control:OnKeyDown("DOWN") + assert.equal(3, control.selValue) + control:OnKeyDown("DOWN") + assert.equal(2, control.selValue) + end) + it("only shows items from the active item set and passive tree", function() local control = newItemListControl() control:UpdateLoadoutList() diff --git a/spec/System/TestItemMods_spec.lua b/spec/System/TestItemMods_spec.lua index 3aa079e67b9..362ce3a1c80 100644 --- a/spec/System/TestItemMods_spec.lua +++ b/spec/System/TestItemMods_spec.lua @@ -7,6 +7,133 @@ describe("TetsItemMods", function() -- newBuild() takes care of resetting everything in setup() end) + it("keeps catalyst edits separate from ordinary quality through serialization", function() + build.itemsTab:CreateDisplayItemFromRaw([[Rarity: Rare + Test Ring + Amethyst Ring + Crafted: true + Prefix: None + Prefix: None + Prefix: None + Suffix: None + Suffix: None + Suffix: None + Quality: 12 + Implicits: 0]]) + local item = build.itemsTab.displayItem + local controls = build.itemsTab.controls + controls.displayItemCatalyst:SetSel(2) + controls.displayItemQualityEdit:SetText("17", true) + controls.displayItemCatalyst:SetSel(3) + local restored = new("Item"):Item(item:BuildRaw()) + assert.are.equal(2, restored.catalyst) + assert.are.equal(17, restored.catalystQuality) + assert.are.equal(12, restored.quality) + controls.displayItemCatalyst:SetSel(1) + restored = new("Item"):Item(item:BuildRaw()) + assert.is_nil(restored.catalystQuality) + assert.are.equal(12, restored.quality) + controls.displayItemQualityEdit:SetText("17", true) + controls.displayItemCatalyst:SetSel(2) + assert.are.equal(17, item.catalystQuality) + end) + + it("reapplies bulk socket edits after individual overrides and serializes both", function() + build.itemsTab:CreateDisplayItemFromRaw("Rarity: Normal\nPlate Vest\nSockets: R-G B") + local controls = build.itemsTab.controls + local item = build.itemsTab.displayItem + controls.displayItemSetColors:SetSel(2) + controls.displayItemSocket2:SetSel(2) + local restored = new("Item"):Item(item:BuildRaw()) + assert.are.equal("W", restored.sockets[1].color) + assert.are.equal("G", restored.sockets[2].color) + assert.are.equal(restored.sockets[1].group, restored.sockets[2].group) + assert.are_not.equal(restored.sockets[2].group, restored.sockets[3].group) + controls.displayItemSetColors:SetSel(2) + controls.displayItemSetLinks:SetSel(4) + controls.displayItemLink2.changeFunc(false) + assert.are_not.equal(item.sockets[2].group, item.sockets[3].group) + controls.displayItemSetLinks:SetSel(4) + restored = new("Item"):Item(item:BuildRaw()) + assert.are.equal(3, #restored.sockets) + for _, socket in ipairs(restored.sockets) do + assert.are.equal("W", socket.color) + assert.are.equal(restored.sockets[1].group, socket.group) + end + end) + + describe("Crucible modifier source", function() + local popupCount + + before_each(function() + popupCount = #main.popups + end) + + after_each(function() + while #main.popups > popupCount do + main:ClosePopup() + end + end) + + local function selectSource(controls, sourceId) + for index, source in ipairs(controls.source.list) do + if source.sourceId == sourceId then + controls.source:SetSel(index) + return + end + end + error("Missing modifier source: " .. sourceId) + end + + it("switches sources and round-trips legacy nodes without replacing explicit modifiers", function() + local itemsTab = build.itemsTab + itemsTab:CreateDisplayItemFromRaw("Rarity: Rare\nTest Axe\nRusted Hatchet\nImplicits: 0\n+10 to Strength") + itemsTab:AddCustomModifierToDisplayItem() + local popup = main.popups[1] + local controls = popup.controls + local originalSource = controls.source:GetSelValue().sourceId + selectSource(controls, "CRUCIBLE") + local node = controls.modSelectNode1 + local selectedMod + for index, entry in ipairs(node.list) do + if entry ~= "None" and #entry.mod == 2 then + node:SetSel(index) + selectedMod = entry + break + end + end + assert.is_not_nil(selectedMod) + selectSource(controls, originalSource) + selectSource(controls, "CRUCIBLE") + assert.are.equals(selectedMod, node:GetSelValue()) + controls.save:Click() + assert.are.equals("+10 to Strength", itemsTab.displayItem.explicitModLines[1].line) + assert.are.equals(2, #itemsTab.displayItem.crucibleModLines) + + itemsTab:CreateDisplayItemFromRaw(itemsTab.displayItem:BuildRaw()) + itemsTab:AddCustomModifierToDisplayItem() + controls = main.popups[1].controls + selectSource(controls, "CRUCIBLE") + assert.are.equals(selectedMod.defaultOrder, controls.modSelectNode1:GetSelValue().defaultOrder) + controls.modSelectNode1:SetSel(1) + selectSource(controls, "CUSTOM") + controls.custom:SetText("+20 to maximum Life") + controls.save:Click() + assert.are.equals(2, #itemsTab.displayItem.crucibleModLines) + assert.are.equals("+20 to maximum Life", itemsTab.displayItem.explicitModLines[2].line) + + itemsTab:AddCustomModifierToDisplayItem() + controls = main.popups[1].controls + selectSource(controls, "CRUCIBLE") + for i = 1, 5 do + controls["modSelectNode" .. i]:SetSel(1) + end + controls.save:Click() + assert.are.equals(0, #itemsTab.displayItem.crucibleModLines) + assert.are.equals(2, #itemsTab.displayItem.explicitModLines) + end) + end) + it("shows versioned reusable variant groups", function() build.itemsTab:CreateDisplayItemFromRaw([[ Rarity: Unique diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 8075f75884c..8e90b9f5b53 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -7,6 +7,123 @@ describe("TestSkills", function() -- newBuild() takes care of resetting everything in setup() end) + local function equipSocketedItem(base, slot) + build.itemsTab:CreateDisplayItemFromRaw("Rarity: Normal\n" .. base .. "\nSockets: R-G B") + build.itemsTab:AddDisplayItem() + build.skillsTab:PasteSocketGroup("Slot: " .. slot .. "\nFireball 20/0 1\n") + runCallback("OnFrame") + return build.itemsTab.slots[slot].selItemId + end + + it("preserves Skills socket edits through Items undo and build reload", function() + local id = equipSocketedItem("Plate Vest", "Body Armour") + local itemsTab = build.itemsTab + local controls = build.skillsTab.controls + controls.displayItemLink2.changeFunc(true) + controls.displayItemSocket2:SetSel(3) + itemsTab:Undo() + assert.are.equal("G", itemsTab.items[id].sockets[2].color) + itemsTab:Redo() + loadBuildFromXML(build:SaveDB("test")) + local sockets = build.itemsTab.items[id].sockets + assert.are.equal("B", sockets[2].color) + assert.are.equal(sockets[1].group, sockets[3].group) + end) + + it("undoes socket and skill edits in order without undoing unrelated Items changes", function() + local itemsTab = build.itemsTab + local bodyId = equipSocketedItem("Plate Vest", "Body Armour") + local glovesId = equipSocketedItem("Iron Gauntlets", "Gloves") + local tab = build.skillsTab + tab:SetDisplayGroup(tab.socketGroupList[1]) + tab.controls.groupLabel:SetText("Before socket edits", true) + tab.controls.displayItemSocket1:SetSel(3) + tab.controls.groupLabel:SetText("Between socket edits", true) + tab:SetDisplayGroup(tab.socketGroupList[2]) + tab.controls.displayItemSocket1:SetSel(4) + itemsTab:CreateDisplayItemFromRaw("Rarity: Normal\nPaua Amulet") + itemsTab:AddDisplayItem() + local amuletId = itemsTab.slots.Amulet.selItemId + + tab:Undo() + assert.are.equal("R", itemsTab.items[glovesId].sockets[1].color) + assert.are.equal("B", itemsTab.items[bodyId].sockets[1].color) + assert.are.equal("Between socket edits", tab.socketGroupList[1].label) + tab:Undo() + assert.are.equal("Before socket edits", tab.socketGroupList[1].label) + assert.are.equal("B", itemsTab.items[bodyId].sockets[1].color) + tab:Undo() + assert.are.equal("R", itemsTab.items[bodyId].sockets[1].color) + assert.are.equal("Before socket edits", tab.socketGroupList[1].label) + tab:Redo() + assert.are.equal("B", itemsTab.items[bodyId].sockets[1].color) + tab:Redo() + assert.are.equal("Between socket edits", tab.socketGroupList[1].label) + tab:Redo() + assert.are.equal("W", itemsTab.items[glovesId].sockets[1].color) + assert.are.equal(amuletId, itemsTab.slots.Amulet.selItemId) + assert.is_not_nil(itemsTab.items[amuletId]) + itemsTab:Undo() + assert.are.equal("R", itemsTab.items[glovesId].sockets[1].color) + assert.are.equal(amuletId, itemsTab.slots.Amulet.selItemId) + end) + + it("leaves later Items changes intact when Skills socket history becomes stale", function() + for _, change in ipairs({ "undo", "sockets", "delete", "replace" }) do + newBuild() + local itemsTab = build.itemsTab + local id = equipSocketedItem("Plate Vest", "Body Armour") + local tab = build.skillsTab + tab.controls.groupLabel:SetText("Keep this label", true) + tab.controls.displayItemSocket1:SetSel(3) + if change == "undo" then + itemsTab:Undo() + elseif change == "sockets" then + local item = itemsTab.items[id] + item.sockets = copyTable(item.sockets) + item.sockets[1].color = "G" + item:BuildAndParseRaw() + itemsTab:AddUndoState() + else + itemsTab:DeleteItem(itemsTab.items[id]) + if change == "replace" then + itemsTab:AddItem(new("Item"):Item("Rarity: Normal\nPlate Vest\nSockets: B-G B")) + itemsTab:AddUndoState() + end + end + local item = itemsTab.items[id] + local raw = item and item:BuildRaw() + tab:Undo() + assert.are.equal("Keep this label", tab.socketGroupList[1].label) + assert.are.equal(item, itemsTab.items[id]) + assert.are.equal(raw, item and item:BuildRaw()) + tab:Redo() + assert.are.equal(item, itemsTab.items[id]) + assert.are.equal(raw, item and item:BuildRaw()) + end + end) + + it("undoes socket optimization and resets socket history when loading a build", function() + local id = equipSocketedItem("Plate Vest", "Body Armour") + local tab = build.skillsTab + tab.controls.optimiseSockets:Click() + assert.are.equal(1, #build.itemsTab.items[id].sockets) + tab:Undo() + assert.are.equal(3, #build.itemsTab.items[id].sockets) + assert.are.equal("R", build.itemsTab.items[id].sockets[1].color) + tab:Redo() + assert.are.equal(1, #build.itemsTab.items[id].sockets) + assert.are.equal("B", build.itemsTab.items[id].sockets[1].color) + loadBuildFromXML(build:SaveDB("test")) + tab = build.skillsTab + tab:SetDisplayGroup(tab.socketGroupList[1]) + tab:Undo() + assert.are.equal("B", build.itemsTab.items[id].sockets[1].color) + tab.controls.displayItemSocket1:SetSel(1) + tab:Undo() + assert.are.equal("B", build.itemsTab.items[id].sockets[1].color) + end) + it("adds envy, ensures +1 level keeps level 25 Envy", function() build.itemsTab:CreateDisplayItemFromRaw("New Item\nAssassin Bow\nGrants Level 1 Summon Raging Spirit\nGrants Level 25 Envy Skill") build.itemsTab:AddDisplayItem() diff --git a/src/Classes/CalcSectionControl.lua b/src/Classes/CalcSectionControl.lua index 668e742d437..cbe1bd74b83 100644 --- a/src/Classes/CalcSectionControl.lua +++ b/src/Classes/CalcSectionControl.lua @@ -25,6 +25,7 @@ function CalcSectionClass:CalcSectionControl(calcsTab, width, id, group, colour, self.group = group self.colour = colour self.width = width + self.rowLabelWidth = calcsTab.rowLabelWidth self.subSection = subSection self.flag = subSection[1].data.flag self.notFlag = subSection[1].data.notFlag @@ -125,16 +126,24 @@ function CalcSectionClass:UpdateSize() for i, subSec in ipairs(self.subSection) do self.controls["toggle"..i].y = yOffset + 3 local tempHeight = 0 + local fixedColWidth = subSec.data.colWidth + if fixedColWidth and not self.calcsTab.compactLayout then + local maxColumns = 0 + for _, rowData in ipairs(subSec.data) do + maxColumns = m_max(maxColumns, #rowData) + end + fixedColWidth = (width - self.rowLabelWidth - 2) / maxColumns + end yOffset = yOffset + 22 for _, rowData in ipairs(subSec.data) do rowData.enabled = self.calcsTab:CheckFlag(rowData) if rowData.enabled then self.enabled = true - local xOffset = 134 + local xOffset = self.rowLabelWidth for colour, colData in ipairs(rowData) do colData.xOffset = xOffset colData.yOffset = yOffset - colData.width = subSec.data.colWidth or width - 136 + colData.width = fixedColWidth or width - self.rowLabelWidth - 2 colData.height = 18 xOffset = xOffset + colData.width end @@ -335,8 +344,8 @@ function CalcSectionClass:HandleOverlayClick(key, cursorX, cursorY) for _, rowData in ipairs(subSec.data) do if self.calcsTab:CheckFlag(rowData) then for _, colData in ipairs(rowData) do - local cellX = x + (colData.xOffset or 134) - local cellW = colData.width or (overlayWidth - 136) + local cellX = x + (colData.xOffset or self.rowLabelWidth) + local cellW = colData.width or (overlayWidth - self.rowLabelWidth - 2) if cursorX >= cellX and cursorX <= cellX + cellW and cursorY >= lineY + 2 and cursorY <= lineY + 19 then if colData.format and self.calcsTab:CheckFlag(colData) then self.calcsTab:SetDisplayStat(colData, true) @@ -424,7 +433,7 @@ function CalcSectionClass:DrawOverlay(viewPort, inputEvents) if self.calcsTab.displayData and (self.overlayBreakdownCell or (self.calcsTab.displayPinned and self.calcsTab.displayData.calcSection == self)) then local cd = self.calcsTab.displayData local origX, origY = cd.x, cd.y - cd.x = x + (cd.xOffset or 134) + cd.x = x + (cd.xOffset or self.rowLabelWidth) cd.y = y + 26 + (cd.yOffset or 0) self.calcsTab.controls.breakdown:Draw(viewPort) cd.x, cd.y = origX, origY @@ -494,18 +503,18 @@ function CalcSectionClass:DrawContent(drawX, startLineY, drawWidth, actor, viewP local textColor = rowData.color or "^7" if rowData.label then SetDrawColor(rowData.bgCol or "^0") - DrawImage(nil, drawX + 2, lineY + 2, 130, 18) + DrawImage(nil, drawX + 2, lineY + 2, self.rowLabelWidth - 4, 18) if self.calcsTab:SearchMatch(rowData.label) then textColor = colorCodes.HIGHLIGHT end - DrawString(drawX + 132, lineY + 2, "RIGHT_X", 16, "VAR", textColor..rowData.label.."^7:") + DrawString(drawX + self.rowLabelWidth - 2, lineY + 2, "RIGHT_X", 16, "VAR", textColor..rowData.label.."^7:") end elseif rowData.label then - DrawString(drawX + 132, lineY + 2, "RIGHT_X", 16, "VAR", "^7"..rowData.label.."^7:") + DrawString(drawX + self.rowLabelWidth - 2, lineY + 2, "RIGHT_X", 16, "VAR", "^7"..rowData.label.."^7:") end for colour, colData in ipairs(rowData) do - local cellX = isOverlay and (drawX + (colData.xOffset or 134)) or colData.x - local cellW = isOverlay and (colData.width or (drawWidth - 136)) or colData.width + local cellX = isOverlay and (drawX + (colData.xOffset or self.rowLabelWidth)) or colData.x + local cellW = isOverlay and (colData.width or (drawWidth - self.rowLabelWidth - 2)) or colData.width local cellY = isOverlay and lineY + 2 or colData.y local cellH = colData.height or 18 diff --git a/src/Classes/CalcsTab.lua b/src/Classes/CalcsTab.lua index 7d79e865d98..5703b900179 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -35,7 +35,8 @@ function CalcsTabClass:CalcsTab(build) self.input.skill_number = 1 self.input.misc_buffMode = "EFFECTIVE" - self.colWidth = 230 + self.colWidth = 260 + self.rowLabelWidth = 145 self.sectionList = { } self.controls.search = new("EditControl"):EditControl({"TOPLEFT",self,"TOPLEFT"}, {4, 5, 260, 20}, "", "Search", "%c", 100, nil, nil, nil, true) @@ -229,6 +230,11 @@ function CalcsTabClass:Draw(viewPort, inputEvents) self.width = viewPort.width self.height = viewPort.height + -- Use the dev layout when three wider columns would overlap the scrollbar or window edge. + self.compactLayout = viewPort.width - self.controls.scrollBar.width < 3 * (260 + 8) + self.colWidth = self.compactLayout and 230 or 260 + self.rowLabelWidth = self.compactLayout and 134 or 145 + -- Arrange the sections local baseX = viewPort.x + 4 local baseY = viewPort.y + 30 @@ -237,6 +243,8 @@ function CalcsTabClass:Draw(viewPort, inputEvents) local colY = { } local maxY = 0 for _, section in ipairs(self.sectionList) do + section.width = section.widthCols * self.colWidth + 8 * (section.widthCols - 1) + section.rowLabelWidth = self.rowLabelWidth section:UpdateSize() if section.enabled and not section.isOverlay then local col @@ -263,12 +271,18 @@ function CalcsTabClass:Draw(viewPort, inputEvents) if maxCol >= 4 then col = 4 end - elseif section.group == 3 then + elseif section.group == 3 or self.compactLayout and section.group == 4 then -- Group 3: Defense (the remaining sections) -- This group is put into a 5th column if there's room for one, otherwise they are handled separately if maxCol >= 5 then col = 5 end + elseif section.group == 4 then + -- Group 4: Miscellaneous defenses + -- This group is put into a 6th column if there's room for one, otherwise it is handled separately + if maxCol >= 6 then + col = 6 + end end if col then section.x = baseX + (self.colWidth + 8) * (col - 1) @@ -287,7 +301,7 @@ function CalcsTabClass:Draw(viewPort, inputEvents) colY[c] = m_max(colY[1], colY[2], colY[3]) end for _, section in ipairs(self.sectionList) do - if section.enabled and not section.isOverlay and (main.portraitMode and section.group == 2 or section.group == 3) then + if section.enabled and not section.isOverlay and (maxCol < 4 and section.group == 2 or section.group == 3 or self.compactLayout and section.group == 4) then local col = 3 if colY[col] + section.height + 4 >= m_max(viewPort.y + viewPort.height, maxY) then -- No room in the 4th column, find the highest available location in columns 1-4 @@ -306,6 +320,27 @@ function CalcsTabClass:Draw(viewPort, inputEvents) end end end + if not self.compactLayout and maxCol < 6 then + -- There's no room for a 6th column, so miscellaneous defenses retain their previous fallback placement + for _, section in ipairs(self.sectionList) do + if section.enabled and not section.isOverlay and section.group == 4 then + local col = maxCol >= 5 and 5 or 3 + if maxCol < 5 then + local minY = colY[col] + for c = 3, 1, -1 do + if colY[c] < minY then + col = c + minY = colY[c] + end + end + end + section.x = baseX + (self.colWidth + 8) * (col - 1) + section.y = colY[col] + colY[col] = section.y + section.height + 8 + maxY = m_max(maxY, colY[col]) + end + end + end self.controls.scrollBar.height = viewPort.height self.controls.scrollBar:SetContentDimension(maxY - (baseY - 26), viewPort.height) for _, section in ipairs(self.sectionList) do diff --git a/src/Classes/CheckBoxControl.lua b/src/Classes/CheckBoxControl.lua index 048d2582a2b..e8b08226de2 100644 --- a/src/Classes/CheckBoxControl.lua +++ b/src/Classes/CheckBoxControl.lua @@ -4,6 +4,7 @@ -- Basic check box control. -- ---@class CheckBoxControl: Control, TooltipHost +---@field linkStyle? boolean Draw borderless parallel bars instead of a check mark. local CheckBoxClass = newClass("CheckBoxControl", "Control", "TooltipHost") function CheckBoxClass:CheckBoxControl(anchor, rect, label, changeFunc, tooltipText, initialState) @@ -42,36 +43,53 @@ function CheckBoxClass:Draw(viewPort, noTooltip) local size = self.width local enabled = self:IsEnabled() local mOver = self:IsMouseOver() - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOver then - SetDrawColor(1, 1, 1) - elseif self.borderFunc then - local r, g, b = self.borderFunc() - SetDrawColor(r, g, b) - else - SetDrawColor(0.5, 0.5, 0.5) - end - DrawImage(nil, x, y, size, size) - if not enabled then - SetDrawColor(0, 0, 0) - elseif self.clicked and mOver then - SetDrawColor(0.5, 0.5, 0.5) - elseif mOver then - SetDrawColor(0.33, 0.33, 0.33) + if self.linkStyle then + if mOver and enabled then + local shade = self.clicked and 0.5 or 0.2 + SetDrawColor(shade, shade, shade) + DrawImage(nil, x, y, size, self.height) + end else - SetDrawColor(0, 0, 0) - end - DrawImage(nil, x + 1, y + 1, size - 2, size - 2) - if self.state then if not enabled then SetDrawColor(0.33, 0.33, 0.33) elseif mOver then SetDrawColor(1, 1, 1) + elseif self.borderFunc then + local r, g, b = self.borderFunc() + SetDrawColor(r, g, b) + else + SetDrawColor(0.5, 0.5, 0.5) + end + DrawImage(nil, x, y, size, size) + if not enabled then + SetDrawColor(0, 0, 0) + elseif self.clicked and mOver then + SetDrawColor(0.5, 0.5, 0.5) + elseif mOver then + SetDrawColor(0.33, 0.33, 0.33) + else + SetDrawColor(0, 0, 0) + end + DrawImage(nil, x + 1, y + 1, size - 2, size - 2) + end + if self.state or self.linkStyle then + if self.linkStyle and not self.state and not mOver then + SetDrawColor(0.2, 0.2, 0.2) + elseif not enabled or not self.state then + SetDrawColor(0.33, 0.33, 0.33) + elseif mOver then + SetDrawColor(1, 1, 1) + elseif self.linkStyle then + SetDrawColor(0.8, 0.8, 0.8) else SetDrawColor(0.75, 0.75, 0.75) end - main:DrawCheckMark(x + size/2, y + size/2, size * 0.8) + if self.linkStyle then + DrawImage(nil, x, y + self.height/2 - 3, size, 2) + DrawImage(nil, x, y + self.height/2 + 1, size, 2) + else + main:DrawCheckMark(x + size/2, y + size/2, size * 0.8) + end end if enabled then SetDrawColor(1, 1, 1) @@ -86,7 +104,7 @@ function CheckBoxClass:Draw(viewPort, noTooltip) end if mOver and not noTooltip then SetDrawLayer(nil, 100) - self:DrawTooltip(x, y, size, size, viewPort, self.state) + self:DrawTooltip(x, y, size, self.height, viewPort, self.state) SetDrawLayer(nil, 0) end end diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 3d1f007cd63..8750b093957 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -41,7 +41,7 @@ function CustomModBlockClass:CustomModBlockControl(anchor, rect, configTab, bloc configTab.build.buildFlag = true end) - self.controls.titleEdit = new("EditControl"):EditControl({"LEFT", self.controls.deleteBtn, "RIGHT"}, {6, 0, 222, 18}, blockData.title or "", nil, nil, nil, function(buf) + self.controls.titleEdit = new("EditControl"):EditControl({"LEFT", self.controls.deleteBtn, "RIGHT"}, {6, 0, self.width - 122, 18}, blockData.title or "", nil, nil, nil, function(buf) blockData.title = buf configTab:AddUndoState() configTab:BuildModList() @@ -76,7 +76,7 @@ function CustomModBlockClass:CustomModBlockControl(anchor, rect, configTab, bloc end end - self.controls.textEdit = new("ResizableEditControl"):ResizableEditControl({"TOPLEFT", self, "TOPLEFT"}, {0, 22, 344, 80, 344, 40, 344, 600}, blockData.text or "", nil, "^%C\t\n", nil, function(buf) + self.controls.textEdit = new("ResizableEditControl"):ResizableEditControl({"TOPLEFT", self, "TOPLEFT"}, {0, 22, self.width, 240, self.width, 40, self.width, 600}, blockData.text or "", nil, "^%C\t\n", nil, function(buf) blockData.text = buf configTab:AddUndoState() configTab:BuildModList() @@ -96,9 +96,17 @@ function CustomModBlockClass:CustomModBlockControl(anchor, rect, configTab, bloc end function CustomModBlockClass:GetSize() - local textHeight = self.controls.textEdit and self.controls.textEdit.height or 80 + local width = self.configTab.customSection.width - 16 + if self.width ~= width then + self.width = width + self.controls.titleEdit.width = width - 122 + self.controls.textEdit.minWidth = width + self.controls.textEdit.maxWidth = width + self.controls.textEdit:SetWidth(width) + end + local textHeight = self.controls.textEdit and self.controls.textEdit.height or 240 self.height = 22 + textHeight + 4 - return 344, self.height + return self.width, self.height end function CustomModBlockClass:IsMouseOver() @@ -163,7 +171,9 @@ function ConfigTabClass:ConfigTab(build) self.controls.sectionAnchor = new("LabelControl"):LabelControl({ "TOPLEFT", self, "TOPLEFT" }, { 0, 20, 0, 0 }, "") -- Set selector - self.controls.setSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.sectionAnchor, "TOPLEFT" }, { 76, -12, 210, 20 }, nil, function(index, value) + self.controls.setLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.sectionAnchor, "TOPLEFT" }, { 18, -10, 0, 16 }, "^7Config Set:") + local setLabelWidth = self.controls.setLabel:GetSize() + self.controls.setSelect = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.setLabel, "RIGHT" }, { 4, 0, 380 - setLabelWidth - 4, 20 }, nil, function(index, value) self:SetActiveConfigSet(self.configSetOrderList[index]) self:AddUndoState() end) @@ -171,15 +181,15 @@ function ConfigTabClass:ConfigTab(build) self.controls.setSelect.enabled = function() return #self.configSetOrderList > 1 end - self.controls.setLabel = new("LabelControl"):LabelControl({ "RIGHT", self.controls.setSelect, "LEFT" }, { -2, 0, 0, 16 }, "^7Config set:") - self.controls.setManage = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.setSelect, "RIGHT" }, { 4, 0, 90, 20 }, "Manage...", function() + self.controls.setManage = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.setSelect, "RIGHT" }, { 11, 0, 90, 20 }, "Manage...", function() self:OpenConfigSetManagePopup() end) - self.controls.search = new("EditControl"):EditControl({ "TOPLEFT", self.controls.sectionAnchor, "TOPLEFT" }, { 8, 15, 360, 20 }, "", "Search", "%c", 100, function() + self.controls.searchLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.setLabel, "TOPLEFT" }, { 0, 27, 0, 16 }, "^7Search:") + self.controls.search = new("EditControl"):EditControl({ "TOPLEFT", self.controls.setSelect, "BOTTOMLEFT" }, { 0, 7, self.controls.setSelect.width, 20 }, "", nil, "%c", 100, function() self:UpdateControls() end, nil, nil, true) - self.controls.toggleConfigs = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.search, "RIGHT" }, { 10, 0, 200, 20 }, function() + self.controls.toggleConfigs = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.search, "RIGHT" }, { 11, 0, 200, 20 }, function() -- dynamic text return self.toggleConfigs and "Hide Ineligible Configurations" or "Show All Configurations" end, function() @@ -260,7 +270,7 @@ function ConfigTabClass:ConfigTab(build) local lastSection for _, varData in ipairs(varList) do if varData.section then - lastSection = new("SectionControl"):SectionControl({"TOPLEFT",self.controls.search,"BOTTOMLEFT"}, {0, 0, 360, 0}, varData.section) + lastSection = new("SectionControl"):SectionControl({"TOPLEFT",self.controls.sectionAnchor,"TOPLEFT"}, {0, 0, 380, 0}, varData.section) lastSection.varControlList = { } lastSection.col = varData.col lastSection.collapsed = false @@ -291,18 +301,19 @@ function ConfigTabClass:ConfigTab(build) t_insert(self.controls, toggle) if varData.section == "Custom Modifiers" then self.customSection = lastSection + self.customSection.width = 450 end else local control if varData.type == "check" then - control = new("CheckBoxControl"):CheckBoxControl({"TOPLEFT",lastSection,"TOPLEFT"}, {234, 0, 18}, varData.label, function(state) + control = new("CheckBoxControl"):CheckBoxControl({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 18}, varData.label, function(state) self.configSets[self.activeConfigSetId].input[varData.var] = state self:AddUndoState() self:BuildModList() self.build.buildFlag = true end) elseif varData.type == "count" or varData.type == "integer" or varData.type == "countAllowZero" or varData.type == "float" then - control = new("EditControl"):EditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {234, 0, 90, 18}, "", nil, ((varData.type == "integer" or varData.type == "countAllowZero") and "^%-%d") or (varData.type == "float" and "^%d.") or "%D", 10, function(buf, placeholder) + control = new("EditControl"):EditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 90, 18}, "", nil, ((varData.type == "integer" or varData.type == "countAllowZero") and "^%-%d") or (varData.type == "float" and "^%d.") or "%D", 10, function(buf, placeholder) if placeholder then self.configSets[self.activeConfigSetId].placeholder[varData.var] = tonumber(buf) else @@ -313,14 +324,14 @@ function ConfigTabClass:ConfigTab(build) self.build.buildFlag = true end) elseif varData.type == "list" then - control = new("DropDownControl"):DropDownControl({"TOPLEFT",lastSection,"TOPLEFT"}, {234, 0, 118, 16}, varData.list, function(index, value) + control = new("DropDownControl"):DropDownControl({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 118, 18}, varData.list, function(index, value) self.configSets[self.activeConfigSetId].input[varData.var] = value.val self:AddUndoState() self:BuildModList() self.build.buildFlag = true end) elseif varData.type == "text" and not varData.resizable then - control = new("EditControl"):EditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {8, 0, 344, 118}, "", nil, "^%C\t\n", nil, function(buf, placeholder) + control = new("EditControl"):EditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {28, 0, 344, 118}, "", nil, "^%C\t\n", nil, function(buf, placeholder) if placeholder then self.configSets[self.activeConfigSetId].placeholder[varData.var] = tostring(buf) else @@ -331,7 +342,7 @@ function ConfigTabClass:ConfigTab(build) self.build.buildFlag = true end, 16) elseif varData.type == "text" and varData.resizable then - control = new("ResizableEditControl"):ResizableEditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {8, 0, 344, 118, nil, nil, nil, 118 + 16 * 40}, "", nil, "^%C\t\n", nil, function(buf, placeholder) + control = new("ResizableEditControl"):ResizableEditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {28, 0, 344, 118, nil, nil, nil, 118 + 16 * 40}, "", nil, "^%C\t\n", nil, function(buf, placeholder) if placeholder then self.configSets[self.activeConfigSetId].placeholder[varData.var] = tostring(buf) else @@ -342,7 +353,7 @@ function ConfigTabClass:ConfigTab(build) self.build.buildFlag = true end, 16) else - control = new("Control"):Control({"TOPLEFT",lastSection,"TOPLEFT"}, {234, 0, 16, 16}) + control = new("Control"):Control({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 16, 16}) end if varData.inactiveText then @@ -1052,9 +1063,34 @@ function ConfigTabClass:Draw(viewPort, inputEvents) end end - local maxCol = m_floor((viewPort.width - 10) / 370) - local maxColY = 0 - local colY = { 0 } + -- Stack Custom Modifiers above the settings when the two panels no longer fit side by side. + local stackedLayout = viewPort.width < 756 + local availableWidth = viewPort.width - 28 + local maxCol = stackedLayout and 1 or m_max(1, m_floor((availableWidth - 18 - 320) / 390)) + local customX = stackedLayout and 18 or 18 + maxCol * 390 + self.customSection.width = stackedLayout and m_min(450, availableWidth - customX) or m_max(320, m_min(450, availableWidth - customX)) + for _, blockControl in ipairs(self.customModsBlockControls) do + if blockControl.stackedLayout ~= stackedLayout then + local textEdit = blockControl.controls.textEdit + if stackedLayout then + blockControl.wideTextHeight = textEdit.height + textEdit:SetHeight(blockControl.stackedTextHeight or 120) + else + if blockControl.stackedLayout ~= nil then + blockControl.stackedTextHeight = textEdit.height + end + textEdit:SetHeight(blockControl.wideTextHeight or textEdit.height) + end + blockControl.stackedLayout = stackedLayout + end + end + local stackedCustomHeight = 0 + if stackedLayout then + local _, customHeight = self.customSection:GetSize() + stackedCustomHeight = customHeight + end + local maxColY = stackedCustomHeight + (stackedLayout and 18 or 0) + local colY = { maxColY } for _, section in ipairs(self.sectionList) do local y = 14 section.shown = true @@ -1074,10 +1110,15 @@ function ConfigTabClass:Draw(viewPort, inputEvents) end section.collapsed = collapsed section.shown = doShow - if doShow then + if doShow and section == self.customSection then + local _, height = section:GetSize() + section.x = customX + section.y = 53 + maxColY = m_max(maxColY, height + 18) + elseif doShow then local width, height = section:GetSize() local col - if section.col and (colY[section.col] or 0) + height + 28 <= viewPort.height and 10 + section.col * 370 <= viewPort.width then + if section.col and section.col <= maxCol and (colY[section.col] or 0) + height + 28 <= viewPort.height then col = section.col else col = 1 @@ -1089,8 +1130,8 @@ function ConfigTabClass:Draw(viewPort, inputEvents) end end colY[col] = colY[col] or 0 - section.x = 10 + (col - 1) * 370 - section.y = colY[col] + 18 + section.x = 18 + (col - 1) * 390 + section.y = colY[col] + 53 colY[col] = colY[col] + height + 18 maxColY = m_max(maxColY, colY[col]) end @@ -1113,6 +1154,9 @@ function ConfigTabClass:Draw(viewPort, inputEvents) main:DrawBackground(viewPort) self:DrawControls(viewPort) + if self.controls.scrollBar:IsShown() then + self.controls.scrollBar:Draw(viewPort) + end end function ConfigTabClass:UpdateLevel() @@ -1323,7 +1367,7 @@ function ConfigTabClass:UpdateCustomModsControls() self.customSection.varControlList = { self.controls.customModsAddBlock } for index, block in ipairs(configSet.customModsList) do - local blockControl = new("CustomModBlockControl"):CustomModBlockControl({"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, 344, 120}, self, index, block) + local blockControl = new("CustomModBlockControl"):CustomModBlockControl({"TOPLEFT", self.customSection, "TOPLEFT"}, {8, 0, self.customSection.width - 16, 266}, self, index, block) blockControl.shown = function() return not self:IsSectionCollapsed(self.customSection) end diff --git a/src/Classes/DropDownControl.lua b/src/Classes/DropDownControl.lua index bb999c49a48..09878380283 100644 --- a/src/Classes/DropDownControl.lua +++ b/src/Classes/DropDownControl.lua @@ -9,6 +9,9 @@ local m_max = math.max local m_floor = math.floor ---@class DropDownControl: Control, ControlHost, TooltipHost, SearchHost +---@field arrowSize? number Arrow size independent of the label font; defaults to half the control height. +---@field clampDrop? boolean Refresh and keep the expanded menu inside the window. +---@field fontSize? Prop Text size; defaults to the dropdown row height. local DropDownClass = newClass("DropDownControl", "Control", "ControlHost", "TooltipHost", "SearchHost") function DropDownClass:DropDownControl(anchor, rect, list, selFunc, tooltipText, ignoreSearchOrder) @@ -110,7 +113,7 @@ function DropDownClass:GetDropCount() end end -function DropDownClass:DrawSearchHighlights(label, searchInfo, x, y, width, height) +function DropDownClass:DrawSearchHighlights(label, searchInfo, x, y, width, height, fontSize, textOffset) if searchInfo and searchInfo.matches then local startX = 0 local endX = 0 @@ -119,14 +122,14 @@ function DropDownClass:DrawSearchHighlights(label, searchInfo, x, y, width, heig local strippedLabel = StripEscapes(label) for _, range in ipairs(searchInfo.ranges) do if range.from - last - 1 > 0 then - startX = DrawStringWidth(height, "VAR", strippedLabel:sub(last + 1, range.from - 1)) + x + endX + startX = DrawStringWidth(fontSize, "VAR", strippedLabel:sub(last + 1, range.from - 1)) + x + endX else startX = endX end - endX = DrawStringWidth(height, "VAR", strippedLabel:sub(range.from, range.to)) + x + startX + endX = DrawStringWidth(fontSize, "VAR", strippedLabel:sub(range.from, range.to)) + x + startX last = range.to - DrawImage(nil, startX, y, endX - startX, height) + DrawImage(nil, startX, y + textOffset, endX - startX, fontSize) end SetDrawColor(1, 1, 1) end @@ -189,6 +192,7 @@ function DropDownClass:IsMouseOver() local mOver if self.dropped then + x = x + (self.dropOffset or 0) width = m_max(width, self.droppedWidth) if self.dropUp then mOver = cursorX >= x and cursorY >= y - dropExtra and cursorX < x + width and cursorY < y + height @@ -215,6 +219,8 @@ function DropDownClass:Draw(viewPort, noTooltip) local enabled = self:IsEnabled() local scrollBar = self.controls.scrollBar local lineHeight = height - 4 + local fontSize = self:GetProperty("fontSize") or lineHeight + local textOffset = (lineHeight - fontSize) / 2 self.dropHeight = lineHeight * m_min(#self.list, 20) scrollBar.y = height + 1 if y + height + self.dropHeight + 4 <= viewPort.y + viewPort.height then @@ -245,9 +251,13 @@ function DropDownClass:Draw(viewPort, noTooltip) -- fit dropHeight to filtered content but keep initial orientation self.dropHeight = m_max(m_min(self.dropHeight, self:GetDropCount() * lineHeight), lineHeight) - local mOver, mOverComp = self:IsMouseOver() local dropExtra = self.dropHeight + 4 scrollBar:SetContentDimension(lineHeight * self:GetDropCount(), self.dropHeight) + if self.clampDrop and self.dropped then + self:CheckDroppedWidth(true) + end + local mOver, mOverComp = self:IsMouseOver() + local dropX = x + (self.dropOffset or 0) local dropY = self.dropUp and y - dropExtra or y + height if not enabled then SetDrawColor(0.33, 0.33, 0.33) @@ -262,7 +272,7 @@ function DropDownClass:Draw(viewPort, noTooltip) DrawImage(nil, x, y, width, height) if self.dropped then SetDrawLayer(nil, 5) - DrawImage(nil, x, dropY, self.droppedWidth, dropExtra) + DrawImage(nil, dropX, dropY, self.droppedWidth, dropExtra) SetDrawLayer(nil, 0) end if not enabled or self.dropped then @@ -280,11 +290,12 @@ function DropDownClass:Draw(viewPort, noTooltip) else SetDrawColor(0.5, 0.5, 0.5) end - main:DrawArrow(x + width - height/2, y + height/2, height/2, height/2, "DOWN") + local arrowSize = self.arrowSize or height/2 + main:DrawArrow(x + width - arrowSize, y + height/2, arrowSize, arrowSize, "DOWN") if self.dropped then SetDrawLayer(nil, 5) SetDrawColor(0, 0, 0) - DrawImage(nil, x + 1, dropY + 1, self.droppedWidth - 2, dropExtra - 2) + DrawImage(nil, dropX + 1, dropY + 1, self.droppedWidth - 2, dropExtra - 2) SetDrawLayer(nil, 0) end if self.otherDragSource then @@ -321,10 +332,10 @@ function DropDownClass:Draw(viewPort, noTooltip) selLabel = selItem end end - SetViewport(x + 2, y + 2, width - height, lineHeight) - DrawString(0, 0, "LEFT", lineHeight, "VAR", selLabel or "") + SetViewport(x + 6, y + 2, width - arrowSize * 2 - 4, lineHeight) + DrawString(0, textOffset, "LEFT", fontSize, "VAR", selLabel or "") if selDetail ~= nil then - local dx = DrawStringWidth(lineHeight, "VAR", selDetail) + local dx = DrawStringWidth(fontSize, "VAR", selDetail) if not enabled or self.dropped then SetDrawColor(0, 0, 0) elseif mOver then @@ -332,13 +343,13 @@ function DropDownClass:Draw(viewPort, noTooltip) else SetDrawColor(0, 0, 0) end - DrawImage(nil, width - dx - 4 - 22, 0, width - 4, lineHeight) + DrawImage(nil, width - dx - 8 - 22, 0, width - 4, lineHeight) if enabled then SetDrawColor(1, 1, 1) else SetDrawColor(0.66, 0.66, 0.66) end - DrawString(width - dx - 22, 0, "LEFT", lineHeight, "VAR", selDetail) + DrawString(width - dx - 4 - 22, textOffset, "LEFT", fontSize, "VAR", selDetail) end SetViewport() @@ -358,7 +369,7 @@ function DropDownClass:Draw(viewPort, noTooltip) if self.hoverSel and not noTooltip then SetDrawLayer(nil, 100) self:DrawTooltip( - x, dropY + 2 + (self.hoverSelDrop - 1) * lineHeight - scrollBar.offset, + dropX, dropY + 2 + (self.hoverSelDrop - 1) * lineHeight - scrollBar.offset, width, lineHeight, viewPort, "HOVER", self.hoverSel, self.list[self.hoverSel]) @@ -366,7 +377,7 @@ function DropDownClass:Draw(viewPort, noTooltip) end -- draw dropdown items - SetViewport(x + 2, dropY + 2, scrollBar.enabled and width - 22 or width - 4, self.dropHeight) + SetViewport(dropX + 6, dropY + 2, scrollBar.enabled and width - 26 or width - 12, self.dropHeight) local dropIndex = 0 for index, listVal in ipairs(self.list) do local searchInfo = self.searchInfos[index] @@ -394,10 +405,10 @@ function DropDownClass:Draw(viewPort, noTooltip) else label = listVal end - DrawString(0, y, "LEFT", lineHeight, "VAR", label) + DrawString(0, y + textOffset, "LEFT", fontSize, "VAR", label) if detail ~= nil then local detail = listVal.detail - local dx = DrawStringWidth(lineHeight, "VAR", detail) + local dx = DrawStringWidth(fontSize, "VAR", detail) if index == self.hoverSel then SetDrawColor(0.33, 0.33, 0.33) else @@ -410,14 +421,14 @@ function DropDownClass:Draw(viewPort, noTooltip) else SetDrawColor(0.66, 0.66, 0.66) end - DrawString(width - dx - 4 - 22, y, "LEFT", lineHeight, "VAR", detail) + DrawString(width - dx - 4 - 22, y + textOffset, "LEFT", fontSize, "VAR", detail) end - self:DrawSearchHighlights(label, searchInfo, 0, y, width - 4, lineHeight) + self:DrawSearchHighlights(label, searchInfo, 0, y, width - 4, lineHeight, fontSize, textOffset) end end SetDrawColor(1, 1, 1) if self:IsSearchActive() and self:GetMatchCount() == 0 then - DrawString(0, 0 , "LEFT", lineHeight, "VAR", "") + DrawString(0, textOffset, "LEFT", fontSize, "VAR", "") end SetViewport() SetDrawLayer(nil, 0) @@ -541,7 +552,7 @@ function DropDownClass:CheckDroppedWidth(enable) if self.dropped and self.controls.scrollBar.enabled then scrollWidth = self.controls.scrollBar.width end - local lineHeight = self.height - 4 + local fontSize = self:GetProperty("fontSize") or self.height - 4 -- do not be smaller than the created width local dWidth = self.width @@ -550,7 +561,7 @@ function DropDownClass:CheckDroppedWidth(enable) line = line.label or "" end -- +10 to stop clipping - dWidth = m_max(dWidth, DrawStringWidth(lineHeight, "VAR", line) + 10) + dWidth = m_max(dWidth, DrawStringWidth(fontSize, "VAR", line) + 10) end -- no greater than self.maxDroppedWidth self.droppedWidth = m_min(dWidth + scrollWidth, self.maxDroppedWidth) @@ -561,13 +572,12 @@ function DropDownClass:CheckDroppedWidth(enable) end -- add 20 to account for the 'down arrow' in the box local boxWidth - boxWidth = DrawStringWidth(lineHeight, "VAR", line or "") + 20 + boxWidth = DrawStringWidth(fontSize, "VAR", line or "") + 20 self.width = m_max(m_min(boxWidth, 390), 190) end - - self.controls.scrollBar.x = self.droppedWidth - self.width - 1 else self.droppedWidth = self.width - self.controls.scrollBar.x = -1 end + self.dropOffset = self.clampDrop and m_min(0, main.screenW - 6 - self:GetPos() - self.droppedWidth) or 0 + self.controls.scrollBar.x = self.dropOffset + self.droppedWidth - self.width - 1 end diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index 899f5b0bcfe..140a4ebc1ba 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -213,10 +213,10 @@ function EditClass:UpdateScrollBars() local width, height = self:GetSize() local textHeight = self.lineHeight or (height - 4) if self.lineHeight then - self.controls.scrollBarH:SetContentDimension(DrawStringWidth(textHeight, self.font, self.buf) + 2, width - 18) + self.controls.scrollBarH:SetContentDimension(DrawStringWidth(textHeight, self.font, self.buf) + 2, width - 22) self.controls.scrollBarV:SetContentDimension(newlineCount(self.buf.."\n") * textHeight, height - (self.controls.scrollBarH.enabled and 18 or 4)) else - self.controls.scrollBarH:SetContentDimension(DrawStringWidth(textHeight, self.font, self.buf) + 2, width - 4 - (self.prompt and DrawStringWidth(textHeight, self.font, self.prompt) + textHeight/2 or 0)) + self.controls.scrollBarH:SetContentDimension(DrawStringWidth(textHeight, self.font, self.buf) + 2, width - 8 - (self.prompt and DrawStringWidth(textHeight, self.font, self.prompt) + textHeight/2 or 0)) end end @@ -271,7 +271,7 @@ function EditClass:Draw(viewPort, noTooltip) SetDrawColor(0, 0, 0) end DrawImage(nil, x + 1, y + 1, width - 2, height - 2) - local textX = x + 2 + local textX = x + 4 local textY = y + 2 local textHeight = self.lineHeight or (height - 4) if self.prompt then @@ -291,10 +291,10 @@ function EditClass:Draw(viewPort, noTooltip) SetDrawLayer(nil, 0) end self:UpdateScrollBars() - local marginL = textX - x - 2 + local marginL = textX - x - 4 local marginR = self.controls.scrollBarV:IsShown() and 14 or 0 local marginB = self.controls.scrollBarH:IsShown() and 14 or 0 - SetViewport(textX, textY, width - 4 - marginL - marginR, height - 4 - marginB) + SetViewport(textX, textY, width - 8 - marginL - marginR, height - 4 - marginB) if not self.hasFocus then if self.buf == '' and self.placeholder then SetDrawColor(self.disableCol) @@ -495,7 +495,7 @@ function EditClass:OnKeyDown(key, doubleClick) self.drag = true local x, y = self:GetPos() local width, height = self:GetSize() - local textX = x + 2 + local textX = x + 4 local textY = y + 2 local textHeight = self.lineHeight or (height - 4) if self.prompt then diff --git a/src/Classes/GemSelectControl.lua b/src/Classes/GemSelectControl.lua index e87c36ecbc2..e1a243fcca3 100644 --- a/src/Classes/GemSelectControl.lua +++ b/src/Classes/GemSelectControl.lua @@ -628,7 +628,7 @@ function GemSelectClass:Draw(viewPort, noTooltip) else hoverControl = self.skillsTab:GetMouseOverControl() end - if hoverControl and hoverControl._className == "GemSelectControl" then + if not self.imbuedSelect and hoverControl and hoverControl._className == "GemSelectControl" and not hoverControl.imbuedSelect then local thisGem = self.skillsTab.displayGroup.gemList[self.index] local hoverGem = self.skillsTab.displayGroup.gemList[hoverControl.index] if thisGem and hoverGem and thisGem.enabled and hoverGem.enabled and thisGem.gemData and hoverGem.gemData and diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index a44f91bc0aa..a0600ccf53d 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -8,12 +8,29 @@ local t_insert = table.insert local t_remove = table.remove local b_rshift = bit.rshift local band = bit.band +local m_min = math.min local m_max = math.max local dkjson = require "dkjson" local influenceInfo = itemLib.influenceInfo.all +local checkBoxSpacing = 13 +local NARROW_VIEWPORT_WIDTH = 756 +local OAUTH_SECTION_HEIGHT = 210 +local NARROW_OAUTH_SECTION_HEIGHT = 244 +local NARROW_SITE_SECTION_HEIGHT = 260 +local OAUTH_ACTION_BUTTON_WIDTH = 220 +local OAUTH_REALM_WIDTH = 60 +local OAUTH_ACTION_COLUMN_GAP = 8 +local OAUTH_ACTION_ROW_GAP = 8 +local NARROW_BUILD_CODE_WIDTH = 220 +local NARROW_IMPORT_CODE_WIDTH = 300 +local BUILD_SHARE_BUTTON_GAP = 8 + +local function checkBoxLabelWidth(label) + return DrawStringWidth(14, "VAR", label) + 5 +end local realmList = { { label = "PC", id = "PC", realmCode = "pc", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, @@ -53,45 +70,45 @@ local function addOAuthControls(self) self.oauthTimer = nil return colorCodes.WARNING .. "Not authenticated" end - return string.format("Logging in... (%d) - URL copied to clipboard", timeLeft) .. (self.oauthErrCode or "") + return string.format("Logging in... (%d) - URL copied to clipboard", timeLeft) -- user is spam changing realms and is rate limited elseif self.isAuthorized() and self.rateLimitEndTime then local timeLeft = m_max(0, self.rateLimitEndTime - os.time()) if timeLeft < 0.5 then self.rateLimitEndTime = nil - return "Authenticated" + return colorCodes.POSITIVE .. "Authenticated" end return colorCodes.WARNING .. string.format("You're doing that too fast. Please wait (%d)", timeLeft) elseif self.isAuthorized() and self.oauthLoading then return fetchButtonEnabled() and "Fetching..." or "Importing..." elseif self.isAuthorized() then - return "Authenticated" + return colorCodes.POSITIVE .. "Authenticated" end return "" end -- space after labels - local labelSpacing = 6 - -- space between rows - local rowSpacing = 6 + local labelSpacing = 8 + -- Offset between labels that leaves 8 units between the 20-unit controls in each row. + local rowSpacing = 13 self.controls.charImportStatusLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.sectionOauthCharImport, "TOPLEFT" }, - { labelSpacing, 14, 200, 16 }, function() + { labelSpacing, 16, 200, 16 }, function() return "^7Character import status: " .. charImportStatus() end) - self.controls.logoutApiButton = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.charImportStatusLabel, "TOPRIGHT" }, - { labelSpacing, 0, 170, 16 }, "^7Logout from Path of Exile API", function() + self.controls.logoutApiButton = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.charImportStatusLabel, "RIGHT" }, + { labelSpacing, 0, OAUTH_ACTION_BUTTON_WIDTH, 20 }, "^7Logout from Path of Exile API", function() main.api:ResetDetails() main:SaveSettings() end) self.controls.logoutApiButton.shown = function() return self.usingOauth and self.isAuthorized() end self.controls.characterImportAnchor = new("Control"):Control({ "TOPLEFT", self.controls.sectionOauthCharImport, "TOPLEFT" }, - { labelSpacing, 40, 200, 16 }) + { labelSpacing, 38, 200, 20 }) self.controls.sectionOauthCharImport.height = function() - return self.isAuthorized() and 200 or 60 + return self.isAuthorized() and (self.narrowLayout and NARROW_OAUTH_SECTION_HEIGHT or OAUTH_SECTION_HEIGHT) or 68 end -- realm select @@ -162,7 +179,8 @@ local function addOAuthControls(self) end self.controls.authenticateButton = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.characterImportAnchor, "TOPLEFT" }, - { 0, 0, 200, 16 }, "^7Authorize with Path of Exile", function() + { 0, 0, 200, 20 }, "^7Authorize with Path of Exile", function() + self.oauthErrCode = nil main.api:FetchAuthToken(function(errCode) if errCode then self.oauthErrCode = errCode @@ -184,20 +202,20 @@ local function addOAuthControls(self) -- Stage: select realm, league, character, and import data self.controls.charSelectHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.sectionOauthCharImport, "TOPLEFT" }, - { labelSpacing, 40, 200, 16 }, "^7Choose character to import data from:") + { labelSpacing, 44, 200, 16 }, "^7Choose character to import data from:") self.controls.charSelectHeader.shown = function() return self.usingOauth and self.isAuthorized() end self.controls.oauthErrorLabel = new("LabelControl"):LabelControl({ "TOPRIGHT", self.controls.sectionOauthCharImport, "TOPRIGHT" }, - { -8, 40, 0, 18 }) + { -8, 40, 0, 16 }) self.controls.oauthErrorLabel.label = function() local text = self.oauthErrCode and string.format("%sError: %s", colorCodes.NEGATIVE, self.oauthErrCode) or "" return text end - self.controls.accountRealm = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.charSelectHeader, "BOTTOMLEFT" }, - { 0, rowSpacing, 60, 20 }, realmList, function() + self.controls.accountRealm = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.logoutApiButton, "BOTTOMLEFT" }, + { 0, OAUTH_ACTION_ROW_GAP, OAUTH_REALM_WIDTH, 20 }, realmList, function() setLeaguesFromCharList() end) self.controls.accountRealm:SelByValue(main.lastRealm or "PC", "id") @@ -210,7 +228,7 @@ local function addOAuthControls(self) return "Fetch Characters" end self.controls.accountRealmFetchButton = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.accountRealm, "RIGHT" }, - { labelSpacing, 0, 130, 20 }, fetchTextFunc, fetchCharacters) + { OAUTH_ACTION_COLUMN_GAP, 0, OAUTH_ACTION_BUTTON_WIDTH - OAUTH_REALM_WIDTH - OAUTH_ACTION_COLUMN_GAP, 20 }, fetchTextFunc, fetchCharacters) self.controls.accountRealmFetchButton.enabled = fetchButtonEnabled -- league select @@ -224,13 +242,15 @@ local function addOAuthControls(self) end end - self.controls.charSelectLeagueLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.accountRealm, "BOTTOMLEFT" }, + self.controls.charSelectLeagueLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.charSelectHeader, "BOTTOMLEFT" }, { 0, rowSpacing, 0, 14 }, "^7League:") self.controls.charSelectLeague = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.charSelectLeagueLabel, "RIGHT" }, - { labelSpacing, 0, 150, 18 }, nil, onLeagueChange) + { 4, 0, 170, 20 }, nil, onLeagueChange) + self.controls.charSelectLeague.fontSize = 14 -- character select - self.controls.charSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.charSelectLeagueLabel, "BOTTOMLEFT" }, - { 0, rowSpacing, 400, 18 }, nil) + self.controls.charSelect = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.charSelectLeague, "RIGHT" }, + { 8, 0, 400, 20 }, nil) + self.controls.charSelect.fontSize = 14 self.controls.charSelect.enabled = function() return self.usingOauth and self.isAuthorized() end @@ -244,10 +264,10 @@ local function addOAuthControls(self) main.lastCharacterHash = common.sha1(charName) self.lastCharacterHash = common.sha1(charName) end - self.controls.charImportHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.charSelect, "BOTTOMLEFT" }, - { 0, rowSpacing, 200, 16 }, "^7Import:") + self.controls.charImportHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.charSelectLeagueLabel, "BOTTOMLEFT" }, + { 0, 18, 200, 14 }, "^7Import:") self.controls.charImportTree = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.charImportHeader, "RIGHT" }, - { labelSpacing, 0, 170, 20 }, "Passive Tree and Jewels", function() + { self.controls.charSelectLeagueLabel:GetProperty("width") + 4 - self.controls.charImportHeader:GetProperty("width"), 0, 170, 20 }, "Passive Tree and Jewels", function() local realm = self.controls.accountRealm:GetSelValue() local league = self.controls.charSelectLeague:GetSelValue() local selectedName = self.controls.charSelect:GetSelValue().label @@ -282,9 +302,9 @@ local function addOAuthControls(self) return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() end self.controls.charImportTreeClearJewels = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.charImportTree, "RIGHT" }, - { 90, 0, 18 }, "Delete jewels:", nil, "Delete all equipped jewels when importing.", true) + { checkBoxLabelWidth("Overwrite Jewels:") + checkBoxSpacing, 0, 18 }, "Overwrite Jewels:", nil, "Overwrite all equipped jewels when importing.", true) self.controls.charImportItems = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.charImportTree, "BOTTOMLEFT" }, - { 0, rowSpacing, 110, 20 }, "Items and Skills", function() + { 0, 12, 110, 20 }, "Items and Skills", function() local realm = self.controls.accountRealm:GetSelValue() local league = self.controls.charSelectLeague:GetSelValue() local selectedName = self.controls.charSelect:GetSelValue().label @@ -312,30 +332,54 @@ local function addOAuthControls(self) self.controls.charImportItems.enabled = function() return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() end + self.controls.charImportAll = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.charImportItems, "BOTTOMLEFT" }, + { 0, 12, 110, 20 }, "All (Overwrite)", function() + local realm = self.controls.accountRealm:GetSelValue() + local league = self.controls.charSelectLeague:GetSelValue() + local selectedName = self.controls.charSelect:GetSelValue().label + main:OpenConfirmPopup("Character Import", "Importing all will overwrite your current passive tree, jewels, equipment and skills.", + "Import", function() + saveDetails(realm.id, league, selectedName) + self.oauthLoading = true + main.api:DownloadCharacter(realm.realmCode, selectedName, function(data, errMsg) + if data and data.character then + self.oauthErrCode = nil + self:ImportItemsAndSkills(data.character, true, true, false) + self:ImportPassiveTreeAndJewels(data.character, true) + else + self.oauthErrCode = errMsg and "Could not import: " .. errMsg or "Could not import character" + end + self.oauthLoading = false + end) + end) + end) + self.controls.charImportAll.enabled = function() + return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() and not self.oauthLoading + end self.controls.charImportItemsClearSkills = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.charImportItems, "RIGHT" }, - { 85, 0, 18 }, "Delete skills:", nil, "Delete all existing skills when importing.", true) + { checkBoxLabelWidth("Overwrite Skills:") + checkBoxSpacing, 0, 18 }, "Overwrite Skills:", nil, "Overwrite all existing skills when importing.", true) self.controls.charImportItemsClearItems = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.charImportItems, "RIGHT" }, - { 220, 0, 18 }, "Delete equipment:", nil, "Delete all equipped items when importing.", true) + { self.controls.charImportItemsClearSkills.x + self.controls.charImportItemsClearSkills.width + checkBoxLabelWidth("Overwrite Equipment:") + checkBoxSpacing, 0, 18 }, "Overwrite Equipment:", nil, "Overwrite all equipped items when importing.", true) self.controls.charImportItemsIgnoreWeaponSwap = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.charImportItems, - "RIGHT" }, { 380, 0, 18 }, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) + "RIGHT" }, { self.controls.charImportItemsClearItems.x + self.controls.charImportItemsClearItems.width + checkBoxLabelWidth("Ignore weapon swap:") + checkBoxSpacing, 0, 18 }, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) end local function addAccountNameControls(self) self.charImportMode = "GETACCOUNTNAME" - self.charImportStatus = "Idle" + self.charImportStatus = colorCodes.WARNING .. "Idle" self.controls.siteCharImportStatusLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.sectionCharSiteImport, "TOPLEFT" }, - { 6, 14, 200, 16 }, function() + { 8, 16, 200, 16 }, function() return "^7Character import status: " .. self.charImportStatus end) -- Stage: input account name self.controls.siteAccountNameHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.sectionCharSiteImport, "TOPLEFT" }, - { 6, 40, 250, 16 }, "^7To start importing a character, enter the character's account name:") + { 8, 40, 250, 16 }, "^7To start importing a character, enter the character's account name:") self.controls.siteAccountNameHeader.shown = function() return self.charImportMode == "GETACCOUNTNAME" end self.controls.siteAccountRealm = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.siteAccountNameHeader, "BOTTOMLEFT" }, - { 0, 4, 60, 20 }, realmList) + { 0, 6, 60, 20 }, realmList) self.controls.siteAccountRealm:SelByValue(main.lastRealm or "PC", "id") self.controls.siteAccountName = new("EditControl"):EditControl({ "LEFT", self.controls.siteAccountRealm, "RIGHT" }, { 8, 0, 200, 20 }, main.lastAccountName or "", nil, "%c", nil, nil, nil, nil, true) @@ -403,31 +447,33 @@ local function addAccountNameControls(self) end self.controls.siteAccountNameUnicode = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.siteAccountRealm, "BOTTOMLEFT" }, - { 0, 34, 0, 14 }, - "^7Note: if the account name contains non-ASCII characters it must be pasted into the textbox,\nnot typed manually.") + { 0, 4, 0, 14 }, + colorCodes.DISABLED .. "Note: Account names containing non-ASCII characters must be pasted, not typed manually.") -- Stage: select character and import data - self.controls.siteCharSelectHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.sectionCharSiteImport, "TOPLEFT" }, - { 6, 40, 200, 16 }, "^7Choose character to import data from:") + self.controls.siteCharSelectHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.siteCharImportStatusLabel, "BOTTOMLEFT" }, + { 0, 8, 200, 16 }, "^7Choose character to import data from:") self.controls.siteCharSelectHeader.shown = function() return self.charImportMode == "SELECTCHAR" or self.charImportMode == "IMPORTING" end self.controls.siteCharSelectLeagueLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.siteCharSelectHeader, "BOTTOMLEFT" }, - { 0, 6, 0, 14 }, "^7League:") + { 0, 9, 0, 14 }, "^7League:") self.controls.siteCharSelectLeague = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.siteCharSelectLeagueLabel, "RIGHT" }, - { 4, 0, 150, 18 }, nil, function(index, value) + { 4, 0, self.controls.charSelectLeague.width, 20 }, nil, function(index, value) local realm = self.controls.siteAccountRealm:GetSelValue() self:BuildCharacterList(realm.realmCode, value.league, self.lastCharList, self.controls.siteCharSelect) end) - self.controls.siteCharSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.siteCharSelectHeader, "BOTTOMLEFT" }, - { 0, 24, 400, 18 }) + self.controls.siteCharSelectLeague.fontSize = 14 + self.controls.siteCharSelect = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.siteCharSelectLeague, "RIGHT" }, + { 8, 0, 400, 20 }) + self.controls.siteCharSelect.fontSize = 14 self.controls.siteCharSelect.enabled = function() return self.charImportMode == "SELECTCHAR" end - self.controls.siteCharImportHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.siteCharSelect, "BOTTOMLEFT" }, - { 0, 16, 200, 16 }, "^7Import:") + self.controls.siteCharImportHeader = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.siteCharSelectLeagueLabel, "BOTTOMLEFT" }, + { 0, 18, 200, 14 }, "^7Import:") self.controls.siteCharImportTree = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.siteCharImportHeader, "RIGHT" }, - { 8, 0, 170, 20 }, "Passive Tree and Jewels", function() + { self.controls.siteCharSelectLeagueLabel:GetProperty("width") + 4 - self.controls.siteCharImportHeader:GetProperty("width"), 0, 170, 20 }, "Passive Tree and Jewels", function() local realm = self.controls.siteAccountRealm:GetSelValue() if self.build.spec:CountAllocNodes() > 0 then main:OpenConfirmPopup("Character Import", "Importing the passive tree will overwrite your current tree.", @@ -443,9 +489,9 @@ local function addAccountNameControls(self) return self.charImportMode == "SELECTCHAR" end self.controls.siteCharImportTreeClearJewels = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.siteCharImportTree, "RIGHT" }, - { 90, 0, 18 }, "Delete jewels:", nil, "Delete all equipped jewels when importing.", true) + { checkBoxLabelWidth("Overwrite Jewels:") + checkBoxSpacing, 0, 18 }, "Overwrite Jewels:", nil, "Overwrite all equipped jewels when importing.", true) self.controls.siteCharImportItems = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.siteCharImportTree, "LEFT" }, - { 0, 36, 110, 20 }, "Items and Skills", function() + { 0, 32, 110, 20 }, "Items and Skills", function() local realm = self.controls.siteAccountRealm:GetSelValue() self:DownloadItems(realm) self:SetPredefinedBuildName() @@ -453,21 +499,33 @@ local function addAccountNameControls(self) self.controls.siteCharImportItems.enabled = function() return self.charImportMode == "SELECTCHAR" end + self.controls.siteCharImportAll = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.siteCharImportItems, "BOTTOMLEFT" }, + { 0, 12, 110, 20 }, "All (Overwrite)", function() + local realm = self.controls.siteAccountRealm:GetSelValue() + main:OpenConfirmPopup("Character Import", "Importing all will overwrite your current passive tree, jewels, equipment and skills.", + "Import", function() + self:DownloadItems(realm, true) + self:SetPredefinedBuildName() + end) + end) + self.controls.siteCharImportAll.enabled = function() + return self.charImportMode == "SELECTCHAR" + end self.controls.siteCharImportItemsClearSkills = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.siteCharImportItems, "RIGHT" }, - { 85, 0, 18 }, "Delete skills:", nil, "Delete all existing skills when importing.", true) + { checkBoxLabelWidth("Overwrite Skills:") + checkBoxSpacing, 0, 18 }, "Overwrite Skills:", nil, "Overwrite all existing skills when importing.", true) self.controls.siteCharImportItemsClearItems = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.siteCharImportItems, "RIGHT" }, - { 220, 0, 18 }, "Delete equipment:", nil, "Delete all equipped items when importing.", true) + { self.controls.siteCharImportItemsClearSkills.x + self.controls.siteCharImportItemsClearSkills.width + checkBoxLabelWidth("Overwrite Equipment:") + checkBoxSpacing, 0, 18 }, "Overwrite Equipment:", nil, "Overwrite all equipped items when importing.", true) self.controls.siteCharImportItemsIgnoreWeaponSwap = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.siteCharImportItems, - "RIGHT" }, { 380, 0, 18 }, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) - self.controls.siteCharBanditNote = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.siteCharImportHeader, "BOTTOMLEFT" }, - { 0, 50, 200, 14 }, - "^7Tip: After you finish importing a character, make sure you update the bandit choice,\nas it can only be imported by logging in above.") - - self.controls.siteCharClose = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.siteCharImportHeader, "BOTTOMLEFT" }, - { 0, 90, 60, 20 }, "Close", function() + "RIGHT" }, { self.controls.siteCharImportItemsClearItems.x + self.controls.siteCharImportItemsClearItems.width + checkBoxLabelWidth("Ignore weapon swap:") + checkBoxSpacing, 0, 18 }, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) + self.controls.siteCharClose = new("ButtonControl"):ButtonControl({ "BOTTOMLEFT", self.controls.sectionCharSiteImport, "BOTTOMLEFT" }, + { 8, -10, 60, 20 }, "Close", function() + self.siteImportRequest = nil self.charImportMode = "GETACCOUNTNAME" - self.charImportStatus = "Idle" + self.charImportStatus = colorCodes.WARNING .. "Idle" end) + self.controls.siteCharClose.shown = function() + return self.charImportMode ~= "GETACCOUNTNAME" + end end ---@class ImportTab: ControlHost, Control @@ -485,30 +543,29 @@ function ImportTabClass:ImportTab(build) end - self.controls.sectionOauthCharImport = new("SectionControl"):SectionControl({ "TOPLEFT", self, "TOPLEFT" }, { 10, 18, 650, 200 }, + self.controls.sectionOauthCharImport = new("SectionControl"):SectionControl({ "TOPLEFT", self, "TOPLEFT" }, { 10, 18, 710, 202 }, "Import From Your Account") addOAuthControls(self) self.controls.sectionCharSiteImport = new("SectionControl"):SectionControl({ "TOPLEFT", self.controls.sectionOauthCharImport, "BOTTOMLEFT" }, - { 0, 18, 650, 250 }, + { 0, 24, 710, 226 }, "Import By Account Name") + self.controls.sectionCharSiteImport.height = function() + return self.narrowLayout and self.charImportMode ~= "GETACCOUNTNAME" and NARROW_SITE_SECTION_HEIGHT or 226 + end addAccountNameControls(self) -- Build import/export self.controls.sectionBuild = new("SectionControl"):SectionControl({ "TOPLEFT", self.controls.sectionCharSiteImport, "BOTTOMLEFT", true }, - { 0, 18, 650, 182 }, "Build Sharing") + { 0, 24, 710, 170 }, "Build Sharing") self.controls.generateCodeLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.sectionBuild, "TOPLEFT" }, - { 6, 14, 0, 16 }, "^7Generate a code to share this build with other Path of Building users:") - self.controls.generateCode = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.generateCodeLabel, "RIGHT" }, { 4, 0, 80, 20 }, "Generate", function() + { 8, 16, 0, 16 }, "^7Generate a code to share this build.") + self.controls.generateCode = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.generateCodeLabel, "RIGHT" }, { 0, 0, 80, 20 }, "Generate", function() self.controls.generateCodeOut:SetText(common.base64.encode(Deflate(self.build:SaveDB("code"))):gsub("+","-"):gsub("/","_")) end) - self.controls.enablePartyExportBuffs = new("CheckBoxControl"):CheckBoxControl({"LEFT",self.controls.generateCode,"RIGHT"}, {100, 0, 18}, "Export Support", function(state) - self.build.partyTab.enableExportBuffs = state - self.build.buildFlag = true - end, "This is for party play, to export support character, it enables the exporting of auras, curses and modifiers to the enemy", false) - self.controls.generateCodeOut = new("EditControl"):EditControl({"TOPLEFT",self.controls.generateCodeLabel,"BOTTOMLEFT"}, {0, 8, 250, 20}, "", "Code", "%Z") + self.controls.generateCodeOut = new("EditControl"):EditControl({"TOPLEFT",self.controls.generateCodeLabel,"BOTTOMLEFT"}, {0, BUILD_SHARE_BUTTON_GAP + 2, 268, 20}, "", "Code", "%Z") self.controls.generateCodeOut.enabled = function() return #self.controls.generateCodeOut.buf > 0 end @@ -519,6 +576,13 @@ function ImportTabClass:ImportTab(build) self.controls.generateCodeCopy.enabled = function() return #self.controls.generateCodeOut.buf > 0 end + self.controls.enablePartyExportBuffsLabel = new("LabelControl"):LabelControl({ "LEFT", self.controls.generateCodeCopy, "LEFT" }, + { 0, -(20 + BUILD_SHARE_BUTTON_GAP), 0, 14 }, "^7Include Support Data") + self.controls.enablePartyExportBuffs = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.enablePartyExportBuffsLabel, "RIGHT" }, + { 4, 0, 18 }, nil, function(state) + self.build.partyTab.enableExportBuffs = state + self.build.buildFlag = true + end, "This is for party play, to export support character, it enables the exporting of auras, curses and modifiers to the enemy", false) local getExportSitesFromImportList = function() local exportWebsites = { } @@ -532,12 +596,12 @@ function ImportTabClass:ImportTab(build) end local exportWebsitesList = getExportSitesFromImportList() - self.controls.exportFrom = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.generateCodeCopy, "RIGHT" }, { 8, 0, 120, 20 }, exportWebsitesList, function(_, selectedWebsite) + self.controls.exportFrom = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.generateCodeCopy, "RIGHT" }, { 8, 0, 112, 20 }, exportWebsitesList, function(_, selectedWebsite) main.lastExportWebsite = selectedWebsite.id self.exportWebsiteSelected = selectedWebsite.id end) self.controls.exportFrom:SelByValue(self.exportWebsiteSelected or main.lastExportWebsite or "Pastebin", "id") - self.controls.generateCodeByLink = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.exportFrom, "RIGHT" }, { 8, 0, 100, 20 }, "Share", function() + self.controls.generateCodeByLink = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.exportFrom, "RIGHT" }, { 8, 0, 80, 20 }, "Share", function() local exportWebsite = exportWebsitesList[self.controls.exportFrom.selIndex] local subScriptId = buildSites.UploadBuild(self.controls.generateCodeOut.buf, exportWebsite) if subScriptId then @@ -553,6 +617,25 @@ function ImportTabClass:ImportTab(build) end) end end) + self.controls.generateCode:SetAnchor("TOPLEFT", self.controls.generateCodeByLink, "TOPLEFT", 0, -(20 + BUILD_SHARE_BUTTON_GAP)) + local importPanelX = self.controls.sectionOauthCharImport:GetPos() + local weaponSwapX = self.controls.charImportItemsIgnoreWeaponSwap:GetPos() + local weaponSwapWidth = self.controls.charImportItemsIgnoreWeaponSwap:GetSize() + local characterSelectX = self.controls.charSelect:GetPos() + self.controls.charSelect.width = weaponSwapX + weaponSwapWidth - characterSelectX + self.controls.siteCharSelect.width = self.controls.charSelect.width + -- Leave 8 units inside the 2-unit section border. + local importPanelWidth = weaponSwapX + weaponSwapWidth - importPanelX + 10 + self.controls.sectionOauthCharImport.width = importPanelWidth + self.controls.sectionCharSiteImport.width = importPanelWidth + self.controls.sectionBuild.width = importPanelWidth + self.wideImportPanelWidth = importPanelWidth + self.wideCharacterSelectWidth = self.controls.charSelect.width + self.wideSiteCharacterSelectWidth = self.controls.siteCharSelect.width + self.wideCharImportItemsClearItemsX = self.controls.charImportItemsClearItems.x + self.wideCharImportItemsIgnoreWeaponSwapX = self.controls.charImportItemsIgnoreWeaponSwap.x + self.wideSiteCharImportItemsClearItemsX = self.controls.siteCharImportItemsClearItems.x + self.wideSiteCharImportItemsIgnoreWeaponSwapX = self.controls.siteCharImportItemsIgnoreWeaponSwap.x self.controls.generateCodeByLink.enabled = function() for _, exportSite in ipairs(exportWebsitesList) do if #self.controls.generateCodeOut.buf > 0 and self.controls.generateCodeOut.buf:match(exportSite.matchURL) then @@ -569,8 +652,8 @@ function ImportTabClass:ImportTab(build) end return #self.controls.generateCodeOut.buf > 0 end - self.controls.generateCodeNote = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.generateCodeOut,"BOTTOMLEFT"}, {0, 4, 0, 14}, "^7Note: this code can be very long; you can use 'Share' to shrink it.") - self.controls.importCodeHeader = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.generateCodeNote,"BOTTOMLEFT"}, {0, 26, 0, 16}, "^7To import a build, enter URL or code here:") + self.controls.generateCodeNote = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.generateCodeOut,"BOTTOMLEFT"}, {0, 4, 0, 14}, colorCodes.DISABLED .. "Note: This code is very long. Use the 'Share' button to generate a short URL.") + self.controls.importCodeHeader = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.generateCodeNote,"BOTTOMLEFT"}, {0, 20, 0, 16}, "^7To import a build, enter URL or code here:") local importCodeHandle = function (buf) self.importCodeSite = nil @@ -668,13 +751,13 @@ function ImportTabClass:ImportTab(build) end end - self.controls.importCodeIn = new("EditControl"):EditControl({"TOPLEFT",self.controls.importCodeHeader,"BOTTOMLEFT"}, {0, 4, 328, 20}, "", nil, nil, nil, importCodeHandle, nil, nil, true) + self.controls.importCodeIn = new("EditControl"):EditControl({"TOPLEFT",self.controls.importCodeHeader,"BOTTOMLEFT"}, {0, 4, 336, 20}, "", nil, nil, nil, importCodeHandle, nil, nil, true) self.controls.importCodeIn.enterFunc = function() if self.importCodeValid then self.controls.importCodeGo.onClick() end end - self.controls.importCodeState = new("LabelControl"):LabelControl({"LEFT",self.controls.importCodeIn,"RIGHT"}, {8, 0, 0, 16}) + self.controls.importCodeState = new("LabelControl"):LabelControl({"LEFT",self.controls.importCodeIn,"RIGHT"}, {14, 0, 0, 16}) self.controls.importCodeState.label = function() return self.importCodeDetail or "" end @@ -682,7 +765,7 @@ function ImportTabClass:ImportTab(build) self.controls.importCodeMode.enabled = function() return (self.build.dbFileName or self.controls.importCodeMode.selIndex == 3) and self.importCodeValid end - self.controls.importCodeGo = new("ButtonControl"):ButtonControl({"LEFT",self.controls.importCodeMode,"RIGHT"}, {8, 0, 160, 20}, "Import", function() + self.controls.importCodeGo = new("ButtonControl"):ButtonControl({"LEFT",self.controls.importCodeMode,"RIGHT"}, {8, 0, 128, 20}, "Import", function() if self.importCodeSite and not self.importCodeXML then self.importCodeFetching = true local selectedWebsite = buildSites.websiteList[self.importCodeSite] @@ -730,6 +813,62 @@ function ImportTabClass:ImportTab(build) return self end +function ImportTabClass:ApplyLayout(viewPort) + self.narrowLayout = viewPort.width < NARROW_VIEWPORT_WIDTH + local panelWidth = self.narrowLayout and m_min(self.wideImportPanelWidth, viewPort.width - 20) or self.wideImportPanelWidth + self.controls.sectionOauthCharImport.width = panelWidth + self.controls.sectionCharSiteImport.width = panelWidth + self.controls.sectionBuild.width = panelWidth + + if self.narrowLayout then + self.controls.charSelect:SetAnchor("TOPLEFT", self.controls.charSelectLeague, "BOTTOMLEFT", 0, 4) + self.controls.charImportHeader:SetAnchor("TOPLEFT", self.controls.charSelectLeagueLabel, "BOTTOMLEFT", 0, 42) + self.controls.charImportItemsClearSkills:SetAnchor("LEFT", self.controls.charImportItems, "RIGHT", checkBoxLabelWidth("Overwrite Skills:") + 5, 0) + self.controls.charImportItemsClearItems:SetAnchor("LEFT", self.controls.charImportItemsClearSkills, "RIGHT", checkBoxLabelWidth("Overwrite Equipment:") + 5, 0) + self.controls.charImportItemsIgnoreWeaponSwap:SetAnchor("LEFT", self.controls.charImportAll, "RIGHT", checkBoxLabelWidth("Ignore weapon swap:") + checkBoxSpacing, 0) + + self.controls.siteAccountHistory:SetAnchor("TOPLEFT", self.controls.siteAccountName, "BOTTOMLEFT", 0, 6) + self.controls.siteAccountNameMissingDiscriminator:SetAnchor("TOPLEFT", self.controls.siteAccountHistory, "BOTTOMLEFT", 0, 8) + self.controls.siteAccountNameUnicode:SetAnchor("TOPLEFT", self.controls.siteAccountRealm, "BOTTOMLEFT", 0, 30) + + self.controls.siteCharSelect:SetAnchor("TOPLEFT", self.controls.siteCharSelectLeague, "BOTTOMLEFT", 0, 4) + self.controls.siteCharImportHeader:SetAnchor("TOPLEFT", self.controls.siteCharSelectLeagueLabel, "BOTTOMLEFT", 0, 42) + self.controls.siteCharImportItemsClearSkills:SetAnchor("LEFT", self.controls.siteCharImportItems, "RIGHT", checkBoxLabelWidth("Overwrite Skills:") + 5, 0) + self.controls.siteCharImportItemsClearItems:SetAnchor("LEFT", self.controls.siteCharImportItemsClearSkills, "RIGHT", checkBoxLabelWidth("Overwrite Equipment:") + 5, 0) + self.controls.siteCharImportItemsIgnoreWeaponSwap:SetAnchor("LEFT", self.controls.siteCharImportAll, "RIGHT", checkBoxLabelWidth("Ignore weapon swap:") + checkBoxSpacing, 0) + + local sectionX = self.controls.sectionOauthCharImport:GetPos() + local characterSelectX = self.controls.charSelect:GetPos() + self.controls.charSelect.width = panelWidth - (characterSelectX - sectionX) - 10 + local siteSectionX = self.controls.sectionCharSiteImport:GetPos() + local siteCharacterSelectX = self.controls.siteCharSelect:GetPos() + self.controls.siteCharSelect.width = panelWidth - (siteCharacterSelectX - siteSectionX) - 10 + self.controls.generateCodeOut.width = m_min(NARROW_BUILD_CODE_WIDTH, panelWidth - 294) + self.controls.importCodeIn.width = m_min(NARROW_IMPORT_CODE_WIDTH, panelWidth - 28) + else + self.controls.charSelect:SetAnchor("LEFT", self.controls.charSelectLeague, "RIGHT", 8, 0) + self.controls.charImportHeader:SetAnchor("TOPLEFT", self.controls.charSelectLeagueLabel, "BOTTOMLEFT", 0, 18) + self.controls.charImportItemsClearSkills:SetAnchor("LEFT", self.controls.charImportItems, "RIGHT", checkBoxLabelWidth("Overwrite Skills:") + checkBoxSpacing, 0) + self.controls.charImportItemsClearItems:SetAnchor("LEFT", self.controls.charImportItems, "RIGHT", self.wideCharImportItemsClearItemsX, 0) + self.controls.charImportItemsIgnoreWeaponSwap:SetAnchor("LEFT", self.controls.charImportItems, "RIGHT", self.wideCharImportItemsIgnoreWeaponSwapX, 0) + + self.controls.siteAccountHistory:SetAnchor("LEFT", self.controls.siteAccountNameGo, "RIGHT", 8, 0) + self.controls.siteAccountNameMissingDiscriminator:SetAnchor("TOPLEFT", self.controls.siteAccountName, "BOTTOMLEFT", 0, 8) + self.controls.siteAccountNameUnicode:SetAnchor("TOPLEFT", self.controls.siteAccountRealm, "BOTTOMLEFT", 0, 4) + + self.controls.siteCharSelect:SetAnchor("LEFT", self.controls.siteCharSelectLeague, "RIGHT", 8, 0) + self.controls.siteCharImportHeader:SetAnchor("TOPLEFT", self.controls.siteCharSelectLeagueLabel, "BOTTOMLEFT", 0, 18) + self.controls.siteCharImportItemsClearSkills:SetAnchor("LEFT", self.controls.siteCharImportItems, "RIGHT", checkBoxLabelWidth("Overwrite Skills:") + checkBoxSpacing, 0) + self.controls.siteCharImportItemsClearItems:SetAnchor("LEFT", self.controls.siteCharImportItems, "RIGHT", self.wideSiteCharImportItemsClearItemsX, 0) + self.controls.siteCharImportItemsIgnoreWeaponSwap:SetAnchor("LEFT", self.controls.siteCharImportItems, "RIGHT", self.wideSiteCharImportItemsIgnoreWeaponSwapX, 0) + + self.controls.charSelect.width = self.wideCharacterSelectWidth + self.controls.siteCharSelect.width = self.wideSiteCharacterSelectWidth + self.controls.generateCodeOut.width = 268 + self.controls.importCodeIn.width = 336 + end +end + -- attempt to fetch the last realm's character list once per instance, if there -- is a last realm saved function ImportTabClass:TryFetchCharacterList() @@ -778,6 +917,7 @@ function ImportTabClass:Draw(viewPort, inputEvents) self.y = viewPort.y self.width = viewPort.width self.height = viewPort.height + self:ApplyLayout(viewPort) self:ProcessControlsInput(inputEvents, viewPort) @@ -811,17 +951,32 @@ function ImportTabClass:SaveAccountHistory() end end -function ImportTabClass:DownloadPassiveTree(realm) +local function beginSiteImport(self) + -- This table also identifies the active operation, including both overwrite-all downloads. + local request = { + accountName = self.controls.siteAccountName.buf, + character = copyTable(self.controls.siteCharSelect:GetSelValue().char), + league = self.lastLeague or self.controls.siteCharSelectLeague:GetSelValueByKey("league"), + } + self.siteImportRequest = request + return request +end + +function ImportTabClass:DownloadPassiveTree(realm, overwriteAll, itemData, request) + request = request or beginSiteImport(self) self.charImportMode = "IMPORTING" self.charImportStatus = "Retrieving character passive tree..." - local accountName = self.controls.siteAccountName.buf - local charSelect = self.controls.siteCharSelect - local charListData = charSelect.list[charSelect.selIndex].char + local accountName = request.accountName + local charListData = request.character launch:DownloadPage( realm.hostName .. "character-window/get-passive-skills?accountName=" .. accountName:gsub("#", "%%23") .. "&character=" .. urlEncode(charListData.name) .. "&realm=" .. realm.realmCode, function(response, errMsg) + if self.siteImportRequest ~= request or self.build.importTab ~= self then + return + end + self.siteImportRequest = nil self.charImportMode = "SELECTCHAR" if errMsg then self.charImportStatus = colorCodes.NEGATIVE .. @@ -833,7 +988,7 @@ function ImportTabClass:DownloadPassiveTree(realm) end self.lastCharacterHash = common.sha1(charListData.name) if not self.lastLeague then - self.lastLeague = self.controls.siteCharSelectLeague:GetSelValueByKey("league") + self.lastLeague = request.league end local responseLua = dkjson.decode(response.body) -- Account-name imports omit quest choices, so keep the build's current values. @@ -844,47 +999,62 @@ function ImportTabClass:DownloadPassiveTree(realm) local charData = copyTable(charListData) charData.passives = responseLua charData.jewels = responseLua.items - local deleteJewels = self.controls.siteCharImportTreeClearJewels.state + if itemData then + self:ImportItemsAndSkills(itemData, true, true, false) + end + local deleteJewels = overwriteAll or self.controls.siteCharImportTreeClearJewels.state self:ImportPassiveTreeAndJewels(charData, deleteJewels) end) end -function ImportTabClass:DownloadItems(realm) +function ImportTabClass:DownloadItems(realm, overwriteAll) + local request = beginSiteImport(self) self.charImportMode = "IMPORTING" self.charImportStatus = "Retrieving character items..." - local accountName = self.controls.siteAccountName.buf - local charSelect = self.controls.siteCharSelect - local charListData = charSelect.list[charSelect.selIndex].char + local accountName = request.accountName + local charListData = request.character launch:DownloadPage( realm.hostName .. "character-window/get-items?accountName=" .. accountName:gsub("#", "%%23") .. "&character=" .. urlEncode(charListData.name) .. "&realm=" .. realm.realmCode, function(response, errMsg) + if self.siteImportRequest ~= request or self.build.importTab ~= self then + return + end self.charImportMode = "SELECTCHAR" if errMsg then + self.siteImportRequest = nil self.charImportStatus = colorCodes.NEGATIVE .. "Error importing character data, try again (" .. errMsg:gsub("\n", " ") .. ")" return elseif response.body == "false" then + self.siteImportRequest = nil self.charImportStatus = colorCodes.NEGATIVE .. "Failed to retrieve character data, try again." return end self.lastCharacterHash = common.sha1(charListData.name) if not self.lastLeague then - self.lastLeague = self.controls.siteCharSelectLeague:GetSelValueByKey("league") + self.lastLeague = request.league end local responseLua = dkjson.decode(response.body) -- modify response to be like the oauth API response local charData = copyTable(charListData) charData.equipment = responseLua.items charData.guardian = responseLua.guardian - local clearItems = self.controls.siteCharImportItemsClearItems.state - local clearSkills = self.controls.siteCharImportItemsClearSkills.state - local ignoreWeaponSwap = self.controls.siteCharImportItemsIgnoreWeaponSwap.state - self:ImportItemsAndSkills(charData, clearItems, clearSkills, ignoreWeaponSwap) + if overwriteAll then + -- Retrieve both responses before overwriting any part of the build. + self:DownloadPassiveTree(realm, true, charData, request) + else + self.siteImportRequest = nil + local clearItems = self.controls.siteCharImportItemsClearItems.state + local clearSkills = self.controls.siteCharImportItemsClearSkills.state + local ignoreWeaponSwap = self.controls.siteCharImportItemsIgnoreWeaponSwap.state + self:ImportItemsAndSkills(charData, clearItems, clearSkills, ignoreWeaponSwap) + end end) end function ImportTabClass:DownloadSiteCharacterList(realm) + self.siteImportRequest = nil function FindMatchingStandardLeague(league) -- Find a Standard league name for a given league name -- Reference https://api.pathofexile.com/league?realm=pc @@ -963,7 +1133,7 @@ function ImportTabClass:DownloadSiteCharacterList(realm) realAccountName = realAccountName:gsub("(.*)[#%-]", "%1#") accountName = realAccountName self.controls.siteAccountName:SetText(realAccountName) - self.charImportStatus = "Character list successfully retrieved." + self.charImportStatus = colorCodes.POSITIVE .. "Character list successfully retrieved." self.charImportMode = "SELECTCHAR" self.lastRealm = realm.id main.lastRealm = realm.id diff --git a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua index bc5c227bf3d..9143cb87169 100644 --- a/src/Classes/ItemDBControl.lua +++ b/src/Classes/ItemDBControl.lua @@ -25,6 +25,7 @@ local ItemDBClass = newClass("ItemDBControl", "ListControl") ---@param dbType "RARE"|"UNIQUE" function ItemDBClass:ItemDBControl(anchor, rect, itemsTab, db, dbType) self:ListControl(anchor, rect, 16, "VERTICAL", false) + self.rowTextInset = 2 self.itemsTab = itemsTab self.db = db self.dbType = dbType @@ -41,27 +42,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) - self.listBuildFlag = true + 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:UpdateTypeList() 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}, copyTable(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) @@ -90,33 +93,62 @@ function ItemDBClass:LoadLeaguesAndTypes() t_insert(self.typeList, type) end self.leaguesAndTypesLoaded = true + self:UpdateTypeList() end -function ItemDBClass:DoesItemMatchFilters(item) - if self.controls.slot.selIndex > 1 then - local primarySlot = item:GetPrimarySlot() - if primarySlot ~= self.slotList[self.controls.slot.selIndex] and primarySlot:gsub(" %d","") ~= self.slotList[self.controls.slot.selIndex] then - return false - end +function ItemDBClass:DoesItemMatchSlot(item) + local slotName = self.controls.slot:GetSelValue() + if slotName == "Any slot" then + return true + elseif slotName == "Jewel" then + -- The database filter does not select a particular passive tree socket. + return item.type == "Jewel" + elseif (slotName == "Weapon 1" or slotName == "Weapon 2") and self.itemsTab.activeItemSet.useSecondWeaponSet then + slotName = slotName .. " Swap" end - local typeSel = self.controls.type.selIndex - if typeSel > 1 then - if typeSel == 2 then - if not item.base.armour then - return false - end - elseif typeSel == 3 then - if not (item.type == "Amulet" or item.type == "Ring" or item.type == "Belt") then - return false - end - elseif typeSel == 4 or typeSel == 5 then - local weaponInfo = self.itemsTab.build.data.weaponTypeInfo[item.type] - if not (weaponInfo and weaponInfo.melee and ((typeSel == 4 and weaponInfo.oneHand) or (typeSel == 5 and not weaponInfo.oneHand))) then - return false + return self.itemsTab:IsItemValidForSlot(item, slotName) +end + +function ItemDBClass:DoesItemMatchType(item, itemType) + if itemType == "Any type" then + return true + elseif itemType == "Armour" then + return item.base.armour ~= nil + elseif itemType == "Jewellery" then + return item.type == "Amulet" or item.type == "Ring" or item.type == "Belt" + elseif itemType == "One Handed Melee" or itemType == "Two Handed Melee" then + local weaponInfo = self.itemsTab.build.data.weaponTypeInfo[item.type] + return weaponInfo and weaponInfo.melee and ((itemType == "One Handed Melee" and weaponInfo.oneHand) or (itemType == "Two Handed Melee" and not weaponInfo.oneHand)) + end + return item.type == itemType +end + +function ItemDBClass:UpdateTypeList() + local selectedType = self.controls.type:GetSelValue() + local typeList = { } + for _, itemType in ipairs(self.typeList) do + local valid = itemType == "Any type" or self.controls.slot.selIndex == 1 + if not valid then + for _, item in pairs(self.db.list) do + if self:DoesItemMatchSlot(item) and self:DoesItemMatchType(item, itemType) then + valid = true + break + end end - elseif item.type ~= self.typeList[typeSel] then - return false end + if valid then + t_insert(typeList, itemType) + end + end + self.controls.type.selIndex = 1 + self.controls.type:SetList(typeList) + self.controls.type:SelByValue(selectedType) + self.listBuildFlag = true +end + +function ItemDBClass:DoesItemMatchFilters(item) + if not self:DoesItemMatchSlot(item) or not self:DoesItemMatchType(item, self.controls.type:GetSelValue()) then + return false end if self.dbType == "UNIQUE" and self.controls.league.selIndex > 1 then if (self.controls.league.selIndex == 2 and item.league) or (self.controls.league.selIndex > 2 and (not item.league or not item.league:match(self.leagueList[self.controls.league.selIndex]))) then @@ -301,8 +333,30 @@ 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 + self:UpdateTypeList() end if self.listBuildFlag then self.listBuildFlag = false @@ -341,6 +395,7 @@ function ItemDBClass:AddValueTooltip(tooltip, index, item) if tooltip:CheckForUpdate(item, IsKeyDown("SHIFT"), launch.devModeAlt, self.itemsTab.build.outputRevision) then self.itemsTab:AddItemTooltip(tooltip, item, nil, true) end + tooltip.minX = self:GetPos() + self:GetSize() + 5 end function ItemDBClass:GetDragValue(index, item) @@ -374,6 +429,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 @@ -382,6 +438,7 @@ function ItemDBClass:OnSelClick(index, item, doubleClick) -- to get stuck to the cursor self.selDragging = false self.itemsTab:CreateDisplayItemFromRaw(item.raw, true) + self.itemsTab.snapHScroll = "ITEM" return false end end diff --git a/src/Classes/ItemListControl.lua b/src/Classes/ItemListControl.lua index a38fe5fbc5e..0ebf5910533 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") @@ -16,18 +28,26 @@ local ItemListClass = newClass("ItemListControl", "ListControl") ---@param forceTooltip boolean? function ItemListClass:ItemListControl(anchor, rect, itemsTab, forceTooltip) self:ListControl(anchor, rect, 16, "VERTICAL", true, itemsTab.itemOrderList, forceTooltip) + self.rowTextInset = 2 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.deleteUnused = new("ButtonControl"):ButtonControl({"LEFT",self.controls.sort,"RIGHT"}, {4, 0, 84, 18}, "Del Unused", function() + self.controls.loadoutFilter = new("DropDownControl"):DropDownControl({"LEFT",self.controls.sortMode,"RIGHT"}, {4, 0, rowControlWidth, 18}, nil, function() + self:UpdateList() + end) + 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 @@ -48,9 +68,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) @@ -71,13 +91,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 @@ -88,7 +108,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 @@ -118,82 +138,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 - local treeTab = self.itemsTab.build.treeTab - for _, spec in ipairs(treeTab.specList) do - if (spec.title or "Default") == filterTitle then - filterSpec = spec + 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 + 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 @@ -247,26 +393,83 @@ 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 - local used = self:FindEquippedAbyssJewel(itemId, true) or self:FindSocketedJewel(itemId, true) or "" - if used == "" then + if isGroupHeader(itemId) then + return "^7" .. itemId.groupHeader + end + local item = self.itemsTab.items[itemId] + local loadoutNames = { } + for loadoutIndex = 4, #self.controls.loadoutFilter.list do + local loadoutName = self.controls.loadoutFilter.list[loadoutIndex] + local itemSet, spec = self:GetLoadoutSetAndSpec(loadoutName) + if self:IsItemInLoadout(itemId, itemSet, spec) then + t_insert(loadoutNames, loadoutName) + end + end + local used = "" + if #loadoutNames > 0 then + used = " ^9(" .. table.concat(loadoutNames, ", ") .. ")" + else + local otherUse = self:FindEquippedAbyssJewel(itemId, false) or self:FindSocketedJewel(itemId, false) local slot, itemSet = self.itemsTab:GetEquippedSlotForItem(item) - if not slot then + if otherUse or itemSet then + used = " ^9(" .. (otherUse or itemSet.title or "Default") .. ")" + elseif not slot then used = " ^9(Unused)" - elseif itemSet then - used = " ^9(Used in '" .. (itemSet.title or "Default") .. "')" end - else - used = " ^9(Used in '" .. used .. "')" end return colorCodes[item.rarity] .. item.name .. used end end function ItemListClass:AddValueTooltip(tooltip, index, itemId) - if main.popups[1] then + if main.popups[1] or isGroupHeader(itemId) then tooltip:Clear() return end @@ -274,6 +477,7 @@ function ItemListClass:AddValueTooltip(tooltip, index, itemId) if tooltip:CheckForUpdate(item, IsKeyDown("SHIFT"), launch.devModeAlt, self.itemsTab.build.outputRevision) then self.itemsTab:AddItemTooltip(tooltip, item) end + tooltip.minX = self:GetPos() + self:GetSize() + 5 end function ItemListClass:GetDragValue(index, itemId) @@ -284,11 +488,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 @@ -328,6 +532,7 @@ function ItemListClass:OnSelClick(index, itemId, doubleClick) local newItem = new("Item"):Item(item:BuildRaw()) newItem.id = item.id self.itemsTab:SetDisplayItem(newItem) + self.itemsTab.snapHScroll = "ITEM" return false end end @@ -380,7 +585,7 @@ 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 diff --git a/src/Classes/ItemSlotControl.lua b/src/Classes/ItemSlotControl.lua index a3f658a78e3..12ba8f2e30f 100644 --- a/src/Classes/ItemSlotControl.lua +++ b/src/Classes/ItemSlotControl.lua @@ -19,7 +19,7 @@ local ItemSlotClass = newClass("ItemSlotControl", "DropDownControl") ---@param slotLabel string ---@param nodeId integer? 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() @@ -127,16 +127,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 diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 0b3852aba58..75d8af8bdaf 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -18,6 +18,7 @@ local m_modf = math.modf local buySimilar = require("Classes.CompareBuySimilar") local addImplicit = require("Modules.AddImplicitPopup") local gemTooltip = require("Classes.GemTooltip") +local socketControls = require("Modules.ItemSocketControls") local rarityDropList = { { label = colorCodes.NORMAL.."Normal", rarity = "NORMAL" }, @@ -27,13 +28,6 @@ local rarityDropList = { { label = colorCodes.RELIC.."Relic", rarity = "RELIC" } } -local socketDropList = { - { label = colorCodes.STRENGTH.."R", color = "R" }, - { label = colorCodes.DEXTERITY.."G", color = "G" }, - { label = colorCodes.INTELLIGENCE.."B", color = "B" }, - { label = colorCodes.SCION.."W", color = "W" } -} - local baseSlots = { "Weapon 1", "Weapon 2", "Helmet", "Body Armour", "Gloves", "Boots", "Amulet", "Ring 1", "Ring 2", "Ring 3", "Belt", "Graft 1", "Graft 2", "Flask 1", "Flask 2", "Flask 3", "Flask 4", "Flask 5" } local forbiddenJewelCounterpart = { @@ -73,6 +67,36 @@ local function isAnointable(item) and (item.canBeAnointed or item.base.type == "Amulet") end +local function isCatalystEligible(item) + return item and item.base and (item.crafted or item.hasModTags) + and (item.base.type == "Amulet" or item.base.type == "Ring" or item.base.type == "Belt") +end + +local function buildInfluenceDisplayList(placeholder, availableInfluences) + local displayList = { placeholder } + for i, curInfluenceInfo in ipairs(availableInfluences) do + displayList[i + 1] = curInfluenceInfo.display + end + return displayList +end + +local function getEditableItemQuality(item) + if isCatalystEligible(item) then + return m_max(item.catalystQuality or 20, 0) + end + return item and item.quality or 0 +end + +local function setEditableItemQuality(item, quality) + if isCatalystEligible(item) then + if item.catalyst and item.catalyst > 0 then + item.catalystQuality = quality + end + else + item.quality = quality + end +end + local function buildModSortList() local sortList = { { label = "Default", stat = nil } } local sortStats = { } @@ -85,6 +109,14 @@ local function buildModSortList() return sortList, sortStats end +local function canAddCustomModifiers(item) + return item and (item.rarity == "MAGIC" or item.rarity == "RARE" or (item.rareLikeUnique and item.rareLikeUnique.supportsCustomModifiers)) +end + +local function canAddCrucibleModifiers(item) + return item and (item:GetPrimarySlot() == "Weapon 1" or item.type == "Shield" or item.canHaveShieldCrucibleTree) +end + ---@class ItemsTab: UndoHandler, ControlHost, Control ---@field displayItem Item? local ItemsTabClass = newClass("ItemsTab", "UndoHandler", "ControlHost", "Control") @@ -108,7 +140,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 +160,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}, "Item Finder (Weighted Mod Search)...", function() self.tradeQuery:PriceItem() end) self.controls.priceDisplayItem.tooltipFunc = function(tooltip) @@ -141,7 +173,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 +234,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) @@ -232,7 +264,7 @@ function ItemsTabClass:ItemsTab(build) self.sockets[node.id] = socketControl addSlot(socketControl) end - self.controls.slotHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT",self.slotAnchor,"TOPLEFT"}, {0, -4, 0, 16}, "^7Equipped items:") + self.controls.slotHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT",self.slotAnchor,"TOPLEFT"}, {0, -4, 0, 14}, "^7Equipped items:") self.controls.weaponSwap1 = new("ButtonControl"):ButtonControl({"BOTTOMRIGHT",self.slotAnchor,"TOPRIGHT"}, {-20, -2, 18, 18}, "I", function() if self.activeItemSet.useSecondWeaponSet then self.activeItemSet.useSecondWeaponSet = false @@ -276,10 +308,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 420 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 +324,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 +351,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 @@ -344,15 +375,15 @@ holding Shift will put it in the second.]]) self.controls.addDisplayItem.label = function() return self.items[self.displayItem.id] and "Save" or "Add to build" end - self.controls.editDisplayItem = new("ButtonControl"):ButtonControl({"LEFT",self.controls.addDisplayItem,"RIGHT"}, {8, 0, 60, 20}, "Edit...", function() + self.controls.editDisplayItem = new("ButtonControl"):ButtonControl({"LEFT",self.controls.addDisplayItem,"RIGHT"}, {8, 0, 100, 20}, "Edit...", function() self:EditDisplayItemText() end) - self.controls.removeDisplayItem = new("ButtonControl"):ButtonControl({"LEFT",self.controls.editDisplayItem,"RIGHT"}, {8, 0, 60, 20}, "Cancel", function() + self.controls.removeDisplayItem = new("ButtonControl"):ButtonControl({"LEFT",self.controls.editDisplayItem,"RIGHT"}, {8, 0, 100, 20}, "Cancel", function() self:SetDisplayItem() end) self.controls.displayItemBuySimilar = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.removeDisplayItem, "RIGHT", true }, - { 8, 0, 100, 20 }, "Buy similar", function() + { 8, 0, 100, 20 }, "Buy Similar...", function() local itemSlot = self:GetComparisonSlotNameForItem(self.displayItem) buySimilar.openPopup(self.displayItem, itemSlot, self.build) end) @@ -466,52 +497,16 @@ holding Shift will put it in the second.]]) self.controls.displayItemSectionSockets = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionVariant,"BOTTOMLEFT"}, {0, 0, 0, function() return self.displayItem and self.displayItem.selectableSocketCount > 0 and 28 or 0 end}) - for i = 1, 6 do - local drop = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemSectionSockets,"TOPLEFT"}, {(i-1) * 64, 0, 36, 20}, socketDropList, function(index, value) - self.displayItem.sockets[i].color = value.color - self.displayItem:BuildAndParseRaw() - self:UpdateDisplayItemTooltip() - end) - drop.shown = function() - return self.displayItem.selectableSocketCount >= i and self.displayItem.sockets[i] and self.displayItem.sockets[i].color ~= "A" - end - self.controls["displayItemSocket"..i] = drop - if i < 6 then - local link = new("CheckBoxControl"):CheckBoxControl({"LEFT",drop,"RIGHT"}, {4, 0, 20}, nil, function(state) - if state and self.displayItem.sockets[i].group ~= self.displayItem.sockets[i+1].group then - for s = i + 1, #self.displayItem.sockets do - self.displayItem.sockets[s].group = self.displayItem.sockets[s].group - 1 - end - elseif not state and self.displayItem.sockets[i].group == self.displayItem.sockets[i+1].group then - for s = i + 1, #self.displayItem.sockets do - self.displayItem.sockets[s].group = self.displayItem.sockets[s].group + 1 - end - end - self.displayItem:BuildAndParseRaw() - self:UpdateDisplayItemTooltip() - end) - link.shown = function() - return self.displayItem.selectableSocketCount > i and self.displayItem.sockets[i+1] and self.displayItem.sockets[i+1].color ~= "A" - end - self.controls["displayItemLink"..i] = link - end + self.controls.displayItemSocketsLabel = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.displayItemSectionSockets,"TOPLEFT"}, {0, 2, 0, 16}, "^7Sockets:") + self.controls.displayItemSocketsLabel.shown = function() + return self.displayItem and self.displayItem.selectableSocketCount > 0 end - self.controls.displayItemAddSocket = new("ButtonControl"):ButtonControl({"TOPLEFT",self.controls.displayItemSectionSockets,"TOPLEFT"}, {function() return (#self.displayItem.sockets - self.displayItem.abyssalSocketCount) * 64 - 12 end, 0, 20, 20}, "+", function() - local insertIndex = #self.displayItem.sockets - self.displayItem.abyssalSocketCount + 1 - t_insert(self.displayItem.sockets, insertIndex, { - color = self.displayItem.defaultSocketColor, - group = self.displayItem.sockets[insertIndex - 1].group + 1 - }) - for s = insertIndex + 1, #self.displayItem.sockets do - self.displayItem.sockets[s].group = self.displayItem.sockets[s].group + 1 - end + socketControls.create(self.controls, self.controls.displayItemSocketsLabel, function() + return self.displayItem + end, function() self.displayItem:BuildAndParseRaw() - self:UpdateSocketControls() self:UpdateDisplayItemTooltip() end) - self.controls.displayItemAddSocket.shown = function() - return #self.displayItem.sockets < self.displayItem.selectableSocketCount + self.displayItem.abyssalSocketCount - end -- Section: Enchant / Anoint / Corrupt self.controls.displayItemSectionEnchant = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionSockets,"BOTTOMLEFT"}, {0, 0, 0, function() @@ -579,10 +574,8 @@ holding Shift will put it in the second.]]) end -- Section: Influence dropdowns - local influenceDisplayList = { "Influence" } - for i, curInfluenceInfo in ipairs(influenceInfo) do - influenceDisplayList[i + 1] = curInfluenceInfo.display - end + local influenceDisplayList1 = buildInfluenceDisplayList("Influence 1", influenceInfo) + local influenceDisplayList2 = buildInfluenceDisplayList("Influence 2", influenceInfo) local function setDisplayItemInfluence(influenceIndexList) self.displayItem:ResetInfluence() if self.displayItem.HasElderShaperAndAllConquerorInfluences then @@ -610,8 +603,10 @@ holding Shift will put it in the second.]]) self.controls.displayItemSectionInfluence = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionEnchant,"BOTTOMLEFT"}, {0, 0, 0, function() return self.displayItem and self.displayItem.canBeInfluenced and 28 or 0 end}) + -- Align the first influence dropdown with the right edge of the quality field below. + local influenceWidth = self.controls.displayItemSocketsLabel:GetSize() + 6 + 60 local influenceTipText = table.concat(main:WrapString("Selecting an influence here will also allow the modifier dropdowns to contain influenced mods.", 16, 140), "\n") - self.controls.displayItemInfluence = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemSectionInfluence,"TOPRIGHT"}, {0, 0, 100, 20}, influenceDisplayList, function(index, value) + self.controls.displayItemInfluence = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemSectionInfluence,"TOPRIGHT"}, {0, 0, influenceWidth, 20}, influenceDisplayList1, function(index, value) local otherIndex = self.controls.displayItemInfluence2.selIndex setDisplayItemInfluence({ index - 1, otherIndex - 1 }) end) @@ -619,7 +614,7 @@ holding Shift will put it in the second.]]) self.controls.displayItemInfluence.shown = function() return self.displayItem and self.displayItem.canBeInfluenced end - self.controls.displayItemInfluence2 = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemInfluence,"TOPRIGHT",true}, {8, 0, 100, 20}, influenceDisplayList, function(index, value) + self.controls.displayItemInfluence2 = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemInfluence,"TOPRIGHT",true}, {8, 0, influenceWidth, 20}, influenceDisplayList2, function(index, value) local otherIndex = self.controls.displayItemInfluence.selIndex setDisplayItemInfluence({ index - 1, otherIndex - 1 }) end) @@ -630,21 +625,27 @@ holding Shift will put it in the second.]]) -- Section: Item Quality self.controls.displayItemSectionQuality = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionInfluence,"BOTTOMLEFT"}, {0, 0, 0, function() - return (self.controls.displayItemQuality:IsShown() and self.controls.displayItemQualityEdit:IsShown()) and 28 or 0 + return (self.controls.displayItemQualityEdit:IsShown() or self.controls.displayItemCatalyst:IsShown()) and 28 or 0 end}) - self.controls.displayItemQuality = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.displayItemSectionQuality, "TOPRIGHT" }, { 0, 0, 0, 16 }, "^7Quality:") + self.controls.displayItemQuality = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.displayItemSectionQuality, "TOPRIGHT" }, { 0, 2, 0, 16 }, "^7Quality:") + self.controls.displayItemQuality.width = self.controls.displayItemSocketsLabel:GetSize() self.controls.displayItemQuality.shown = function() - return self.displayItem and self.displayItem.quality and (self.displayItem.base.type ~= "Amulet" or self.displayItem.base.type ~= "Belt" or self.displayItem.base.type ~= "Jewel" or self.displayItem.base.type ~= "Quiver" or self.displayItem.base.type ~= "Ring" or self.displayItem.type ~= "Graft") + return self.displayItem and (self.displayItem.quality ~= nil or isCatalystEligible(self.displayItem)) end - self.controls.displayItemQualityEdit = new("EditControl"):EditControl({"LEFT",self.controls.displayItemQuality,"RIGHT"}, {2, 0, 60, 20}, nil, nil, "%D", 2, function(buf) - self.displayItem.quality = tonumber(buf) + self.controls.displayItemQualityEdit = new("EditControl"):EditControl({"LEFT",self.controls.displayItemQuality,"RIGHT"}, {6, 0, 60, 20}, nil, nil, "%D", 2, function(buf) + setEditableItemQuality(self.displayItem, tonumber(buf)) + if isCatalystEligible(self.displayItem) and self.displayItem.crafted then + for i = 1, self.displayItem.affixLimit do + -- Force affix selectors to update + local drop = self.controls["displayItemAffix"..i] + drop.selFunc(drop.selIndex, drop.list[drop.selIndex]) + end + end self.displayItem:BuildAndParseRaw() self:UpdateDisplayItemTooltip() end) - self.controls.displayItemQualityEdit.shown = function() - return self.displayItem and self.displayItem.quality and (self.displayItem.base.type ~= "Amulet" or self.displayItem.base.type ~= "Belt" or self.displayItem.base.type ~= "Jewel" or self.displayItem.base.type ~= "Quiver" or self.displayItem.base.type ~= "Ring" or self.displayItem.type ~= "Graft") - end + self.controls.displayItemQualityEdit.shown = self.controls.displayItemQuality.shown local sortingOptions = { { stat = nil, label = "Default" } @@ -654,19 +655,18 @@ holding Shift will put it in the second.]]) table.insert(sortingOptions, option) end end - -- Section: Catalysts - self.controls.displayItemSectionCatalyst = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionQuality,"BOTTOMLEFT"}, {0, 0, 0, function() - return (self.controls.displayItemCatalyst:IsShown() or self.controls.displayItemCatalystQualityEdit:IsShown()) and 28 or 0 - end}) - self.controls.displayItemCatalyst = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemSectionCatalyst,"TOPRIGHT"}, {0, 0, 250, 20}, + self.controls.displayItemCatalyst = new("DropDownControl"):DropDownControl({"LEFT",self.controls.displayItemQualityEdit,"RIGHT",true}, {8, 0, 250, 20}, {"Catalyst","Abrasive (Attack)","Accelerating (Speed)","Dextral (Suffix)","Fertile (Life & Mana)","Imbued (Caster)","Intrinsic (Attribute)","Noxious (Physical & Chaos Damage)", "Prismatic (Resistance)","Sinistral (Prefix)","Tempering (Defense)","Turbulent (Elemental)","Unstable (Critical)"}, function(index, value) + local quality = tonumber(self.controls.displayItemQualityEdit.buf) or 20 self.displayItem.catalyst = index - 1 - if not self.displayItem.catalystQuality then - self.displayItem.catalystQuality = 20 - self.controls.displayItemCatalystQualityEdit:SetText(self.displayItem.catalystQuality) + if index > 1 then + self.displayItem.catalystQuality = self.displayItem.catalystQuality or quality + else + self.displayItem.catalystQuality = nil end + self.controls.displayItemQualityEdit:SetText(getEditableItemQuality(self.displayItem)) if self.displayItem.crafted then for i = 1, self.displayItem.affixLimit do -- Force affix selectors to update @@ -678,26 +678,11 @@ holding Shift will put it in the second.]]) self:UpdateDisplayItemTooltip() end) self.controls.displayItemCatalyst.shown = function() - return self.displayItem and (self.displayItem.crafted or self.displayItem.hasModTags) and (self.displayItem.base.type == "Amulet" or self.displayItem.base.type == "Ring" or self.displayItem.base.type == "Belt") - end - self.controls.displayItemCatalystQualityEdit = new("EditControl"):EditControl({"LEFT",self.controls.displayItemCatalyst,"RIGHT"}, {2, 0, 60, 20}, nil, nil, "%D", 2, function(buf) - self.displayItem.catalystQuality = tonumber(buf) - if self.displayItem.crafted then - for i = 1, self.displayItem.affixLimit do - -- Force affix selectors to update - local drop = self.controls["displayItemAffix"..i] - drop.selFunc(drop.selIndex, drop.list[drop.selIndex]) - end - end - self.displayItem:BuildAndParseRaw() - self:UpdateDisplayItemTooltip() - end) - self.controls.displayItemCatalystQualityEdit.shown = function() - return self.displayItem and (self.displayItem.crafted or self.displayItem.hasModTags) and self.displayItem.catalyst and self.displayItem.catalyst > 0 + return isCatalystEligible(self.displayItem) end -- Section: Cluster Jewel - self.controls.displayItemSectionClusterJewel = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionCatalyst,"BOTTOMLEFT"}, {0, 0, 0, function() + self.controls.displayItemSectionClusterJewel = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionQuality,"BOTTOMLEFT"}, {0, 0, 0, function() return self.controls.displayItemClusterJewelSkill:IsShown() and 52 or 0 end}) self.controls.displayItemClusterJewelSkill = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemSectionClusterJewel,"TOPLEFT"}, {0, 0, 300, 20}, { }, function(index, value) @@ -717,13 +702,16 @@ holding Shift will put it in the second.]]) self:CraftClusterJewel() end) - self.controls.craftingSortingLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.displayItemSectionClusterJewel, "BOTTOMLEFT" }, { 0, 0, 0, 16 }, "^7Modifier sorting:") + local affixX = 40 + local affixRangeWidth = 300 + self.controls.craftingSortingLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.displayItemSectionClusterJewel, "BOTTOMLEFT" }, { 0, 2, 0, 16 }, "^7Sort by:") + self.controls.craftingSortingLabel.width = self.controls.displayItemSocketsLabel:GetSize() self.controls.craftingSortingLabel.shown = function() return self.displayItem and self.displayItem.crafted and -- cluster jewels don't have good comparison support and sorting would be misleading not (self.displayItem.base.type == "Jewel" and self.displayItem.base.subType == "Cluster") end - self.controls.craftingSorting = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.craftingSortingLabel, "RIGHT" }, { 4, 0, 200, 20 }, sortingOptions, function() + self.controls.craftingSorting = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.craftingSortingLabel, "RIGHT" }, { 6, 0, affixX + affixRangeWidth - self.controls.craftingSortingLabel:GetSize() - 6, 20 }, sortingOptions, function() self:UpdateAffixControls() end) @@ -742,7 +730,7 @@ holding Shift will put it in the second.]]) local h = 6 for i = 1, maxModCount do if self.controls["displayItemAffix"..i]:IsShown() then - h = h + 24 + h = h + 25 if self.controls["displayItemAffixRange"..i]:IsShown() then h = h + 18 end @@ -796,7 +784,7 @@ holding Shift will put it in the second.]]) end return range end - drop = new("DropDownControl"):DropDownControl({"TOPLEFT",prev,"TOPLEFT"}, {i==1 and 40 or 0, 0, 418, 20}, nil, function(index, value) + drop = new("DropDownControl"):DropDownControl({"TOPLEFT",prev,"TOPLEFT"}, {i==1 and affixX or 0, 0, 418, 20}, nil, function(index, value) local affix = { modId = "None", fractured = self.displayItem[drop.outputTable][drop.outputIndex].fractured } if value.modId then affix.modId = value.modId @@ -813,7 +801,7 @@ holding Shift will put it in the second.]]) self:UpdateAffixControls() end) drop.y = function() - return i == 1 and 0 or 24 + (prev.slider:IsShown() and 18 or 0) + return i == 1 and 0 or 25 + (prev.slider:IsShown() and 18 or 0) end drop.tooltipFunc = function(tooltip, mode, index, value) local modList = value.modList @@ -931,7 +919,7 @@ holding Shift will put it in the second.]]) drop.shown = function() return self.displayItem and self.displayItem.crafted and i <= self.displayItem.affixLimit end - slider = new("SliderControl"):SliderControl({"TOPLEFT",drop,"BOTTOMLEFT"}, {0, 2, 300, 16}, function(val) + slider = new("SliderControl"):SliderControl({"TOPLEFT",drop,"BOTTOMLEFT"}, {0, 2, affixRangeWidth, 16}, function(val) local affix = self.displayItem[drop.outputTable][drop.outputIndex] local index, range = slider:GetDivVal() affix.modId = drop.list[drop.selIndex].modList[index] @@ -941,7 +929,7 @@ holding Shift will put it in the second.]]) self:UpdateDisplayItemTooltip() end) slider.width = function() - return slider.divCount and 300 or 100 + return slider.divCount and affixRangeWidth or 100 end slider.tooltipFunc = function(tooltip, val) local modList = drop.list[drop.selIndex].modList @@ -984,9 +972,8 @@ holding Shift will put it in the second.]]) end -- Section: Custom modifiers - -- if either Custom or Crucible mod buttons are shown, create the control for the list of mods self.controls.displayItemSectionCustom = new("Control"):Control({"TOPLEFT",self.controls.displayItemSectionAffix,"BOTTOMLEFT",true}, {0, 0, 0, function() - return (self.controls.displayItemAddCustom:IsShown() or self.controls.displayItemAddCrucible:IsShown()) and 28 + self.displayItem.customCount * 22 or 0 + return self.controls.displayItemAddCustom:IsShown() and 28 + self.displayItem.customCount * 22 or 0 end}) self.controls.displayItemSectionCustom.shown = function() return self.displayItem ~= nil @@ -995,18 +982,7 @@ holding Shift will put it in the second.]]) self:AddCustomModifierToDisplayItem() end) self.controls.displayItemAddCustom.shown = function() - return self.displayItem and (self.displayItem.rarity == "MAGIC" or self.displayItem.rarity == "RARE" or (self.displayItem.rareLikeUnique and self.displayItem.rareLikeUnique.supportsCustomModifiers)) - end - - -- Section: Crucible modifiers - -- if the Add modifier button is not shown, take its place, otherwise move it to the right of it - self.controls.displayItemAddCrucible = new("ButtonControl"):ButtonControl({"TOPLEFT",self.controls.displayItemSectionCustom,"TOPLEFT"}, {function() - return (self.controls.displayItemAddCustom:IsShown() and 128) or 0 - end, 0, 150, 20}, "Add Crucible mod...", function() - self:AddCrucibleModifierToDisplayItem() - end) - self.controls.displayItemAddCrucible.shown = function() - return self.displayItem and (self.displayItem:GetPrimarySlot() == "Weapon 1" or self.displayItem.type == "Shield" or self.displayItem.canHaveShieldCrucibleTree) + return canAddCustomModifiers(self.displayItem) or canAddCrucibleModifiers(self.displayItem) end -- Section: Modifier Range @@ -1445,7 +1421,11 @@ function ItemsTabClass:Draw(viewPort, inputEvents) end self.controls.scrollBarV:SetContentDimension(contentHeight, viewPort.height - (h and 20 or 0)) self.controls.scrollBarH:SetContentDimension(contentWidth, viewPort.width - (v and 20 or 0)) - if self.snapHScroll == "RIGHT" then + if self.snapHScroll == "ITEM" then + local x, y = self.anchorDisplayItem:GetPos() + self.controls.scrollBarH:SetOffset(x - self.x - 16) + self.controls.scrollBarV:SetOffset(y - self.y - 8) + elseif self.snapHScroll == "RIGHT" then self.controls.scrollBarH:SetOffset(self.controls.scrollBarH.offsetMax) elseif self.snapHScroll == "LEFT" then self.controls.scrollBarH:SetOffset(0) @@ -1551,12 +1531,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 +1600,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 +1608,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 +1626,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 +1788,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 +1798,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 @@ -2032,7 +1984,12 @@ end -- Sets the display item to the given item function ItemsTabClass:SetDisplayItem(item) + self.controls.displayItemSetColors:SetSel(1, true) + self.controls.displayItemSetLinks:SetSel(1, true) self.displayItem = item + if item and not (item.catalyst and item.catalyst > 0) then + item.catalystQuality = nil + end if item then -- Update the display item controls self:UpdateDisplayItemTooltip() @@ -2078,12 +2035,9 @@ function ItemsTabClass:SetDisplayItem(item) -- Set both influence dropdowns local influence1 = 1 local influence2 = 1 - local influenceDisplayList = { "Influence" } - for i, curInfluenceInfo in ipairs((item.canHaveEldritchInfluence or item.type == "Helmet" or item.type == "Body Armour" or item.type == "Gloves" or item.type == "Boots") and itemLib.influenceInfo.all or itemLib.influenceInfo.default) do - influenceDisplayList[i + 1] = curInfluenceInfo.display - end - self.controls.displayItemInfluence.list = influenceDisplayList - self.controls.displayItemInfluence2.list = influenceDisplayList + local availableInfluences = (item.canHaveEldritchInfluence or item.type == "Helmet" or item.type == "Body Armour" or item.type == "Gloves" or item.type == "Boots") and itemLib.influenceInfo.all or itemLib.influenceInfo.default + self.controls.displayItemInfluence.list = buildInfluenceDisplayList("Influence 1", availableInfluences) + self.controls.displayItemInfluence2.list = buildInfluenceDisplayList("Influence 2", availableInfluences) for i, curInfluenceInfo in ipairs(influenceInfo) do if item[curInfluenceInfo.key] then if influence1 == 1 then @@ -2097,13 +2051,8 @@ function ItemsTabClass:SetDisplayItem(item) -- Initialising these controls must not re-craft the parsed item. self.controls.displayItemInfluence:SetSel(influence1, true) self.controls.displayItemInfluence2:SetSel(influence2, true) - self.controls.displayItemQualityEdit:SetText(item.quality) self.controls.displayItemCatalyst:SetSel((item.catalyst or 0) + 1, true) - if item.catalystQuality then - self.controls.displayItemCatalystQualityEdit:SetText(m_max(item.catalystQuality, 0)) - else - self.controls.displayItemCatalystQualityEdit:SetText(0) - end + self.controls.displayItemQualityEdit:SetText(getEditableItemQuality(self.displayItem)) self:UpdateCustomControls() self:UpdateDisplayItemRangeLines() if item.clusterJewel and item.crafted then @@ -2136,13 +2085,7 @@ function ItemsTabClass:ToggleDisplayItemModLine(modLine) end function ItemsTabClass:UpdateSocketControls() - local sockets = self.displayItem.sockets - for i = 1, #sockets - self.displayItem.abyssalSocketCount do - self.controls["displayItemSocket"..i]:SelByValue(sockets[i].color, "color") - if i > 1 then - self.controls["displayItemLink"..(i-1)].state = sockets[i].group == sockets[i-1].group - end - end + socketControls.update(self.controls, self.displayItem) end function ItemsTabClass:UpdateClusterJewelControls() @@ -2941,19 +2884,19 @@ function ItemsTabClass:EnchantDisplayItem(enchantSlot) end end controls.enchantmentSourceLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {95, 45, 0, 16}, "^7Source:") - controls.enchantmentSource = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 180, 18}, enchantmentSourceList, function(index, value) + controls.enchantmentSource = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 216, 18}, enchantmentSourceList, function(index, value) buildEnchantmentList() controls.enchantment:SetSel(m_min(controls.enchantment.selIndex, #enchantmentList)) if controls.sort then applySort(controls.sort.list[controls.sort.selIndex].stat, true) end end) - controls.sortLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {350, 45, 0, 16}, "^7Sort by:") - controls.sort = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {355, 45, 240, 18}, sortList, function(index, value) + controls.sortLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {438.5, 45, 0, 16}, "^7Sort by:") + controls.sort = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {443.5, 45, 256.5, 18}, sortList, function(index, value) applySort(value.stat, true) end) controls.enchantmentLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {95, 70, 0, 16}, "^7Enchantment:") - controls.enchantment = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {100, 70, 495, 18}, enchantmentList) + controls.enchantment = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {100, 70, 600, 18}, enchantmentList) controls.enchantment.tooltipFunc = function(tooltip, mode, index) tooltip:Clear() self:AddItemTooltip(tooltip, enchantItem(index), nil, true) @@ -2969,7 +2912,7 @@ function ItemsTabClass:EnchantDisplayItem(enchantSlot) controls.close = new("ButtonControl"):ButtonControl(nil, {88, 100, 80, 20}, "Cancel", function() main:ClosePopup() end) - main:OpenPopup(605, 130, "Enchant Item", controls) + main:OpenPopup(710, 130, "Enchant Item", controls) end ---Gets the name of the anointed node on an item @@ -3122,7 +3065,30 @@ function ItemsTabClass:CorruptDisplayItem() local controls = { } local implicitList = { } local shownExplicits = {} - local explicitOffset = 0 + local popupWidth = 605 + local padding = 12 + local gap = 8 + local controlHeight = 20 + local rowHeight = controlHeight + gap + local hasRollRanges = self.displayItem.rarity == "UNIQUE" or self.displayItem.rarity == "RELIC" + local contentY = 24 + (hasRollRanges and rowHeight or 0) + local fieldX = padding + DrawStringWidth(16, "VAR", "Implicit #5:") + gap + local tabWidth = m_max(80, DrawStringWidth(16, "VAR", "Volatile Vaal Orb") + padding * 2) + local function implicitHeight(implicitNum) + return contentY + rowHeight * (implicitNum + 1) - gap + padding * 2 + controlHeight + end + local function layoutRollRanges() + local y = contentY + for _, i in ipairs(shownExplicits) do + local label = controls["rollRangeLabel" .. i] + label.y = y + 2 + controls["rollRangeValue" .. i].y = y + 2 + controls["rollRangeSlider" .. i].y = y + local _, lineBreaks = label.label:gsub("\n", "") + y = y + m_max(controlHeight, (lineBreaks + 1) * 16 + 4) + gap + end + return y - (#shownExplicits > 0 and gap or 0) + padding * 2 + controlHeight + end local corruptedRanges = {} local sourceList = { "Corrupted", "Scourge" } local sortList, sortStats = buildModSortList() @@ -3341,40 +3307,39 @@ function ItemsTabClass:CorruptDisplayItem() item:BuildAndParseRaw() return item end - if self.displayItem.rarity == "UNIQUE" or self.displayItem.rarity == "RELIC" then + if hasRollRanges then local item = new("Item"):Item(self.displayItem:BuildRaw()) - local offset = 20 + local sliderX = padding + DrawStringWidth(16, "VAR", "1.22") + gap + local labelX = sliderX + 80 + gap for i, mod in ipairs(item.explicitModLines) do local modRange = mod.range or main.defaultItemAffixQuality if itemLib.isModLineScalable(mod.line, modRange, mod.valueScalar) and item:CheckModLineVariant(mod) then local function formatLabel(corruptedRange) local line = itemLib.applyRange(mod.line, modRange, mod.valueScalar or 1, corruptedRange) - local lines = main:WrapString("^7" .. line, 16, 430) - return table.concat(lines, "\n"), #lines + local lines = main:WrapString("^7" .. line, 16, popupWidth - labelX - padding) + return table.concat(lines, "\n") end - controls["rollRangeValue" .. i] = new("LabelControl"):LabelControl({ "TOPLEFT", nil, "TOPLEFT" }, - { 10, 10 + offset, 200, 16 }, "^71.00") - controls["rollRangeSlider" .. i] = new("SliderControl"):SliderControl({ "LEFT", controls["rollRangeValue" .. i], "RIGHT" }, { 5, 0, 80, 18 }, function(val) + controls["rollRangeValue" .. i] = new("LabelControl"):LabelControl({ "TOPRIGHT", nil, "TOPLEFT" }, + { sliderX - gap, contentY + 2, 0, 16 }, "^71.00") + controls["rollRangeSlider" .. i] = new("SliderControl"):SliderControl({ "TOPLEFT", nil, "TOPLEFT" }, { sliderX, contentY, 80, controlHeight }, function(val) corruptedRanges[i] = 0.78 + round(0.44 * val, 2) -- 0.78-1.22 controls["rollRangeValue" .. i].label = "^7" .. string.format("%.2f", corruptedRanges[i]) controls["rollRangeLabel" .. i].label = formatLabel(corruptedRanges[i]) + main.popups[1].height = layoutRollRanges() end) corruptedRanges[i] = mod.corruptedRange or 1 controls["rollRangeSlider" .. i].val = ((corruptedRanges[i]) - 0.78) / 0.44 controls["rollRangeValue" .. i].label = "^7" .. string.format("%.2f", corruptedRanges[i]) - local label, lineCount = formatLabel(corruptedRanges[i]) - offset = offset + 16 * (lineCount - 1) - controls["rollRangeLabel" .. i] = new("LabelControl"):LabelControl({ "LEFT", controls["rollRangeSlider" .. i], "RIGHT" }, - { 5, 0, 200, 16 }, label) + local label = formatLabel(corruptedRanges[i]) + controls["rollRangeLabel" .. i] = new("LabelControl"):LabelControl({ "TOPLEFT", nil, "TOPLEFT" }, + { labelX, contentY + 2, 0, 16 }, label) -- hide them by default as they are a secondary window controls["rollRangeLabel" .. i].shown = false controls["rollRangeSlider" .. i].shown = false controls["rollRangeValue" .. i].shown = false - offset = offset + 20 t_insert(shownExplicits, i) end end - explicitOffset = offset end local function setImplicitControlsShown(implicitNum, canChangeImplicits) for i = 1, maxImplicitNum do @@ -3384,7 +3349,7 @@ function ItemsTabClass:CorruptDisplayItem() end controls.implicitCannotBeChangedLabel.shown = implicitNum > 0 and not canChangeImplicits end - controls.implicits = new("ButtonControl"):ButtonControl({ "TOPLEFT", nil, "TOPLEFT" }, { 5, 5, 80, 20 }, "Implicits", + controls.implicits = new("ButtonControl"):ButtonControl({ "TOPLEFT", nil, "TOPLEFT" }, { padding, 24, tabWidth, controlHeight }, "Implicits", function() local implicitNum = currentModType ~= "ScourgeUpside" and itemMaxCorruptImplicits or 4 local canChangeImplicits = currentModType ~= "Corrupted" or not self.displayItem.implicitsCannotBeChanged @@ -3398,13 +3363,14 @@ function ItemsTabClass:CorruptDisplayItem() controls.sourceLabel.shown = true controls.sort.shown = true controls.sortLabel.shown = true - main.popups[1].height = 103 + 20 * implicitNum + main.popups[1].height = implicitHeight(implicitNum) end) - controls.implicits.shown = function() - return self.displayItem.rarity == "UNIQUE" or self.displayItem.rarity == "RELIC" + controls.implicits.shown = hasRollRanges + controls.implicits.locked = function() + return controls.source:IsShown() end - controls.rolls = new("ButtonControl"):ButtonControl({ "LEFT", controls.implicits, "RIGHT" }, { 5, 0, 80, 20 }, - "Roll Ranges", + controls.rolls = new("ButtonControl"):ButtonControl({ "LEFT", controls.implicits, "RIGHT" }, { gap, 0, tabWidth, controlHeight }, + "Volatile Vaal Orb", function() setImplicitControlsShown(0, false) for _, i in ipairs(shownExplicits) do @@ -3416,14 +3382,13 @@ function ItemsTabClass:CorruptDisplayItem() controls.sourceLabel.shown = false controls.sort.shown = false controls.sortLabel.shown = false - main.popups[1].height = 55 + explicitOffset + main.popups[1].height = layoutRollRanges() end) - controls.rolls.shown = function() - return self.displayItem.rarity == "UNIQUE" or self.displayItem.rarity == "RELIC" + controls.rolls.shown = hasRollRanges + controls.rolls.locked = function() + return not controls.source:IsShown() end - controls.sourceLabel = new("LabelControl"):LabelControl({ "TOPRIGHT", nil, "TOPLEFT" }, { 95, 30, 0, 16 }, - "^7Source:") - controls.source = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { 100, 30, 150, 18 }, + controls.source = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { fieldX, contentY, 150, controlHeight }, sourceList, function(index, value) if value == "Scourge" then currentModType = "ScourgeUpside" @@ -3431,14 +3396,14 @@ function ItemsTabClass:CorruptDisplayItem() buildImplicitList("ScourgeDownside") local implicitNum = (self.displayItem.rarity == "UNIQUE" or self.displayItem.rarity == "RELIC") and 4 or 3 setImplicitControlsShown(implicitNum, true) - main.popups[1].height = 103 + 20 * implicitNum + main.popups[1].height = implicitHeight(implicitNum) buildScourgeList(controls.implicit3, controls.implicit4, "ScourgeDownside") buildScourgeList(controls.implicit4, controls.implicit3, "ScourgeDownside") else buildImplicitList(value) currentModType = value setImplicitControlsShown(itemMaxCorruptImplicits, not self.displayItem.implicitsCannotBeChanged) - main.popups[1].height = 103 + 20 * itemMaxCorruptImplicits + main.popups[1].height = implicitHeight(itemMaxCorruptImplicits) end if controls.sort then applySort(controls.sort.list[controls.sort.selIndex].stat) @@ -3449,20 +3414,22 @@ function ItemsTabClass:CorruptDisplayItem() controls[string.format("implicit%d", i)]:SetSel(1) end end) + controls.source.fontSize = 14 + controls.sourceLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.source, "LEFT" }, { -gap, 0, 0, 16 }, "^7Source:") controls.source.enabled = #sourceList > 1 - controls.sortLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {350, 20, 0, 16}, "^7Sort by:") - controls.sort = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {355, 20, 240, 18}, sortList, function(index, value) + controls.sort = new("DropDownControl"):DropDownControl({ "TOPRIGHT", nil, "TOPRIGHT" }, { -padding, contentY, 240, controlHeight }, sortList, function(index, value) applySort(value.stat) end) - local implicitRowSize = 20 - local implicitYPos = 35 - controls.implicitCannotBeChangedLabel = new("LabelControl"):LabelControl({ "TOPLEFT", nil, "TOPLEFT" }, { 20, implicitYPos + implicitRowSize, 0, 20 }, "^7This Items Implicits Cannot Be Changed") + controls.sort.fontSize = 14 + controls.sortLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.sort, "LEFT" }, { -gap, 0, 0, 16 }, "^7Sort by:") + controls.implicitCannotBeChangedLabel = new("LabelControl"):LabelControl({ "TOPLEFT", nil, "TOPLEFT" }, { fieldX, contentY + rowHeight + 2, 0, 16 }, "^7This Items Implicits Cannot Be Changed") controls.implicitCannotBeChangedLabel.shown = self.displayItem.implicitsCannotBeChanged for i = 1, maxImplicitNum do local controlName = "implicit" .. i - controls[controlName .. "Label"] = new("LabelControl"):LabelControl({ "TOPRIGHT", nil, "TOPLEFT" }, { 75, implicitYPos + i * implicitRowSize, 0, 16 }, + controls[controlName] = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { fieldX, contentY + i * rowHeight, popupWidth - fieldX - padding, controlHeight }, nil) + controls[controlName].fontSize = 14 + controls[controlName .. "Label"] = new("LabelControl"):LabelControl({ "RIGHT", controls[controlName], "LEFT" }, { -gap, 0, 0, 16 }, string.format("^7Implicit #%d:", i)) - controls[controlName] = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { 80, implicitYPos + i * implicitRowSize, 440, 18 }, nil) controls[controlName].tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if mode ~= "OUT" and value and value.mod then @@ -3496,7 +3463,7 @@ function ItemsTabClass:CorruptDisplayItem() controls["implicit" .. i].selFunc() end end - controls.save = new("ButtonControl"):ButtonControl({ "BOTTOM", nil, "BOTTOM" }, { -45, -4, 80, 20 }, "Corrupt", function() + controls.save = new("ButtonControl"):ButtonControl({ "BOTTOM", nil, "BOTTOM" }, { -(80 + gap) / 2, -padding, 80, controlHeight }, "Corrupt", function() self:SetDisplayItem(corruptItem(controls.implicit1.shown)) main:ClosePopup() end) @@ -3504,10 +3471,10 @@ function ItemsTabClass:CorruptDisplayItem() tooltip:Clear() self:AddItemTooltip(tooltip, corruptItem(controls.implicit1.shown), nil, false) end - controls.close = new("ButtonControl"):ButtonControl({ "BOTTOM", nil, "BOTTOM" }, { 45, -4, 80, 20 }, "Cancel", function() + controls.close = new("ButtonControl"):ButtonControl({ "BOTTOM", nil, "BOTTOM" }, { (80 + gap) / 2, -padding, 80, controlHeight }, "Cancel", function() main:ClosePopup() end) - main:OpenPopup(605, 103 + 20 * itemMaxCorruptImplicits, "Corrupt Item", controls) + main:OpenPopup(popupWidth, implicitHeight(itemMaxCorruptImplicits), "Corrupt Item", controls) end local delveDropOnlyCategories = require("Data.DelveDropOnly") @@ -3747,7 +3714,7 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() end setDefaultSortOrder() end - if self.displayItem.type ~= "Tincture" and self.displayItem.type ~= "Graft" then + if canAddCustomModifiers(self.displayItem) and self.displayItem.type ~= "Tincture" and self.displayItem.type ~= "Graft" then if self.displayItem.type ~= "Jewel" then t_insert(sourceList, { label = "Crafting Bench", sourceId = "MASTER" }) end @@ -3794,8 +3761,15 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() i = i + 1 end end - t_insert(sourceList, { label = "Custom", sourceId = "CUSTOM" }) + local canAddCrucible = canAddCrucibleModifiers(self.displayItem) + if canAddCrucible then + t_insert(sourceList, { label = "Crucible (Legacy)", sourceId = "CRUCIBLE" }) + end + if canAddCustomModifiers(self.displayItem) then + t_insert(sourceList, { label = "Custom", sourceId = "CUSTOM" }) + end buildMods(sourceList[1].sourceId) + local applyCrucibleModifiers local function addModifier() local item = new("Item"):Item(self.displayItem:BuildRaw()) item.id = self.displayItem.id @@ -3804,6 +3778,8 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() if controls.custom.buf:match("%S") then t_insert(item.explicitModLines, { line = controls.custom.buf, custom = true }) end + elseif sourceId == "CRUCIBLE" then + applyCrucibleModifiers(item) else local listMod = modList[controls.modSelect.selIndex] if listMod then @@ -3815,29 +3791,41 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() item:BuildAndParseRaw() return item end - controls.sourceLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {95, 20, 0, 16}, "^7Source:") - controls.source = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {100, 20, 150, 18}, sourceList, function(index, value) - buildMods(value.sourceId) - controls.modSelect:SetSel(1) - if controls.sort then + local popupWidth = 710 + local padding = 12 + local gap = 8 + local controlHeight = 20 + local rowHeight = controlHeight + gap + local contentY = 24 + local fieldX = padding + DrawStringWidth(16, "VAR", "Modifier:") + gap + local fieldWidth = popupWidth - fieldX - padding + local popupHeight = contentY + rowHeight + controlHeight * 2 + padding * 2 + controls.source = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {fieldX, contentY, 150, controlHeight}, sourceList, function(index, value) + if value.sourceId ~= "CRUCIBLE" then + buildMods(value.sourceId) + controls.modSelect:SetSel(1) applySort(controls.sort.list[controls.sort.selIndex].stat, true) end + main.popups[1].height = popupHeight + (value.sourceId == "CRUCIBLE" and rowHeight * 4 or 0) end) + controls.source.fontSize = 14 + controls.sourceLabel = new("LabelControl"):LabelControl({"RIGHT",controls.source,"LEFT"}, {-gap, 0, 0, 16}, "^7Source:") controls.source.enabled = #sourceList > 1 - controls.sortLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {350, 20, 0, 16}, "^7Sort by:") - controls.sortLabel.shown = function() - return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" - end - controls.sort = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {355, 20, 240, 18}, sortList, function(index, value) + controls.sort = new("DropDownControl"):DropDownControl({"TOPRIGHT",nil,"TOPRIGHT"}, {-padding, contentY, 240, controlHeight}, sortList, function(index, value) applySort(value.stat, true) end) + controls.sort.fontSize = 14 + controls.sortLabel = new("LabelControl"):LabelControl({"RIGHT",controls.sort,"LEFT"}, {-gap, 0, 0, 16}, "^7Sort by:") controls.sort.shown = function() - return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" + local sourceId = sourceList[controls.source.selIndex].sourceId + return sourceId ~= "CUSTOM" and sourceId ~= "CRUCIBLE" end - controls.modSelectLabel = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {95, 45, 0, 16}, "^7Modifier:") - controls.modSelect = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 600, 18}, modList) + controls.modSelect = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {fieldX, contentY + rowHeight, fieldWidth, controlHeight}, modList) + controls.modSelect.fontSize = 14 + controls.modSelectLabel = new("LabelControl"):LabelControl({"RIGHT",controls.modSelect,"LEFT"}, {-gap, 0, 0, 16}, "^7Modifier:") controls.modSelect.shown = function() - return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" + local sourceId = sourceList[controls.source.selIndex].sourceId + return sourceId ~= "CUSTOM" and sourceId ~= "CRUCIBLE" end controls.modSelect.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() @@ -3848,11 +3836,15 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() self:AddModComparisonTooltip(tooltip, value.mod) end end - controls.custom = new("EditControl"):EditControl({"TOPLEFT",nil,"TOPLEFT"}, {100, 45, 440, 18}) + controls.custom = new("EditControl"):EditControl({"TOPLEFT",nil,"TOPLEFT"}, {fieldX, contentY + rowHeight, fieldWidth, controlHeight}) + controls.customLabel = new("LabelControl"):LabelControl({"RIGHT",controls.custom,"LEFT"}, {-gap, 0, 0, 16}, "^7Modifier:") controls.custom.shown = function() return sourceList[controls.source.selIndex].sourceId == "CUSTOM" end - controls.save = new("ButtonControl"):ButtonControl(nil, {-45, 75, 80, 20}, "Add", function() + if canAddCrucible then + applyCrucibleModifiers = self:CreateCrucibleModControls(controls, fieldWidth, rowHeight, gap) + end + controls.save = new("ButtonControl"):ButtonControl({"BOTTOMRIGHT",nil,"BOTTOM"}, {-gap / 2, -padding, 80, controlHeight}, "Add", function() self:SetDisplayItem(addModifier()) main:ClosePopup() end) @@ -3860,15 +3852,14 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() tooltip:Clear() self:AddItemTooltip(tooltip, addModifier()) end - controls.close = new("ButtonControl"):ButtonControl(nil, {45, 75, 80, 20}, "Cancel", function() + controls.close = new("ButtonControl"):ButtonControl({"BOTTOMLEFT",nil,"BOTTOM"}, {gap / 2, -padding, 80, controlHeight}, "Cancel", function() main:ClosePopup() end) - main:OpenPopup(710, 105, "Add Modifier to Item", controls, "save", sourceList[controls.source.selIndex].sourceId == "CUSTOM" and "custom") + main:OpenPopup(popupWidth, popupHeight + (sourceList[1].sourceId == "CRUCIBLE" and rowHeight * 4 or 0), "Add Modifier to Item", controls, "save", sourceList[controls.source.selIndex].sourceId == "CUSTOM" and "custom") end --- Opens the crucible modifier popup -function ItemsTabClass:AddCrucibleModifierToDisplayItem() - local controls = { } +-- Adds Crucible node selectors and returns the function that applies their selections. +function ItemsTabClass:CreateCrucibleModControls(controls, fieldWidth, rowHeight, gap) local modList = {[1] = {"None"}, [2] = {"None"}, [3] = {"None"}, [4] = {"None"}, [5] = {"None"}} local itemModMap, nodeSelections = { }, { } local function getLabelFromMod(mod) @@ -3915,9 +3906,7 @@ function ItemsTabClass:AddCrucibleModifierToDisplayItem() end) end end - local function addModifier() - local item = new("Item"):Item(self.displayItem:BuildRaw()) - item.id = self.displayItem.id + local function applyModifiers(item) item.crucibleModLines = { } local listMod = { modList[1][controls.modSelectNode1.selIndex], @@ -3933,18 +3922,21 @@ function ItemsTabClass:AddCrucibleModifierToDisplayItem() end end end - item:BuildAndParseRaw() - return item end -- set up name map to know what modLines the item has as we build the mods out for _, mod in ipairs(self.displayItem.crucibleModLines) do itemModMap[mod.line] = true end buildCrucibleMods() - local y = 45 + local function crucibleShown() + return controls.source:GetSelValue().sourceId == "CRUCIBLE" + end for i = 1,5 do - controls["modSelectNode"..i.."Label"] = new("LabelControl"):LabelControl({"TOPRIGHT",nil,"TOPLEFT"}, {95, y, 0, 16}, "^7Node "..i..":") - controls["modSelectNode"..i] = new("DropDownControl"):DropDownControl({"TOPLEFT",nil,"TOPLEFT"}, {100, y, 555, 18}, modList[i]) + local control = new("DropDownControl"):DropDownControl({"TOPLEFT",controls.source,"BOTTOMLEFT"}, {0, gap + (i - 1) * rowHeight, fieldWidth, controls.source.height}, modList[i]) + control.fontSize = 14 + control.shown = crucibleShown + controls["modSelectNode"..i] = control + controls["modSelectNode"..i.."Label"] = new("LabelControl"):LabelControl({"RIGHT",control,"LEFT"}, {-gap, 0, 0, 16}, "^7Node "..i..":") controls["modSelectNode"..i].tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if mode ~= "OUT" and value and value ~= "None" then @@ -3954,7 +3946,6 @@ function ItemsTabClass:AddCrucibleModifierToDisplayItem() self:AddModComparisonTooltip(tooltip, value.mod) end end - y = y + 22 end -- populate dropdowns with item mods for nodeId, defaultOrder in pairs(nodeSelections) do @@ -3964,18 +3955,7 @@ function ItemsTabClass:AddCrucibleModifierToDisplayItem() end end end - controls.save = new("ButtonControl"):ButtonControl(nil, {-45, 157, 80, 20}, "Add", function() - self:SetDisplayItem(addModifier()) - main:ClosePopup() - end) - controls.save.tooltipFunc = function(tooltip) - tooltip:Clear() - self:AddItemTooltip(tooltip, addModifier()) - end - controls.close = new("ButtonControl"):ButtonControl(nil, {45, 157, 80, 20}, "Cancel", function() - main:ClosePopup() - end) - main:OpenPopup(710, 185, "Add Crucible Modifier to Item", controls, "save") + return applyModifiers end diff --git a/src/Classes/ListControl.lua b/src/Classes/ListControl.lua index 55407583503..9b49a46f6bd 100644 --- a/src/Classes/ListControl.lua +++ b/src/Classes/ListControl.lua @@ -32,6 +32,7 @@ local m_floor = math.floor ---@class ListControl: Control, ControlHost ---@field list T[] +---@field rowTextInset? number Leading inset for row text and icons; defaults to zero. local ListClass = newClass("ListControl", "Control", "ControlHost") ---@param anchor Anchor? @@ -207,8 +208,10 @@ function ListClass:Draw(viewPort, noTooltip) end DrawImage(nil, x + 1, y + 1, width - 2, height - 2) self:DrawControls(viewPort, (noTooltip and not self.forceTooltip) and self) + local mouseOverControl = self:GetMouseOverControl() SetViewport(x + 2, y + 2, self.scroll and width - 20 or width, height - 4 - (self.scroll and self.scrollH and 16 or 0)) + local textOffsetX = self.rowTextInset or 0 local textOffsetY = self.showRowSeparators and 2 or 0 local textHeight = rowHeight - textOffsetY * 2 local ttIndex, ttValue, ttX, ttY, ttWidth @@ -230,12 +233,12 @@ function ListClass:Draw(viewPort, noTooltip) icon = self:GetRowIcon(colIndex, index, value) end local textWidth = DrawStringWidth(textHeight, colFont, text) - if textWidth > colWidth - 2 then - local clipIndex = DrawStringCursorIndex(textHeight, colFont, text, colWidth - clipWidth - 2, 0) + if textWidth > colWidth - textOffsetX - 2 then + local clipIndex = DrawStringCursorIndex(textHeight, colFont, text, colWidth - textOffsetX - clipWidth - 2, 0) text = text:sub(1, clipIndex - 1) .. "..." textWidth = DrawStringWidth(textHeight, colFont, text) end - if not scrollBarV.dragging and (not self.selDragActive or (self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource))) then + if not mouseOverControl and not scrollBarV.dragging and (not self.selDragActive or (self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource))) then if relX >= colOffset and relX < (self.scroll and width - 20 or width) and relY >= 0 and relY >= lineY and relY < height - 2 - (self.scroll and self.scrollH and 18 or 0) and relY < lineY + rowHeight then ttIndex = index ttValue = value @@ -284,10 +287,10 @@ function ListClass:Draw(viewPort, noTooltip) end -- TODO: handle icon size properly, for now assume they are 16x16 if icon == nil then - DrawString(colOffset, lineY + textOffsetY, "LEFT", textHeight, colFont, text) + DrawString(colOffset + textOffsetX, lineY + textOffsetY, "LEFT", textHeight, colFont, text) else - DrawImage(icon, colOffset, lineY, 16, 16) - DrawString(colOffset + 16 + 2, lineY + textOffsetY, "LEFT", textHeight, colFont, text) + DrawImage(icon, colOffset + textOffsetX, lineY, 16, 16) + DrawString(colOffset + textOffsetX + 16 + 2, lineY + textOffsetY, "LEFT", textHeight, colFont, text) end end if self.colLabels then diff --git a/src/Classes/SectionControl.lua b/src/Classes/SectionControl.lua index e0acb6fd213..a63e4bc00ac 100644 --- a/src/Classes/SectionControl.lua +++ b/src/Classes/SectionControl.lua @@ -25,9 +25,9 @@ function SectionClass:Draw() local label = self:GetProperty("label") local labelWidth = DrawStringWidth(14, "VAR", label) SetDrawColor(0.66, 0.66, 0.66) - DrawImage(nil, x + 6, y - 8, labelWidth + 6, 18) + DrawImage(nil, x + 8, y - 8, labelWidth + 12, 18) SetDrawColor(0, 0, 0) - DrawImage(nil, x + 7, y - 7, labelWidth + 4, 16) + DrawImage(nil, x + 9, y - 7, labelWidth + 10, 16) SetDrawColor(1, 1, 1) - DrawString(x + 9, y - 6, "LEFT", 14, "VAR", label) + DrawString(x + 14, y - 6, "LEFT", 14, "VAR", label) end \ No newline at end of file diff --git a/src/Classes/SharedItemListControl.lua b/src/Classes/SharedItemListControl.lua index cbbb32728f4..2590aeebe0e 100644 --- a/src/Classes/SharedItemListControl.lua +++ b/src/Classes/SharedItemListControl.lua @@ -18,9 +18,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() diff --git a/src/Classes/SkillListControl.lua b/src/Classes/SkillListControl.lua index f5b86e4084a..1d6683433c8 100644 --- a/src/Classes/SkillListControl.lua +++ b/src/Classes/SkillListControl.lua @@ -6,6 +6,8 @@ local ipairs = ipairs local t_insert = table.insert local t_remove = table.remove +local m_ceil = math.ceil +local m_floor = math.floor local slot_map = { ["Weapon 1"] = { icon = NewImageHandle(), path = "Assets/icon_weapon.png" }, ["Weapon 2"] = { icon = NewImageHandle(), path = "Assets/icon_weapon_2.png" }, @@ -36,13 +38,20 @@ function SkillListClass:SkillListControl(anchor, rect, skillsTab) self:ListControl(anchor, rect, 16, "VERTICAL", true, skillsTab.socketGroupList) self.skillsTab = skillsTab self.label = "^7Socket Groups:" - self.controls.delete = new("ButtonControl"):ButtonControl({"BOTTOMRIGHT",self,"TOPRIGHT"}, {0, -2, 60, 18}, "Delete", function() + self.labelPositionOffset[2] = -4 + local buttonGap = 4 + local buttonRowWidth = self:GetSize() - m_ceil(DrawStringWidth(16, "VAR", self.label)) - 6 - buttonGap * 2 + local buttonWidth = m_floor(buttonRowWidth / 3) + local buttonWidthRemainder = buttonRowWidth - buttonWidth * 3 + local newButtonWidth = buttonWidth + (buttonWidthRemainder >= 1 and 1 or 0) + local deleteAllButtonWidth = buttonWidth + (buttonWidthRemainder >= 2 and 1 or 0) + self.controls.delete = new("ButtonControl"):ButtonControl({"BOTTOMRIGHT",self,"TOPRIGHT"}, {0, -6, buttonWidth, 20}, "Delete", function() self:OnSelDelete(self.selIndex, self.selValue) end) self.controls.delete.enabled = function() return self.selValue ~= nil and self.selValue.source == nil end - self.controls.deleteAll = new("ButtonControl"):ButtonControl({"RIGHT",self.controls.delete,"LEFT"}, {-4, 0, 70, 18}, "Delete All", function() + self.controls.deleteAll = new("ButtonControl"):ButtonControl({"RIGHT",self.controls.delete,"LEFT"}, {-buttonGap, 0, deleteAllButtonWidth, 20}, "Delete All", function() main:OpenConfirmPopup("Delete All", "Are you sure you want to delete all socket groups in this build?", "Delete", function() wipeTable(self.list) skillsTab:RebuildImbuedSupportBySlot() @@ -56,7 +65,7 @@ function SkillListClass:SkillListControl(anchor, rect, skillsTab) self.controls.deleteAll.enabled = function() return #self.list > 0 end - self.controls.new = new("ButtonControl"):ButtonControl({"RIGHT",self.controls.deleteAll,"LEFT"}, {-4, 0, 60, 18}, "New", function() + self.controls.new = new("ButtonControl"):ButtonControl({"RIGHT",self.controls.deleteAll,"LEFT"}, {-buttonGap, 0, newButtonWidth, 20}, "New", function() local newGroup = { label = "", enabled = true, diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index f1326ddc980..79574c523fa 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -9,6 +9,40 @@ local t_insert = table.insert local t_remove = table.remove local m_min = math.min local m_max = math.max +local socketControls = require("Modules.ItemSocketControls") +local calcsHelpers = require("Classes.CompareCalcsHelpers") + +local NARROW_VIEWPORT_WIDTH = 756 +local NARROW_OPTION_OFFSET = -75 +local GEM_NAME_WIDTH = 300 +local NARROW_GEM_NAME_WIDTH = 210 + +local gemSocketColours = { "R", "G", "B" } + +local function gemInstanceHasCount(gemInstance) + if not gemInstance then + return false + end + local grantedEffectList = gemInstance.gemData and gemInstance.gemData.grantedEffectList or { gemInstance.grantedEffect } + for index, grantedEffect in ipairs(grantedEffectList) do + if not grantedEffect.support and not grantedEffect.unsupported and (not grantedEffect.hasGlobalEffect or gemInstance["enableGlobal"..index]) then + return true + end + end + return false +end + +local function socketLayoutsMatch(sockets, expected) + if #sockets ~= #expected then + return false + end + for i, socket in ipairs(sockets) do + if socket.color ~= expected[i].color or socket.group ~= expected[i].group then + return false + end + end + return true +end local groupSlotDropList = { { label = "None" }, @@ -75,7 +109,15 @@ local sortGemTypeList = { { label = "Effective Hit Pool", type = "TotalEHP" }, } +---@class SkillSocketEdit +---@field id integer +---@field previousId integer +---@field item Item +---@field before table[] +---@field after table[] + ---@class SkillsTab: UndoHandler, ControlHost, Control +---@field socketEdit SkillSocketEdit? local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Control") ---@param build Build @@ -96,7 +138,7 @@ function SkillsTabClass:SkillsTab(build) self.defaultGemQuality = main.defaultGemQuality -- Set selector - self.controls.setSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", self, "TOPLEFT" }, { 76, 8, 210, 20 }, nil, function(index, value) + self.controls.setSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", self, "TOPLEFT" }, { 80, 8, 227, 20 }, nil, function(index, value) self:SetActiveSkillSet(self.skillSetOrderList[index]) self:AddUndoState() end) @@ -104,13 +146,13 @@ function SkillsTabClass:SkillsTab(build) self.controls.setSelect.enabled = function() return #self.skillSetOrderList > 1 end - self.controls.setLabel = new("LabelControl"):LabelControl({ "RIGHT", self.controls.setSelect, "LEFT" }, { -2, 0, 0, 16 }, "^7Skill set:") - self.controls.setManage = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.setSelect, "RIGHT" }, { 4, 0, 90, 20 }, "Manage...", function() + self.controls.setLabel = new("LabelControl"):LabelControl({ "RIGHT", self.controls.setSelect, "LEFT" }, { -6, 0, 0, 16 }, "^7Skill set:") + self.controls.setManage = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.setSelect, "RIGHT" }, { 4, 0, 89, 20 }, "Manage...", function() self:OpenSkillSetManagePopup() end) -- Socket group list - self.controls.groupList = new("SkillListControl"):SkillListControl({ "TOPLEFT", self, "TOPLEFT" }, { 20, 54, 360, 300 }, self) + self.controls.groupList = new("SkillListControl"):SkillListControl({ "TOPLEFT", self, "TOPLEFT" }, { 20, 60, 380, 300 }, self) self.controls.groupTip = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.groupList, "BOTTOMLEFT" }, { 0, 8, 0, 14 }, [[ ^7Usage Tips: @@ -124,7 +166,7 @@ function SkillsTabClass:SkillsTab(build) -- Gem options local optionInputsX = 170 local optionInputsY = 45 - self.controls.optionSection = new("SectionControl"):SectionControl({ "TOPLEFT", self.controls.groupList, "BOTTOMLEFT" }, { 0, optionInputsY + 50, 360, 156 }, "Gem Options") + self.controls.optionSection = new("SectionControl"):SectionControl({ "TOPLEFT", self.controls.groupList, "BOTTOMLEFT" }, { 0, optionInputsY + 50, 380, 156 }, "Gem Options") self.controls.sortGemsByDPS = new("CheckBoxControl"):CheckBoxControl({ "TOPLEFT", self.controls.groupList, "BOTTOMLEFT" }, { optionInputsX, optionInputsY + 70, 20 }, "Sort gems by DPS:", function(state) self.sortGemsByDPS = state end, nil, true) @@ -162,14 +204,19 @@ function SkillsTabClass:SkillsTab(build) self.anchorGroupDetail.shown = function() return self.displayGroup ~= nil end - self.controls.groupLabel = new("EditControl"):EditControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, 0, 380, 20 }, nil, "Label", "%c", 50, function(buf) + self.controls.groupLabel = new("EditControl"):EditControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, 0, 380, 20 }, nil, nil, "%c", 50, function(buf) self.displayGroup.label = buf self:ProcessSocketGroup(self.displayGroup) self:AddUndoState() self.build.buildFlag = true end) - self.controls.groupSlotLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, 30, 0, 16 }, "^7Socketed in:") - self.controls.groupSlot = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 85, 28, 130, 20 }, groupSlotDropList, function(index, value) + self.controls.groupLabelLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, 2, 0, 16 }, "^7Label:") + self.controls.groupLabel:SetPlaceholder("") + self.controls.groupLabel.inactiveCol = "^7" + self.controls.groupLabel.disableCol = "^8" + self.controls.groupSlot = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { function() + return DrawStringWidth(16, "VAR", "Item Sockets:") + 6 + end, 28, 128, 20 }, groupSlotDropList, function(index, value) -- maintain imbued support to new slot if self.imbuedSupportBySlot[self.displayGroup.slot] and self.displayGroup.imbuedSupport then if value.slotName and not self.imbuedSupportBySlot[value.slotName] then @@ -186,6 +233,8 @@ function SkillsTabClass:SkillsTab(build) self:AddUndoState() self.build.buildFlag = true end) + self.controls.groupSlot.arrowSize = 6 + self.controls.groupSlotLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, 30, 0, 16 }, "^7Socketed in:") self.controls.groupSlot.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if mode == "OUT" or index == 1 then @@ -204,17 +253,22 @@ function SkillsTabClass:SkillsTab(build) self.controls.groupSlot.enabled = function() return self.displayGroup.source == nil end - self.controls.groupEnabled = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.groupSlot, "RIGHT" }, { 70, 0, 20 }, "Enabled:", function(state) + self.controls.groupEnabled = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.groupSlot, "RIGHT" }, { 88, 0, 20 }, "Enabled:", function(state) self.displayGroup.enabled = state self:AddUndoState() self.build.buildFlag = true end) - self.controls.includeInFullDPS = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.groupEnabled, "RIGHT" }, { 145, 0, 20 }, "Include in Full DPS:", function(state) + self.controls.includeInFullDPS = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.groupEnabled, "RIGHT" }, { 156, 0, 20 }, "Include in Full DPS:", function(state) self.displayGroup.includeInFullDPS = state self:AddUndoState() self.build.buildFlag = true end) + self.controls.groupLabel:SetAnchor("TOPLEFT", self.controls.groupSlot, "TOPLEFT", 0, -28) + self.controls.groupLabel.width = function() + return self.controls.includeInFullDPS:GetPos() + self.controls.includeInFullDPS:GetSize() - self.controls.groupSlot:GetPos() + end + local function getSelectedItem() local item local groupSlot = self.controls.groupSlot:GetSelValue() @@ -229,20 +283,16 @@ function SkillsTabClass:SkillsTab(build) end return item, groupSlot end - self.controls.socketsLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.groupSlotLabel, "BOTTOMLEFT" }, { 0, 8, 0, 16 }, function() + self.controls.socketsLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, 58, 0, 16 }, function() local item = getSelectedItem() - local socketLine = "" - if item and item.base and not item.base.socketLimit then - socketLine = "This item cannot have sockets" - elseif item then - socketLine = self.build.itemsTab:GetSocketDescriptionLine(item) - end - return "^7Item sockets: " .. socketLine + return "^7Item Sockets:" .. (item and item.selectableSocketCount == 0 and " This item cannot have sockets" or "") end) self.controls.socketsLabel.shown = function() - local item = getSelectedItem() - return not not item + return getSelectedItem() ~= nil end + socketControls.create(self.controls, self.controls.socketsLabel, getSelectedItem, function(previousSockets) + self:CommitSocketEdit(getSelectedItem(), previousSockets) + end, false) local function getSocketCounts(item) local abyssalSocketCount = 0 for _, socket in ipairs(item.sockets) do @@ -253,25 +303,25 @@ function SkillsTabClass:SkillsTab(build) local maxSockets = (item.base.socketLimit or 0) - abyssalSocketCount return maxSockets, abyssalSocketCount end - self.controls.optimiseSockets = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.socketsLabel, "RIGHT" }, { 4, 0, 120, 18 }, "Optimise Sockets", function() + self.controls.optimiseSockets = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.socketsLabel, "RIGHT" }, { function() + local item = getSelectedItem() + return (#item.sockets - item.abyssalSocketCount) * 48 + (self.controls.displayItemAddSocket:IsShown() and 36 or 10) + end, 0, 120, 20 }, "Optimise Sockets", function() local item, groupSlot = getSelectedItem() if not item or not groupSlot or not item.base then return end - - self.build.itemsTab:AddUndoState() - local maxSockets, abyssalSocketCount = getSocketCounts(item) local groupCount = 0 + local previousSockets = item.sockets item.sockets = {} for _, group in ipairs(self.socketGroupList) do - local colours = { "R", "G", "B" } if group.slot == groupSlot.slotName then for _, gem in ipairs(group.gemList) do local grantedEffect = gem.grantedEffect or (gem.gemData and gem.gemData.grantedEffect) if grantedEffect and maxSockets > 0 then - local gemColour = grantedEffect.color and colours[grantedEffect.color] or "W" + local gemColour = grantedEffect.color and gemSocketColours[grantedEffect.color] or "W" table.insert(item.sockets, { color = gemColour, group = groupCount }) maxSockets = maxSockets - 1 end @@ -283,9 +333,7 @@ function SkillsTabClass:SkillsTab(build) groupCount = groupCount + 1 table.insert(item.sockets, { color = "A", group = groupCount }) end - item:BuildAndParseRaw() - self:UpdateSocketGroups() - self.build.buildFlag = true + self:CommitSocketEdit(item, previousSockets) end) self.controls.optimiseSockets.shown = function() local item = getSelectedItem() @@ -318,8 +366,8 @@ function SkillsTabClass:SkillsTab(build) -- buildFlag to true triggers the reload/run the CalcSetup to add on the support -- the last var in the GemSelectControl init, the true, sets imbuedSelect to true which sets the level to 1 and support filtering self.imbuedSupportBySlot = { } - self.controls.imbuedSupportLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.socketsLabel, "BOTTOMLEFT", true }, { 0, 8, 0, 16 }, colorCodes.CRAFTED .. "Imbued Support:") - self.controls.imbuedSupport = new("GemSelectControl"):GemSelectControl({ "LEFT", self.controls.imbuedSupportLabel, "RIGHT" }, { 8, 0, 250, 20 }, self, 1, function(gemData, _, _, gemMatch, slotName) + self.controls.imbuedSupportLabel = new("LabelControl"):LabelControl({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, 0, 0, 16 }, "^8(Legacy) Imbued Support:") + self.controls.imbuedSupport = new("GemSelectControl"):GemSelectControl({ "LEFT", self.controls.imbuedSupportLabel, "RIGHT" }, { 8, 0, 256, 20 }, self, 1, function(gemData, _, _, gemMatch, slotName) local targetSlot = slotName or (self.displayGroup and self.displayGroup.slot) if not targetSlot then return @@ -358,7 +406,7 @@ function SkillsTabClass:SkillsTab(build) return self.displayGroup and not self.displayGroup.source end - self.controls.imbuedSupportClear = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.imbuedSupportLabel, "RIGHT" }, { 260, 0, 20, 20}, "x", function() + self.controls.imbuedSupportClear = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.imbuedSupport, "RIGHT" }, { 2, 0, 20, 20 }, "x", function() self.controls.imbuedSupport.gemId = nil self.controls.imbuedSupport:SetText("") self.controls.imbuedSupport:gemChangeFunc(nil) @@ -426,14 +474,27 @@ will automatically apply to the skill.]] self:SetActiveSkillSet(1) -- Skill gem slots - self.anchorGemSlots = new("Control"):Control({ "TOPLEFT", self.controls.imbuedSupportLabel, "BOTTOMLEFT" }, { 0, 30, 0, 0 }) + self.anchorGemSlots = new("Control"):Control({ "TOPLEFT", self.anchorGroupDetail, "TOPLEFT" }, { 0, function() + return self.controls.socketsLabel:IsShown() and 110 or 76 + end, 0, 0 }) self.gemSlots = {} self:CreateGemSlot(1) self.controls.gemNameHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT", self.gemSlots[1].nameSpec, "TOPLEFT"}, {0, -2, 0, 16}, "^7Gem name:") self.controls.gemLevelHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT", self.gemSlots[1].level, "TOPLEFT"}, {0, -2, 0, 16}, "^7Level:") self.controls.gemQualityHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT", self.gemSlots[1].quality, "TOPLEFT"}, {0, -2, 0, 16}, "^7Quality:") self.controls.gemEnableHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT", self.gemSlots[1].enabled, "TOPLEFT"}, {-16, -2, 0, 16}, "^7Enabled:") - self.controls.gemCountHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT", self.gemSlots[1].count, "TOPLEFT"}, {8, -2, 0, 16}, "^7Count:") + self.controls.gemCountHeader = new("LabelControl"):LabelControl({"BOTTOMLEFT", self.gemSlots[1].enabled, "TOPRIGHT"}, {32, -2, 0, 16}, "^7Count:") + self.controls.gemCountHeader.shown = function() + if not self.displayGroup then + return false + end + for _, gemInstance in ipairs(self.displayGroup.gemList) do + if gemInstanceHasCount(gemInstance) then + return true + end + end + return false + end return self end @@ -521,6 +582,8 @@ function SkillsTabClass:LoadSkill(node, skillSetId) end function SkillsTabClass:Load(xml, fileName) + self.socketEdit = nil + self.nextSocketEditId = nil self.activeSkillSetId = 0 self.skillSets = { } self.skillSetOrderList = { } @@ -633,6 +696,19 @@ function SkillsTabClass:Draw(viewPort, inputEvents) self.y = viewPort.y self.width = viewPort.width self.height = viewPort.height + self.narrowLayout = viewPort.width < NARROW_VIEWPORT_WIDTH + self.controls.groupTip.shown = not self.narrowLayout + local optionOffset = self.narrowLayout and NARROW_OPTION_OFFSET or 0 + self.controls.optionSection.y = self.controls.optionSection.rectStart[2] + optionOffset + self.controls.sortGemsByDPS.y = self.controls.sortGemsByDPS.rectStart[2] + optionOffset + self.controls.defaultLevel.y = self.controls.defaultLevel.rectStart[2] + optionOffset + self.controls.defaultQuality.y = self.controls.defaultQuality.rectStart[2] + optionOffset + self.controls.showSupportGemTypes.y = self.controls.showSupportGemTypes.rectStart[2] + optionOffset + self.controls.showLegacyGems.y = self.controls.showLegacyGems.rectStart[2] + optionOffset + local gemNameWidth = self.narrowLayout and NARROW_GEM_NAME_WIDTH or GEM_NAME_WIDTH + for _, slot in ipairs(self.gemSlots) do + slot.nameSpec.width = gemNameWidth + end self.controls.scrollBarH.width = viewPort.width self.controls.scrollBarH.x = viewPort.x self.controls.scrollBarH.y = viewPort.y + viewPort.height - 18 @@ -689,6 +765,9 @@ function SkillsTabClass:Draw(viewPort, inputEvents) self:UpdateGemSlots() self:DrawControls(viewPort) + if self.controls.scrollBarH:IsShown() then + self.controls.scrollBarH:Draw(viewPort) + end end function SkillsTabClass:CopySocketGroup(socketGroup) @@ -783,7 +862,7 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."Delete"] = slot.delete -- Gem name specification - slot.nameSpec = new("GemSelectControl"):GemSelectControl({ "LEFT", slot.delete, "RIGHT" }, { 2, 0, 300, 20 }, self, index, function(gemId, addUndo, focusLost, bufMatchesGem) + slot.nameSpec = new("GemSelectControl"):GemSelectControl({ "LEFT", slot.delete, "RIGHT" }, { 2, 0, self.narrowLayout and NARROW_GEM_NAME_WIDTH or GEM_NAME_WIDTH, 20 }, self, index, function(gemId, addUndo, focusLost, bufMatchesGem) if not self.displayGroup then return end @@ -839,7 +918,7 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."Name"] = slot.nameSpec -- Gem level - slot.level = new("EditControl"):EditControl({ "LEFT", slot.nameSpec, "RIGHT" }, { 2, 0, 60, 20 }, nil, nil, "%D", 2, function(buf) + slot.level = new("EditControl"):EditControl({ "LEFT", slot.nameSpec, "RIGHT" }, { 4, 0, 60, 20 }, nil, nil, "%D", 2, function(buf) local gemInstance = self.displayGroup.gemList[index] if not gemInstance then gemInstance = { nameSpec = "", level = self.defaultGemLevel or 20, quality = self.defaultGemQuality or 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, count = 1, new = true } @@ -861,7 +940,7 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."Level"] = slot.level -- Gem quality - slot.quality = new("EditControl"):EditControl({"LEFT",slot.level,"RIGHT"}, {2, 0, 60, 20}, nil, nil, "%D", 2, function(buf) + slot.quality = new("EditControl"):EditControl({"LEFT",slot.level,"RIGHT"}, {4, 0, 60, 20}, nil, nil, "%D", 2, function(buf) local gemInstance = self.displayGroup.gemList[index] if not gemInstance then gemInstance = { nameSpec = "", level = self.defaultGemLevel or 20, quality = self.defaultGemQuality or 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, count = 1, new = true } @@ -886,7 +965,8 @@ function SkillsTabClass:CreateGemSlot(index) end end slot.quality.tooltipFunc = function(tooltip) - if tooltip:CheckForUpdate(self.build.outputRevision, self.displayGroup) then + if tooltip:CheckForUpdate(self.build.outputRevision, self.displayGroup, main.screenW) then + tooltip.maxWidth = m_min(600, main.screenW - 24) -- Get the gem instance from the skills local gemInstance = self.displayGroup.gemList[index] if not gemInstance then @@ -935,6 +1015,8 @@ function SkillsTabClass:CreateGemSlot(index) end end + self:AddGemQualityTooltip(tooltip, gemInstance) + local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator(self.build) if calcFunc then local storedQuality = self.displayGroup.gemList[index].quality @@ -952,7 +1034,7 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."Quality"] = slot.quality -- Enable gem - slot.enabled = new("CheckBoxControl"):CheckBoxControl({"LEFT",slot.quality,"RIGHT"}, {18, 0, 20}, nil, function(state) + slot.enabled = new("CheckBoxControl"):CheckBoxControl({"LEFT",slot.quality,"RIGHT"}, {24, 0, 20}, nil, function(state) local gemInstance = self.displayGroup.gemList[index] if not gemInstance then gemInstance = { nameSpec = "", level = self.defaultGemLevel or 20, quality = self.defaultGemQuality or 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, count = 1, new = true } @@ -991,7 +1073,7 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."Enable"] = slot.enabled -- Count gem - slot.count = new("EditControl"):EditControl({"LEFT",slot.enabled,"RIGHT"}, {18, 0, 60, 20}, nil, nil, "%D", 2, function(buf) + slot.count = new("EditControl"):EditControl({"LEFT",slot.enabled,"RIGHT"}, {24, 0, 60, 20}, nil, nil, "%D", 2, function(buf) local gemInstance = self.displayGroup.gemList[index] if not gemInstance then gemInstance = { nameSpec = "", level = self.defaultGemLevel or 20, quality = self.defaultGemQuality or 0, enabled = true, enableGlobal1 = true, count = 1, new = true } @@ -1009,15 +1091,7 @@ function SkillsTabClass:CreateGemSlot(index) end) slot.count.shown = function() local gemInstance = self.displayGroup and self.displayGroup.gemList[index] - if gemInstance then - local grantedEffectList = gemInstance.gemData and gemInstance.gemData.grantedEffectList or { gemInstance.grantedEffect } - for index, grantedEffect in ipairs(grantedEffectList) do - if not grantedEffect.support and not grantedEffect.unsupported and (not grantedEffect.hasGlobalEffect or gemInstance["enableGlobal"..index]) then - return true - end - end - end - return false + return gemInstanceHasCount(gemInstance) end slot.count.tooltipFunc = function(tooltip) if tooltip:CheckForUpdate(self.build.outputRevision, self.displayGroup) then @@ -1097,6 +1171,7 @@ function SkillsTabClass:UpdateGemSlots() slot.nameSpec.inactiveCol = self.displayGroup.gemList[slotIndex].color end end + self.controls.imbuedSupportLabel:SetAnchor("TOPLEFT", self.gemSlots[#self.displayGroup.gemList + 1].delete, "BOTTOMLEFT", 0, 26) end -- Find the skill gem matching the given specification @@ -1231,6 +1306,54 @@ function SkillsTabClass:ProcessSocketGroup(socketGroup) end end end + self:UpdateGroupLabel(socketGroup) +end + +-- Keep automatic names out of the persisted label so they follow gem edits and imports. +function SkillsTabClass:UpdateGroupLabel(group) + local names, disabledNames = { }, { } + for _, gem in ipairs(group.gemList) do + local effect = gem.gemData and gem.gemData.grantedEffect or gem.grantedEffect + if effect and not effect.support then + t_insert(gem.enabled and names or disabledNames, effect.name) + end + end + local automaticLabel = #names > 0 and table.concat(names, ", ") or #disabledNames > 0 and table.concat(disabledNames, ", ") or "" + group.displayLabel = group.label and group.label:match("%S") and group.label or (#names == 0 and #disabledNames > 0 and "^x7F7F7F" or "") .. automaticLabel + if self.displayGroup == group then + self.controls.groupLabel:SetPlaceholder(automaticLabel) + end +end + +local qualitySources = { + { "Socket colour", "socketQuality" }, + { "Support", "supportQuality" }, +} + +function SkillsTabClass:AddGemQualityTooltip(tooltip, gem) + local effect = gem.displayEffect + local rows = { { source = "Gem", quality = string.format("%g%%", gem.quality or 0) } } + if effect then + for _, source in ipairs(qualitySources) do + local value = effect[source[2]] or 0 + if value ~= 0 then + t_insert(rows, { source = source[1], quality = string.format("%+g%%", value) }) + end + end + for _, property in ipairs(effect.gemPropertyInfo or { }) do + if property.value.key == "quality" and property.value.value ~= 0 then + local source = calcsHelpers.ResolveSourceName(property.mod, self.build) + if source == "" then + local customName = property.mod.source and property.mod.source:match("^Custom:(.+)") + source = customName and "Custom: " .. customName or "Unknown source" + end + t_insert(rows, { source = source, quality = string.format("%+g%%", property.value.value) }) + end + end + end + t_insert(rows, { source = "Total", quality = string.format("%g%%", effect and effect.quality or gem.quality or 0) }) + tooltip:AddTable({ { label = "Source", key = "source" }, { label = "Quality", key = "quality", right = true } }, rows, true) + tooltip:AddSeparator(10) end -- reprocess socket groups on rebuild @@ -1251,18 +1374,14 @@ function SkillsTabClass:UpdateSocketGroups() -- since PoB processes split links on an item as separate -- groups, we can assume that we continue from where the last -- socket group with the slot ended at - local colours = { "R", "G", "B" } local gemIdx = gemOffset + i if slot then local item = self.build.itemsTab.items[slot.selItemId] if item and item.sockets then - -- e.g. dialla's malefaction - if item.sockets.colourAlwaysMatches then - gemInstance.matchesSocket = true - else - local gemColour = grantedEffect.color and colours[grantedEffect.color] - gemInstance.matchesSocket = item.sockets[gemIdx] and (item.sockets[gemIdx].color == gemColour) - end + local socket = item.sockets[gemIdx] + -- Dialla's Malefaction grants the bonus without requiring the gem colour to match. + gemInstance.matchesSocket = not not (socket and (item.sockets.colourAlwaysMatches or + grantedEffect.color and socket.color == gemSocketColours[grantedEffect.color])) end end end @@ -1270,7 +1389,17 @@ function SkillsTabClass:UpdateSocketGroups() slotSocketedCounts[socketGroup.slot] = gemOffset + #socketGroup.gemList end end + self:UpdateItemSocketControls() end + +function SkillsTabClass:UpdateItemSocketControls() + local slot = self.displayGroup and self.build.itemsTab.slots[self.displayGroup.slot] + local item = slot and self.build.itemsTab.items[slot.selItemId] + if item then + socketControls.update(self.controls, item) + end +end + -- Set the skill to be displayed/edited function SkillsTabClass:SetDisplayGroup(socketGroup) self.displayGroup = socketGroup @@ -1278,8 +1407,9 @@ function SkillsTabClass:SetDisplayGroup(socketGroup) self:ProcessSocketGroup(socketGroup) -- Update the main controls - self.controls.groupLabel:SetText(socketGroup.label) + self.controls.groupLabel:SetText(socketGroup.label or "") self.controls.groupSlot:SelByValue(socketGroup.slot, "slotName") + self:UpdateItemSocketControls() self.controls.groupEnabled.state = socketGroup.enabled self.controls.includeInFullDPS.state = socketGroup.includeInFullDPS and socketGroup.enabled self.controls.groupCount:SetText(socketGroup.groupCount or 1) @@ -1394,8 +1524,49 @@ function SkillsTabClass:AddSocketGroupTooltip(tooltip, socketGroup) end end +function SkillsTabClass:CommitSocketEdit(item, previousSockets) + item:BuildAndParseRaw() + self.nextSocketEditId = (self.nextSocketEditId or 0) + 1 + self.socketEdit = { + id = self.nextSocketEditId, + previousId = self.socketEdit and self.socketEdit.id or 0, + item = item, + before = copyTable(previousSockets), + after = copyTable(item.sockets), + } + self.build.itemsTab:AddUndoState() + self:UpdateSocketGroups() + self:AddUndoState() + self.build.buildFlag = true +end + +---@param socketEdit SkillSocketEdit? +function SkillsTabClass:RestoreSocketEdit(socketEdit) + if self.socketEdit == socketEdit then + return + end + local edit, expected, restored + if self.socketEdit and self.socketEdit.previousId == (socketEdit and socketEdit.id or 0) then + edit = self.socketEdit + expected, restored = edit.after, edit.before + elseif socketEdit and socketEdit.previousId == (self.socketEdit and self.socketEdit.id or 0) then + edit = socketEdit + expected, restored = edit.before, edit.after + end + self.socketEdit = socketEdit + -- A replacement, Items undo, or later socket edit supersedes this Skills action. + if not edit or self.build.itemsTab.items[edit.item.id] ~= edit.item or not socketLayoutsMatch(edit.item.sockets, expected) then + return + end + edit.item.sockets = copyTable(restored) + edit.item:BuildAndParseRaw() + self.build.itemsTab:AddUndoState() + self:UpdateSocketGroups() +end + function SkillsTabClass:CreateUndoState() local state = { } + state.socketEdit = self.socketEdit state.activeSkillSetId = self.activeSkillSetId state.skillSets = { } for skillSetIndex, skillSet in pairs(self.skillSets) do @@ -1436,6 +1607,7 @@ function SkillsTabClass:RestoreUndoState(state) -- Load active socket group for both skillsTab and calcsTab from UndoState self.build.mainSocketGroup = state.activeSocketGroup self.build.calcsTab.input.skill_number = state.activeSocketGroup2 + self:RestoreSocketEdit(state.socketEdit) end -- Opens the skill set manager diff --git a/src/Classes/Tooltip.lua b/src/Classes/Tooltip.lua index 0a806c0cedc..bfee2e5190c 100644 --- a/src/Classes/Tooltip.lua +++ b/src/Classes/Tooltip.lua @@ -41,6 +41,7 @@ for _, recipeName in pairs(recipeNames) do end ---@class Tooltip +---@field minX? number Minimum left edge for hover tooltips, allowing overflow to the right. local TooltipClass = newClass("Tooltip") function TooltipClass:Tooltip() @@ -63,6 +64,7 @@ function TooltipClass:Clear(clearUpdateParams) self.recipe = nil self.center = false self.maxWidth = nil + self.minX = nil ---@type string|[number, number, number] self.color = { 0.5, 0.3, 0 } t_insert(self.blocks, { height = 0 }) @@ -103,9 +105,16 @@ function TooltipClass:AddLine(size, text, font, modLine, background) self.blocks[#self.blocks].height = self.blocks[#self.blocks].height + size + 2 end if self.maxWidth then - for _, wrappedLine in ipairs(main:WrapString(line, size, self.maxWidth - H_PAD)) do - t_insert(self.lines, { size = size, text = wrappedLine, block = #self.blocks, font = fontToUse, center = self.center, modLine = modLine, background = background }) + local wrappedLines = main:WrapString(line, size, self.maxWidth - H_PAD) + local color = "" + for _, wrappedLine in ipairs(wrappedLines) do + t_insert(self.lines, { size = size, text = color .. wrappedLine, block = #self.blocks, font = fontToUse, center = self.center, modLine = modLine, background = background }) + -- Continuation lines retain the last inline colour, including unsupported-mod warnings. + for pos, marker in wrappedLine:gmatch("()%^([%dx])") do + color = wrappedLine:sub(pos, pos + (marker == "x" and 7 or 1)) + end end + self.blocks[#self.blocks].height = self.blocks[#self.blocks].height + (#wrappedLines - 1) * (size + 2) else t_insert(self.lines, { size = size, text = line, block = #self.blocks, font = fontToUse, center = self.center, modLine = modLine, background = background }) end @@ -113,6 +122,40 @@ function TooltipClass:AddLine(size, text, font, modLine, background) end end +-- Use the same column padding, type sizes and row heights as Calcs breakdown tables. +---@param colList table[] Columns with label, key, and optional right alignment +---@param rowList table[] Rows indexed by the column keys +---@param fullWidth boolean? Extend the first column and grid to the tooltip edges +function TooltipClass:AddTable(colList, rowList, fullWidth) + local width = 4 + for _, col in ipairs(colList) do + col.width = DrawStringWidth(16, "VAR", col.label) + 6 + for _, row in ipairs(rowList) do + col.width = m_max(col.width, DrawStringWidth(12, "VAR", tostring(row[col.key] or "")) + 6) + end + width = width + col.width + end + for index = 0, #rowList do + local cells = { } + for _, col in ipairs(colList) do + t_insert(cells, index == 0 and col.label or tostring(rowList[index][col.key] or "")) + end + local size = index == 0 and 18 or 12 + t_insert(self.lines, { + text = table.concat(cells, "\t"), + size = size, + font = "VAR", + block = #self.blocks, + width = width, + cells = cells, + colList = colList, + fullWidth = fullWidth, + header = index == 0, + }) + self.blocks[#self.blocks].height = self.blocks[#self.blocks].height + size + 2 + end +end + function TooltipClass:SetRecipe(recipe) self.recipe = recipe end @@ -167,7 +210,7 @@ function TooltipClass:GetSize() ttH = ttH + data.size + 2 end if data.text then - ttW = m_max(ttW, DrawStringWidth(data.size, data.font, data.text)) + ttW = m_max(ttW, data.width or DrawStringWidth(data.size, data.font, data.text)) end end @@ -298,18 +341,48 @@ function TooltipClass:CalculateColumns(ttY, ttX, ttH, ttW, viewPort) local lineX = lineCentered and (x + ttW / 2) or (x + (H_PAD / 2)) local lineAlign = lineCentered and "CENTER_X" or "LEFT" - local stackEntry = {lineX, y, lineAlign, data.size, font, data.text} - if data.modLine and data.modLine.disabled then - stackEntry.strikethrough = true + if data.cells then + local tableWidth = data.fullWidth and ttW or data.width + local colX = x + (data.fullWidth and 0 or H_PAD / 2) + 4 + local gridY, gridHeight = y, data.size + 2 + if data.fullWidth then + -- Extend dividers through section padding to meet the surrounding borders. + local prevLine, nextLine = self.lines[i - 1], self.lines[i + 1] + if data.header then + local topPad = y == ttY + 2 * BORDER_WIDTH and BORDER_WIDTH or prevLine and not prevLine.text and not prevLine.separatorImage and prevLine.size / 2 + 1 or 0 + gridY, gridHeight = gridY - topPad, gridHeight + topPad + end + if not nextLine or nextLine.colList ~= data.colList then + local bottomPad = nextLine and not nextLine.text and not nextLine.separatorImage and self.lines[i + 2] and self.lines[i + 2].text and nextLine.size / 2 - 1 or BORDER_WIDTH + gridHeight = gridHeight + bottomPad + end + end + if not data.header then + t_insert(drawStack, {nil, data.fullWidth and x + BORDER_WIDTH or colX - 2, y - 1, data.fullWidth and ttW - 2 * BORDER_WIDTH or tableWidth - 4, 1, color = { 0.5, 0.5, 0.5 }, tableGrid = true}) + end + for index, col in ipairs(data.colList) do + local colWidth = col.width + (index == 1 and tableWidth - data.width or 0) + if index > 1 then + t_insert(drawStack, {nil, colX - 2, gridY, 1, gridHeight, color = { 0.5, 0.5, 0.5 }, tableGrid = true}) + end + local right = not data.header and col.right + t_insert(drawStack, {right and colX + colWidth - 4 or colX, y + (data.header and 2 or 1), right and "RIGHT_X" or "LEFT", data.header and 16 or 12, "VAR", "^7" .. data.cells[index]}) + colX = colX + colWidth + end + else + local stackEntry = {lineX, y, lineAlign, data.size, font, data.text} + if data.modLine and data.modLine.disabled then + stackEntry.strikethrough = true + end + stackEntry.background = data.background + t_insert(drawStack, stackEntry) end - stackEntry.background = data.background - t_insert(drawStack, stackEntry) data.bounds = { x = x + (H_PAD / 2), y = y, width = ttW - H_PAD, height = data.size + 2 } y = y + data.size + 2 -- track max width for extra columns if columns > 1 then - extraColumnWidth = m_max(extraColumnWidth, DrawStringWidth(data.size, font, data.text) + H_PAD) + extraColumnWidth = m_max(extraColumnWidth, data.fullWidth and ttW or (data.width or DrawStringWidth(data.size, font, data.text)) + H_PAD) end elseif data.separatorImage and main.showFlavourText then @@ -356,7 +429,7 @@ function TooltipClass:CalculateColumns(ttY, ttX, ttH, ttW, viewPort) end -- Resize separators/dividers (technically unlikely to appear in extra columns, but just in case) - if not isText then + if not isText and not line.tableGrid then -- separator images have `width` value at index 4 if line[1] and type(line[1]) == "table" and line[1].isSeparator then line[4] = extraColumnWidth - H_PAD -- "fancy" separators get extra padding @@ -435,7 +508,9 @@ function TooltipClass:Draw(x, y, w, h, viewPort) local isHoverToolTip = w and h -- `w` and `h` typically only provided for hover tooltips if isHoverToolTip then ttX = ttX + w + 5 - if ttX + ttW > viewPort.x + viewPort.width then + if self.minX then + ttX = m_max(ttX, self.minX) + elseif ttX + ttW > viewPort.x + viewPort.width then ttX = m_max(viewPort.x, x - 5 - ttW) if ttX + ttW > x then ttY = ttY + h @@ -454,7 +529,7 @@ function TooltipClass:Draw(x, y, w, h, viewPort) -- If hover tooltip and extra columns don't fit, shift to left and adjust drawStack (because hover tooltips can't scroll) if columns > 1 and isHoverToolTip and totalDrawWidth + ttX >= viewPort.x + viewPort.width then - local newX = m_max(viewPort.x, viewPort.x + viewPort.width - totalDrawWidth) + local newX = m_max(self.minX or viewPort.x, viewPort.x + viewPort.width - totalDrawWidth) local offsetX = newX - ttX ttX = newX @@ -588,6 +663,8 @@ function TooltipClass:Draw(x, y, w, h, viewPort) else SetDrawColor(1, 1, 1) end + elseif line.color then + SetDrawColor(unpack(line.color)) elseif type(self.color) == "string" then SetDrawColor(self.color) else diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 842b3b4e09a..034402a03f0 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -474,7 +474,7 @@ function TreeTabClass:Draw(viewPort, inputEvents) SetDrawLayer(1) SetDrawColor(0.05, 0.05, 0.05) - DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (30 + bottomDrawerHeight + linesHeight), viewPort.width, 30 + bottomDrawerHeight + linesHeight) + DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (30 + bottomDrawerHeight + linesHeight), viewPort.width, 34 + bottomDrawerHeight + linesHeight) if self.showConvert then local height = viewPort.width < convertMaxWidth and (bottomDrawerHeight + linesHeight) or 0 SetDrawColor(0.05, 0.05, 0.05) diff --git a/src/Modules/AddImplicitPopup.lua b/src/Modules/AddImplicitPopup.lua index b324c47cddb..41ee8c0e083 100644 --- a/src/Modules/AddImplicitPopup.lua +++ b/src/Modules/AddImplicitPopup.lua @@ -25,6 +25,18 @@ function M.AddImplicitToDisplayItem(itemsTab, displayItem) local modGroups = {} local sortList, sortStats = buildModSortList() if not displayItem then return end + local popupWidth = 710 + local padding = 12 + local gap = 8 + local controlHeight = 20 + local rowHeight = controlHeight + gap + local contentY = 24 + local fieldX = padding + DrawStringWidth(16, "VAR", "Modifier:") + gap + local fieldWidth = popupWidth - fieldX - padding + local function popupHeight(sourceId) + local rowCount = (sourceId == "EXARCH" or sourceId == "EATER") and 3 or 2 + return contentY + (rowCount - 1) * rowHeight + controlHeight * 2 + padding * 2 + end -- these closures should probably be refactored to be outside this function -- at some point local function setDefaultSortOrder() @@ -307,10 +319,8 @@ function M.AddImplicitToDisplayItem(itemsTab, displayItem) item:BuildAndParseRaw() return item end - controls.sourceLabel = new("LabelControl"):LabelControl({ "TOPRIGHT", nil, "TOPLEFT" }, { 95, 20, 0, 16 }, "^7Source:") - controls.source = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { 100, 20, 150, 18 }, sourceList, function(index, value) + controls.source = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { fieldX, contentY, 150, controlHeight }, sourceList, function(index, value) if value.sourceId ~= "CUSTOM" then - controls.modSelectLabel.y = 70 buildMods(value.sourceId) controls.modGroupSelect:SetSel(1) controls.modSelect.list = modList[modGroups[1].modListIndex] @@ -318,38 +328,32 @@ function M.AddImplicitToDisplayItem(itemsTab, displayItem) if controls.sort then applySort(controls.sort.list[controls.sort.selIndex].stat, true) end - else - controls.modSelectLabel.y = 45 end + main.popups[1].height = popupHeight(value.sourceId) end) + controls.source.fontSize = 14 + controls.sourceLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.source, "LEFT" }, { -gap, 0, 0, 16 }, "^7Source:") controls.source.enabled = #sourceList > 1 - controls.sortLabel = new("LabelControl"):LabelControl({ "TOPRIGHT", nil, "TOPLEFT" }, { 350, 20, 0, 16 }, "^7Sort by:") - controls.sortLabel.shown = function() - return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" - end - controls.sort = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { 355, 20, 240, 18 }, sortList, function(index, value) + controls.sort = new("DropDownControl"):DropDownControl({ "TOPRIGHT", nil, "TOPRIGHT" }, { -padding, contentY, 240, controlHeight }, sortList, function(index, value) applySort(value.stat, true) end) + controls.sort.fontSize = 14 + controls.sortLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.sort, "LEFT" }, { -gap, 0, 0, 16 }, "^7Sort by:") controls.sort.shown = function() return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" end - controls.modGroupSelectLabel = new("LabelControl"):LabelControl({ "TOPRIGHT", nil, "TOPLEFT" }, { 95, 45, 0, 16 }, function() + controls.modGroupSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { fieldX, contentY + rowHeight, fieldWidth, controlHeight }, modGroups, function(index, value) + controls.modSelect.list = modList[value.modListIndex] + controls.modSelect:SetSel(1) + end) + controls.modGroupSelect.fontSize = 14 + controls.modGroupSelectLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.modGroupSelect, "LEFT" }, { -gap, 0, 0, 16 }, function() if controls.modSelect:IsShown() then return "^7Type:" else return "^7Modifier:" end end) - controls.modGroupSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { 100, 45, 600, 18 }, modGroups, function(index, value) - controls.modSelect.list = modList[value.modListIndex] - controls.modSelect:SetSel(1) - end) - controls.modGroupSelectLabel.shown = function() - if sourceList[controls.source.selIndex].sourceId == "CUSTOM" then - controls.modSelectLabel.y = 45 - end - return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" - end controls.modGroupSelect.shown = function() return sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" end @@ -365,8 +369,9 @@ function M.AddImplicitToDisplayItem(itemsTab, displayItem) itemsTab:AddModComparisonTooltip(tooltip, value.mod, value.type == "vestigial") end end - controls.modSelectLabel = new("LabelControl"):LabelControl({ "TOPRIGHT", nil, "TOPLEFT" }, { 95, 70, 0, 16 }, "^7Modifier:") - controls.modSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { 100, 70, 600, 18 }, sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" and modList[modGroups[1].modListIndex] or {}) + controls.modSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { fieldX, contentY + rowHeight * 2, fieldWidth, controlHeight }, sourceList[controls.source.selIndex].sourceId ~= "CUSTOM" and modList[modGroups[1].modListIndex] or {}) + controls.modSelect.fontSize = 14 + controls.modSelectLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.modSelect, "LEFT" }, { -gap, 0, 0, 16 }, "^7Modifier:") local modSelectHidden = { CUSTOM = true, -- vestigial implicits aren't grouped together, and the type selector @@ -376,7 +381,6 @@ function M.AddImplicitToDisplayItem(itemsTab, displayItem) controls.modSelect.shown = function() return not modSelectHidden[sourceList[controls.source.selIndex].sourceId] end - controls.modSelectLabel.shown = controls.modSelect.shown controls.modSelect.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if mode ~= "OUT" and value then @@ -389,11 +393,12 @@ function M.AddImplicitToDisplayItem(itemsTab, displayItem) itemsTab:AddModComparisonTooltip(tooltip, value.mod) end end - controls.custom = new("EditControl"):EditControl({ "TOPLEFT", nil, "TOPLEFT" }, { 100, 45, 440, 18 }) + controls.custom = new("EditControl"):EditControl({ "TOPLEFT", nil, "TOPLEFT" }, { fieldX, contentY + rowHeight, fieldWidth, controlHeight }) + controls.customLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.custom, "LEFT" }, { -gap, 0, 0, 16 }, "^7Modifier:") controls.custom.shown = function() return sourceList[controls.source.selIndex].sourceId == "CUSTOM" end - controls.save = new("ButtonControl"):ButtonControl({ "BOTTOMRIGHT", nil, "BOTTOM" }, { -4, -8, 80, 20 }, "Add", function() + controls.save = new("ButtonControl"):ButtonControl({ "BOTTOMRIGHT", nil, "BOTTOM" }, { -gap / 2, -padding, 80, controlHeight }, "Add", function() itemsTab:SetDisplayItem(addModifier()) main:ClosePopup() end) @@ -401,14 +406,10 @@ function M.AddImplicitToDisplayItem(itemsTab, displayItem) tooltip:Clear() itemsTab:AddItemTooltip(tooltip, addModifier()) end - controls.close = new("ButtonControl"):ButtonControl({ "BOTTOMLEFT", nil, "BOTTOM" }, { 4, -8, 80, 20 }, "Cancel", function() + controls.close = new("ButtonControl"):ButtonControl({ "BOTTOMLEFT", nil, "BOTTOM" }, { gap / 2, -padding, 80, controlHeight }, "Cancel", function() main:ClosePopup() end) - local popupHeight = 130 - if not controls.modSelect.shown() then - popupHeight = popupHeight - 20 - end - main:OpenPopup(710, popupHeight, "Add Implicit to Item", controls, "save", sourceList[controls.source.selIndex].sourceId == "CUSTOM" and "custom") + main:OpenPopup(popupWidth, popupHeight(sourceList[controls.source.selIndex].sourceId), "Add Implicit to Item", controls, "save", sourceList[controls.source.selIndex].sourceId == "CUSTOM" and "custom") end return M diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 3ad9d8eafff..b90a6525a96 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -14,6 +14,18 @@ local m_huge = math.huge local m_floor = math.floor local m_abs = math.abs local s_format = string.format +local sideBarWidth = 322 +local narrowViewportWidth = 756 +local topBarLabels = { + { "classLabel", "Class:", "Class:" }, + { "loadoutsLabel", "Loadouts:", "Load:" }, +} +local topBarFlexibleControls = { + { "classDrop", 60 }, + { "ascendDrop", 60 }, + { "secondaryAscendDrop", 60 }, + { "buildLoadouts", 60 }, +} ---@class Build: ControlHost ---@field spec PassiveSpec added by TreeTab @@ -21,6 +33,25 @@ local s_format = string.format ---@field powerBuilderCallback fun() local buildMode = new("ControlHost"):ControlHost() +local function isNarrowViewport() + return main.screenW - sideBarWidth < narrowViewportWidth +end + +local function fitTopBarLabel(label, shortLabel, width) + if DrawStringWidth(16, "VAR", label) <= width then + return "^7"..label + end + label = shortLabel + if DrawStringWidth(16, "VAR", label) > width then + local suffix = DrawStringWidth(16, "VAR", label:sub(1, 1).."...") <= width and "..." or "" + while #label > 0 and DrawStringWidth(16, "VAR", label..suffix) > width do + label = label:sub(1, -2) + end + label = label..suffix + end + return "^7"..label +end + local function InsertIfNew(t, val) if (not t) then return end for i,v in ipairs(t) do @@ -119,52 +150,59 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin local miscTooltip = new("Tooltip"):Tooltip() -- Controls: top bar, left side - self.anchorTopBarLeft = new("Control"):Control(nil, {4, 4, 0, 20}) - self.controls.back = new("ButtonControl"):ButtonControl({"LEFT",self.anchorTopBarLeft,"RIGHT"}, {0, 0, 60, 20}, "<< Back", function() + local topBarButtonWidth = (sideBarWidth - 4 - 6 * 2 - 8 * 3) / 4 + -- Use absolute positions to preserve half-unit widths without anchor rounding. + self.controls.back = new("ButtonControl"):ButtonControl(nil, {6, 6, topBarButtonWidth, 20}, "<< Back", function() if self.unsaved then self:OpenSavePopup("LIST") else self:CloseBuild() end end) - self.controls.save = new("ButtonControl"):ButtonControl({"LEFT",self.controls.back,"RIGHT"}, {8, 0, 50, 20}, "Save", function() + self.controls.new = new("ButtonControl"):ButtonControl(nil, {6 + 3 * (topBarButtonWidth + 8), 6, topBarButtonWidth, 20}, "New", function() + if self:CanExit("NEW") then + self:NewBuild() + end + end) + self.controls.save = new("ButtonControl"):ButtonControl(nil, {6 + topBarButtonWidth + 8, 6, topBarButtonWidth, 20}, "Save", function() self:SaveDBFile() end) self.controls.save.enabled = function() return not self.dbFileName or self.unsaved end - self.controls.saveAs = new("ButtonControl"):ButtonControl({"LEFT",self.controls.save,"RIGHT"}, {8, 0, 70, 20}, "Save As", function() + self.controls.saveAs = new("ButtonControl"):ButtonControl(nil, {6 + 2 * (topBarButtonWidth + 8), 6, topBarButtonWidth, 20}, "Save As", function() self:OpenSaveAsPopup() end) self.controls.saveAs.enabled = function() return self.dbFileName end - -- conditional for smaller screens to move "Current build" to the side bar + -- conditional for smaller screens to move "Build" to the side bar local function buildNameConditional() - return self.anchorTopBarRight:GetPos() < 800 + return main.screenW < 1750 end - self.controls.buildName = new("Control"):Control({"LEFT",self.controls.saveAs,"RIGHT"}, {4, 36, 0, 20}) + self.controls.buildName = new("Control"):Control(nil, {function() return buildNameConditional() and 4 or sideBarWidth + 6 end, function() return buildNameConditional() and 38 or 6 end, 0, 20}) self.controls.buildName.width = function(control) - local limit = buildNameConditional() and 203 or - (self.anchorTopBarRight:GetPos() - 98 - 62 - - self.controls.pointDisplay:GetSize() - self.controls.levelScalingButton:GetSize() - self.controls.characterLevel:GetSize() - - self.controls.back:GetSize() - self.controls.save:GetSize() - self.controls.saveAs:GetSize()) + local labelWidth = DrawStringWidth(16, "VAR", "Build:") + 4 + local rightEdge = buildNameConditional() and sideBarWidth - 10 or m_floor(self.controls.pointDisplay:GetPos() - 4 - DrawStringWidth(16, "VAR", "Passives:")) - 16 + local limit = m_max(0, m_min(330, m_floor(rightEdge - control:GetPos() - labelWidth - 22))) local bnw = DrawStringWidth(16, "VAR", self.buildName) self.strWidth = m_min(bnw, limit) self.strLimited = bnw > limit - return self.strWidth + 98 + return labelWidth + self.strWidth + 23 end self.controls.buildName.Draw = function(control) local x, y = control:GetPos() local width, height = control:GetSize() + local labelWidth = DrawStringWidth(16, "VAR", "Build:") + 4 SetDrawColor(0.5, 0.5, 0.5) - DrawImage(nil, x + 91, y, self.strWidth + 6, 20) + DrawImage(nil, x + labelWidth, y, self.strWidth + 22, 20) SetDrawColor(0, 0, 0) - DrawImage(nil, x + 92, y + 1, self.strWidth + 4, 18) + DrawImage(nil, x + labelWidth + 1, y + 1, self.strWidth + 20, 18) SetDrawColor(1, 1, 1) - SetViewport(x, y + 2, self.strWidth + 94, 16) - DrawString(0, 0, "LEFT", 16, "VAR", "Current build: "..self.buildName) + DrawString(x, y + 2, "LEFT", 16, "VAR", "Build:") + SetViewport(x + labelWidth + 11, y + 2, self.strWidth, 16) + DrawString(0, 0, "LEFT", 16, "VAR", self.buildName) SetViewport() if control:IsMouseInBounds() then SetDrawLayer(nil, 10) @@ -178,30 +216,24 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin SetDrawLayer(nil, 0) end end - self.controls.buildName.x = function() - return buildNameConditional() and -196 or 8 - end - self.controls.buildName.y = function() - return buildNameConditional() and 32 or 0 - end - - -- Controls: top bar, right side - self.anchorTopBarRight = new("Control"):Control(nil, {function() return main.screenW / 2 + 6 end, 4, 0, 20}) - local function getPointDisplayX() -- I had it hardcoded to -323 before switching to the control sizing - return - (23 + self.controls.pointDisplay:GetSize() + self.controls.levelScalingButton:GetSize() + self.controls.characterLevel:GetSize()) - end - self.controls.pointDisplay = new("Control"):Control({"LEFT",self.anchorTopBarRight,"RIGHT"}, {function() return getPointDisplayX() end, 0, 0, 20}) + -- Controls: top bar, character settings + self.anchorTopBarLeft = new("Control"):Control(nil, {function() + -- The build-name box ends one unit before the control's right edge. + return buildNameConditional() and sideBarWidth + 6 or self.controls.buildName:GetPos() + self.controls.buildName:GetSize() - 1 + 16 + end, 6, 0, 20}) + self.controls.pointDisplay = new("Control"):Control(nil, {function() return main.screenW / 2 - 3 - self.controls.pointDisplay:GetSize() end, 6, 0, 20}) + self.controls.pointLabel = new("LabelControl"):LabelControl({"RIGHT",self.controls.pointDisplay,"LEFT"}, {-4, 0, 0, 16}, "^7Passives:") self.controls.pointDisplay.width = function(control) - return DrawStringWidth(16, "FIXED", control.str) + 8 + return DrawStringWidth(16, "FIXED", control.str) + (isNarrowViewport() and 8 or 10) end self.controls.pointDisplay.Draw = function(control) local x, y = control:GetPos() local width, height = control:GetSize() SetDrawColor(1, 1, 1) - DrawImage(nil, x, y, width + 2, height) + DrawImage(nil, x, y, width, height) SetDrawColor(0, 0, 0) - DrawImage(nil, x + 1, y + 1, width, height - 2) + DrawImage(nil, x + 1, y + 1, width - 2, height - 2) SetDrawColor(1, 1, 1) DrawString(x + 4, y + 2, "LEFT", 16, "FIXED", control.str) if control:IsMouseInBounds() then @@ -212,20 +244,37 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin SetDrawLayer(nil, 0) end end - self.controls.levelScalingButton = new("ButtonControl"):ButtonControl({"LEFT",self.controls.pointDisplay,"RIGHT"}, {7, 0, 50, 20}, self.characterLevelAutoMode and "Auto" or "Manual", function() + self.controls.levelScalingButton = new("ButtonControl"):ButtonControl({"LEFT",self.controls.pointDisplay,"RIGHT"}, {16, 0, function() + return isNarrowViewport() and 24 or 62 + end, 20}, function() + if isNarrowViewport() then + return self.characterLevelAutoMode and "A" or "M" + end + return self.characterLevelAutoMode and "Auto" or "Manual" + end, function() self.characterLevelAutoMode = not self.characterLevelAutoMode - self.controls.levelScalingButton.label = self.characterLevelAutoMode and "Auto" or "Manual" self.configTab:BuildModList() self.modFlag = true self.buildFlag = true end) - self.controls.characterLevel = new("EditControl"):EditControl({"LEFT",self.controls.levelScalingButton,"RIGHT"}, {5, 0, 106, 20}, "", "Level", "%D", 3, function(buf) + local levelScalingTooltip = self.controls.levelScalingButton.tooltip + levelScalingTooltip:AddLine(14, "Manual: Set the character level directly.\nAuto: Estimate the character level from allocated passives.") + self.controls.levelScalingButton.onHover = function() + local control = self.controls.levelScalingButton + local x, y = control:GetPos() + local _, height = control:GetSize() + local tooltipWidth = levelScalingTooltip:GetSize() + local tooltipX = m_max(main.viewPort.x, m_min(x, main.viewPort.x + main.viewPort.width - tooltipWidth)) + SetDrawLayer(nil, 100) + levelScalingTooltip:Draw(tooltipX, y + height + 5, nil, nil, main.viewPort) + SetDrawLayer(nil, 0) + end + self.controls.characterLevel = new("EditControl"):EditControl({"LEFT",self.controls.levelScalingButton,"RIGHT"}, {5, 0, 110, 20}, "", "Level", "%D", 3, function(buf) self.characterLevel = m_min(m_max(tonumber(buf) or 1, 1), 100) self.configTab:BuildModList() self.modFlag = true self.buildFlag = true self.characterLevelAutoMode = false - self.controls.levelScalingButton.label = "Manual" end) self.controls.characterLevel:SetText(self.characterLevel) self.controls.characterLevel.tooltipFunc = function(tooltip) @@ -256,7 +305,8 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin end end end - self.controls.classDrop = new("DropDownControl"):DropDownControl({"LEFT",self.controls.characterLevel,"RIGHT"}, {10, 0, 85, 20}, nil, function(index, value) + self.controls.classLabel = new("LabelControl"):LabelControl({"LEFT",self.controls.characterLevel,"RIGHT"}, {40, 0, 0, 16}, "^7Class:") + self.controls.classDrop = new("DropDownControl"):DropDownControl({"LEFT",self.controls.classLabel,"RIGHT"}, {4, 0, 85, 20}, nil, function(index, value) if value.classId ~= self.spec.curClassId then if self.spec:CountAllocNodes() == 0 or self.spec:IsClassConnected(value.classId) then self.spec:SelectClass(value.classId) @@ -280,13 +330,15 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin end end end) + self.controls.classDrop.clampDrop = true self.controls.ascendDrop = new("DropDownControl"):DropDownControl({"LEFT",self.controls.classDrop,"RIGHT"}, {4, 0, 120, 20}, nil, function(index, value) self.spec:SelectAscendClass(value.ascendClassId) self.spec:AddUndoState() self.spec:SetWindowTitleWithBuildClass() self.buildFlag = true end) - self.controls.secondaryAscendDrop = new("DropDownControl"):DropDownControl({"LEFT",self.controls.ascendDrop,"RIGHT"}, {4, 0, 160, 20}, { + self.controls.ascendDrop.clampDrop = true + self.controls.secondaryAscendDrop = new("DropDownControl"):DropDownControl({"LEFT",self.controls.ascendDrop,"RIGHT"}, {4, 0, 155, 20}, { { label = "None", ascendClassId = 0 }, }, function(index, value) if not value or not self.spec then @@ -298,11 +350,13 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.buildFlag = true end) self.controls.secondaryAscendDrop.enableDroppedWidth = true + self.controls.secondaryAscendDrop.clampDrop = true self.controls.secondaryAscendDrop.maxDroppedWidth = 360 local initialSecondarySelection = (self.spec and self.spec.curSecondaryAscendClassId) or 0 self.controls.secondaryAscendDrop:SelByValue(initialSecondarySelection, "ascendClassId") - self.controls.buildLoadouts = new("DropDownControl"):DropDownControl({"LEFT",self.controls.secondaryAscendDrop,"RIGHT"}, {4, 0, 190, 20}, {}, function(index, value) + self.controls.loadoutsLabel = new("LabelControl"):LabelControl({"LEFT",self.controls.secondaryAscendDrop,"RIGHT"}, {40, 0, 0, 16}, "^7Loadouts:") + self.controls.buildLoadouts = new("DropDownControl"):DropDownControl({"LEFT",self.controls.loadoutsLabel,"RIGHT"}, {4, 0, 190, 20}, {}, function(index, value) if value == "^7^7Loadouts:" or value == "^7^7-----" then self.controls.buildLoadouts:SetSel(1) return @@ -417,6 +471,7 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.buildLoadouts:SelByValue(value) end) + self.controls.buildLoadouts.clampDrop = true if buildName == "~~temp~~" then -- Remove temporary build file @@ -434,51 +489,51 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.extraSaveStats = displayStatsModule.extraSaveStats -- Controls: Side bar - self.anchorSideBar = new("Control"):Control(nil, {4, 60, 0, 0}) + self.anchorSideBar = new("Control"):Control(nil, {6, 60, 0, 0}) self.anchorSideBar.y = function() - return buildNameConditional() and 60 or 36 + return buildNameConditional() and 64 or 40 end - self.controls.modeImport = new("ButtonControl"):ButtonControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 0, 134, 20}, "Import/Export Build", function() + self.controls.modeImport = new("ButtonControl"):ButtonControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 0, 137, 20}, "Import/Export Build", function() self.viewMode = "IMPORT" self.importTab:TryFetchCharacterList() end) self.controls.modeImport.locked = function() return self.viewMode == "IMPORT" end - self.controls.modeNotes = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeImport,"RIGHT"}, {4, 0, 58, 20}, "Notes", function() + self.controls.modeNotes = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeImport,"RIGHT"}, {4, 0, 60, 20}, "Notes", function() self.viewMode = "NOTES" end) self.controls.modeNotes.locked = function() return self.viewMode == "NOTES" end - self.controls.modeConfig = new("ButtonControl"):ButtonControl({"TOPRIGHT",self.anchorSideBar,"TOPLEFT"}, {300, 0, 100, 20}, "Configuration", function() + self.controls.modeConfig = new("ButtonControl"):ButtonControl({"TOPRIGHT",self.anchorSideBar,"TOPLEFT"}, {306, 0, 101, 20}, "Configuration", function() self.viewMode = "CONFIG" end) self.controls.modeConfig.locked = function() return self.viewMode == "CONFIG" end - self.controls.modeTree = new("ButtonControl"):ButtonControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 26, 72, 20}, "Tree", function() + self.controls.modeTree = new("ButtonControl"):ButtonControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 26, 73, 20}, "Tree", function() self.viewMode = "TREE" end) self.controls.modeTree.locked = function() return self.viewMode == "TREE" end - self.controls.modeSkills = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeTree,"RIGHT"}, {4, 0, 72, 20}, "Skills", function() + self.controls.modeSkills = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeTree,"RIGHT"}, {4, 0, 74, 20}, "Skills", function() self.viewMode = "SKILLS" end) self.controls.modeSkills.locked = function() return self.viewMode == "SKILLS" end - self.controls.modeItems = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeSkills,"RIGHT"}, {4, 0, 72, 20}, "Items", function() + self.controls.modeItems = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeSkills,"RIGHT"}, {4, 0, 74, 20}, "Items", function() self.viewMode = "ITEMS" end) self.controls.modeItems.locked = function() return self.viewMode == "ITEMS" end - self.controls.modeCalcs = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeItems,"RIGHT"}, {4, 0, 72, 20}, "Calcs", function() + self.controls.modeCalcs = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeItems,"RIGHT"}, {4, 0, 73, 20}, "Calcs", function() self.viewMode = "CALCS" end) self.controls.modeCalcs.locked = function() return self.viewMode == "CALCS" end - self.controls.modeParty = new("ButtonControl"):ButtonControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 52, 72, 20}, "Party", function() + self.controls.modeParty = new("ButtonControl"):ButtonControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 52, 73, 20}, "Party", function() self.viewMode = "PARTY" end) self.controls.modeParty.locked = function() return self.viewMode == "PARTY" end - self.controls.modeCompare = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeParty,"RIGHT"}, {4, 0, 72, 20}, "Compare", function() + self.controls.modeCompare = new("ButtonControl"):ButtonControl({"LEFT",self.controls.modeParty,"RIGHT"}, {4, 0, 74, 20}, "Compare", function() self.viewMode = "COMPARE" end) self.controls.modeCompare.locked = function() return self.viewMode == "COMPARE" end -- Skills - self.controls.mainSkillLabel = new("LabelControl"):LabelControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 80, 300, 16}, "^7Main Skill:") - self.controls.mainSocketGroup = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSkillLabel = new("LabelControl"):LabelControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 80, 306, 16}, "^7Main Skill:") + self.controls.mainSocketGroup = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, {0, 2, 306, 18}, nil, function(index, value) self.mainSocketGroup = index self.modFlag = true self.buildFlag = true @@ -490,25 +545,25 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.skillsTab:AddSocketGroupTooltip(tooltip, socketGroup) end end - self.controls.mainSkill = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSocketGroup,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSkill = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSocketGroup,"BOTTOMLEFT"}, {0, 4, 306, 18}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] mainSocketGroup.mainActiveSkill = index self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillPart = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkill,"BOTTOMLEFT",true}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSkillPart = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkill,"BOTTOMLEFT",true}, {0, 4, 306, 18}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillPart = index self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillStageCountLabel = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.mainSkillPart,"BOTTOMLEFT",true}, {0, 3, 0, 16}, "^7Stages:") { + self.controls.mainSkillStageCountLabel = new("LabelControl"):LabelControl({"TOPLEFT",self.controls.mainSkillPart,"BOTTOMLEFT",true}, {0, 5, 0, 16}, "^7Stages:") { shown = function() return self.controls.mainSkillStageCount:IsShown() end, } - self.controls.mainSkillStageCount = new("EditControl"):EditControl({"LEFT",self.controls.mainSkillStageCountLabel,"RIGHT",true}, {2, 0, 60, 18}, nil, nil, "%D", nil, function(buf) + self.controls.mainSkillStageCount = new("EditControl"):EditControl({"LEFT",self.controls.mainSkillStageCountLabel,"RIGHT",true}, {4, 0, 60, 18}, nil, nil, "%D", nil, function(buf) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillStageCount = tonumber(buf) @@ -527,7 +582,7 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillMinion = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillMineCountLabel,"BOTTOMLEFT",true}, {0, 3, 178, 18}, nil, function(index, value) + self.controls.mainSkillMinion = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillMineCountLabel,"BOTTOMLEFT",true}, {0, 3, 184, 18}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance if value.itemSetId then @@ -559,19 +614,33 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.mainSkillMinionLibrary = new("ButtonControl"):ButtonControl({"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 18}, "Manage Spectres...", function() self:OpenSpectreLibrary() end) - self.controls.mainSkillMinionSkill = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillMinion,"BOTTOMLEFT",true}, {0, 2, 200, 16}, nil, function(index, value) + self.controls.mainSkillMinionSkill = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillMinion,"BOTTOMLEFT",true}, {8, 2, 176, 18}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillMinionSkill = index self.modFlag = true self.buildFlag = true end) - self.controls.statBoxAnchor = new("Control"):Control({"TOPLEFT",self.controls.mainSkillMinionSkill,"BOTTOMLEFT",true}, {0, 2, 0, 0}) - self.controls.statBox = new("TextListControl"):TextListControl({"TOPLEFT",self.controls.statBoxAnchor,"BOTTOMLEFT"}, {0, 2, 300, 0}, {{x=170,align="RIGHT_X"},{x=174,align="LEFT"}}) + self.controls.statBoxAnchor = new("Control"):Control(nil, {0, 0, 0, 0}) + function self.controls.statBoxAnchor.GetPos() + local controls = self.controls + local lastControl = controls.mainSkillMinionSkill:IsShown() and controls.mainSkillMinionSkill + or controls.mainSkillMinion:IsShown() and controls.mainSkillMinion + or controls.mainSkillMineCount:IsShown() and controls.mainSkillMineCount + or controls.mainSkillStageCount:IsShown() and controls.mainSkillStageCount + or controls.mainSkillPart:IsShown() and controls.mainSkillPart + or controls.mainSkill:IsShown() and controls.mainSkill + or controls.mainSocketGroup + local x = controls.mainSocketGroup:GetPos() + local _, y = lastControl:GetPos() + local _, height = lastControl:GetSize() + return x, y + height + (lastControl == controls.mainSkillStageCount and 5 or 4) + end + self.controls.statBox = new("TextListControl"):TextListControl({"TOPLEFT",self.controls.statBoxAnchor,"BOTTOMLEFT"}, {0, 0, 306, 0}, {{x=176,align="RIGHT_X"},{x=180,align="LEFT"}}) self.controls.statBox.height = function(control) local x, y = control:GetPos() local warnHeight = main.showWarnings and #self.controls.warnings.lines > 0 and 18 or 0 - return main.screenH - main.mainBarHeight - 4 - y - warnHeight + return main.screenH - main.mainBarHeight - 6 - y - warnHeight end function self.controls.statBox.onClick(hoveredLine) self:SetDisplayStat(hoveredLine, true) @@ -927,12 +996,19 @@ function buildMode:EstimatePlayerProgress() if SecondaryAscUsed > secondaryAscMax then InsertIfNew(self.controls.warnings.lines, "You have too many secondary ascendancy points allocated") end self.Act = level < 90 and act <= 10 and act or "Endgame" - self.controls.pointDisplay.str = string.format("%s%3d / %3d %s%d / %d", + self.controls.pointDisplay.wideStr = string.format("%s%3d / %3d %s%d / %d", PointsUsed > usedMax and colorCodes.NEGATIVE or "^7", PointsUsed, usedMax, AscUsed > ascMax and colorCodes.NEGATIVE or "^7", AscUsed, ascMax ) + self.controls.pointDisplay.narrowStr = string.format("%s%d/%d %s%d/%d", + PointsUsed > usedMax and colorCodes.NEGATIVE or "^7", + PointsUsed, usedMax, + AscUsed > ascMax and colorCodes.NEGATIVE or "^7", + AscUsed, ascMax + ) + self.controls.pointDisplay.str = isNarrowViewport() and self.controls.pointDisplay.narrowStr or self.controls.pointDisplay.wideStr self.controls.pointDisplay.req = string.format( "Required Level: %d\nEstimated Progress:\nAct: %s\nQuestpoints: %d\nExtra Skillpoints: %d%s", level, self.Act, acts[act].questPoints, actExtra(act, extra), labSuggest @@ -968,6 +1044,11 @@ function buildMode:GetArgs() return self.dbFileName, self.buildName end +function buildMode:NewBuild() + main.modes.LIST.subPath = self.dbFileSubPath + main:SetMode("BUILD", false, "Unnamed build") +end + function buildMode:CloseBuild() main:SetWindowTitleSubtext() main:SetMode("LIST", self.dbFileName and self.buildName, self.dbFileSubPath) @@ -1157,6 +1238,50 @@ function buildMode:UpdateSecondaryAscendancyDropdown(forceListUpdate) secondaryDrop.enabled = self.spec ~= nil and (self.secondaryAscendDropEntryCount or 1) > 1 end +function buildMode:LayoutTopBar() + if self.controls.pointDisplay.wideStr then + self.controls.pointDisplay.str = isNarrowViewport() and self.controls.pointDisplay.narrowStr or self.controls.pointDisplay.wideStr + end + local pointLabelWidth = self.controls.pointDisplay:GetPos() - 4 - self.anchorTopBarLeft:GetPos() + self.controls.pointLabel.label = fitTopBarLabel("Passives:", "Pts:", m_max(0, pointLabelWidth)) + local fullWidth, shortWidth = 0, 0 + for _, entry in ipairs(topBarLabels) do + self.controls[entry[1]].label = "" + self.controls[entry[1]].x = 40 + fullWidth = fullWidth + DrawStringWidth(16, "VAR", entry[2]) + shortWidth = shortWidth + DrawStringWidth(16, "VAR", entry[3]) + end + local flexibleWidth = 0 + for _, entry in ipairs(topBarFlexibleControls) do + local control = self.controls[entry[1]] + control.width = control.rectStart[3] + flexibleWidth = flexibleWidth + control.width - entry[2] + end + local loadouts = self.controls.buildLoadouts + local available = main.screenW - 6 - loadouts:GetPos() - loadouts:GetSize() + -- Use section spacing before narrowing labels or dropdowns; keep Level readable. + local gapReduction = m_min(32, m_max(0, (fullWidth - available) / 2)) + for _, entry in ipairs(topBarLabels) do + self.controls[entry[1]].x = 40 - gapReduction + end + available = main.screenW - 6 - loadouts:GetPos() - loadouts:GetSize() + local labelIndex = available >= fullWidth and 2 or 3 + if available < shortWidth then + local shrink = m_min(1, (shortWidth - available) / flexibleWidth) + for _, entry in ipairs(topBarFlexibleControls) do + local control = self.controls[entry[1]] + control.width = m_floor(control.width - (control.width - entry[2]) * shrink) + end + available = main.screenW - 6 - loadouts:GetPos() - loadouts:GetSize() + end + local labelScale = m_min(1, m_max(0, available) / (labelIndex == 2 and fullWidth or shortWidth)) + for _, entry in ipairs(topBarLabels) do + local label = entry[labelIndex] + local width = DrawStringWidth(16, "VAR", label) * labelScale + self.controls[entry[1]].label = fitTopBarLabel(label, entry[3], width) + end +end + function buildMode:OnFrame(inputEvents) -- Stop at drawing the background if the loaded build needs to be converted if not self.targetVersion then @@ -1247,6 +1372,7 @@ function buildMode:OnFrame(inputEvents) end end + self:LayoutTopBar() self:ProcessControlsInput(inputEvents, main.viewPort) self.controls.classDrop:SelByValue(self.spec.curClassId, "classId") @@ -1290,12 +1416,11 @@ function buildMode:OnFrame(inputEvents) -- Update contents of main skill dropdowns self:RefreshSkillSelectControls(self.controls, self.mainSocketGroup, "") -- Draw contents of current tab - local sideBarWidth = 312 local tabViewPort = { x = sideBarWidth, - y = 32, + y = 34, width = main.screenW - sideBarWidth, - height = main.screenH - 32 + height = main.screenH - 34 } if self.viewMode == "IMPORT" then self.importTab:Draw(tabViewPort, inputEvents) @@ -1330,16 +1455,15 @@ function buildMode:OnFrame(inputEvents) -- Draw top bar background SetDrawColor(0.2, 0.2, 0.2) - DrawImage(nil, 0, 0, main.screenW, 28) + DrawImage(nil, 0, 0, main.screenW, 30) SetDrawColor(0.85, 0.85, 0.85) - DrawImage(nil, 0, 28, main.screenW, 4) - DrawImage(nil, main.screenW/2 - 2, 0, 4, 28) + DrawImage(nil, 0, 30, main.screenW, 4) -- Draw side bar background SetDrawColor(0.1, 0.1, 0.1) - DrawImage(nil, 0, 32, sideBarWidth - 4, main.screenH - 32) + DrawImage(nil, 0, 34, sideBarWidth - 4, main.screenH - 34) SetDrawColor(0.85, 0.85, 0.85) - DrawImage(nil, sideBarWidth - 4, 32, 4, main.screenH - 32) + DrawImage(nil, sideBarWidth - 4, 34, 4, main.screenH - 34) local hovered = self.controls.statBox and self.controls.statBox.hoveredLine @@ -1421,6 +1545,7 @@ end function buildMode:OpenSavePopup(mode) local modeDesc = { ["LIST"] = "now?", + ["NEW"] = "first?", ["EXIT"] = "before exiting?", ["UPDATE"] = "before updating?", } @@ -1435,6 +1560,8 @@ function buildMode:OpenSavePopup(mode) main:ClosePopup() if mode == "LIST" then self:CloseBuild() + elseif mode == "NEW" then + self:NewBuild() elseif mode == "EXIT" then Exit() elseif mode == "UPDATE" then @@ -2325,6 +2452,8 @@ function buildMode:SaveDBFile() if action == "LIST" then self:CloseBuild() + elseif action == "NEW" then + self:NewBuild() elseif action == "EXIT" then Exit() elseif action == "UPDATE" then diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index e6b2284aa57..8dcf0865571 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -1820,7 +1820,7 @@ return { { label = "Cooldown Recovery", { format = "{0:mod:1}%", { modName = "TinctureCooldownRecovery", modType = "INC", actor = "player"}, }, }, } } } }, -{ 1, "Rage", 3, colorCodes.RAGE, {{ defaultCollapsed = true, label = "Rage", data = { +{ 1, "Rage", 4, colorCodes.RAGE, {{ defaultCollapsed = false, label = "Rage", data = { extra = "{0:output:Rage} ({1:output:RageEffect})", { label = "Total", { format = "{0:output:Rage}", }, }, { label = "Rage Effect", { format = "{1:output:RageEffect}", { modName = "RageEffect" }, }, }, @@ -1836,7 +1836,7 @@ return { { label = "Inherent Rage Loss", { format = "{1:output:InherentRageLoss} /s", { modName = "InherentRageLoss" }, { modName = { "InherentRageLossIsPrevented" } }, }, }, } } } }, -{ 1, "Charges", 3, colorCodes.NORMAL, {{ defaultCollapsed = true, label = "Charges", data = { +{ 1, "Charges", 4, colorCodes.NORMAL, {{ defaultCollapsed = false, label = "Charges", data = { extra = colorCodes.RAGE.."{0:output:EnduranceCharges}^7, "..colorCodes.EVASION.."{0:output:FrenzyCharges}^7, "..colorCodes.MANA.."{0:output:PowerCharges}",} }, { defaultCollapsed = true, label = "Endurance", haveOutput="UseEnduranceCharges", data = { extra = colorCodes.RAGE.."{0:output:EnduranceCharges} ^7/ "..colorCodes.RAGE.."{0:output:EnduranceChargesMax}", @@ -1861,7 +1861,7 @@ return { } }, } }, -- misc defense -{ 1, "MiscDefences", 3, colorCodes.DEFENCE, {{ defaultCollapsed = false, label = "Other Defences", data = { +{ 1, "MiscDefences", 4, colorCodes.DEFENCE, {{ defaultCollapsed = false, label = "Other Defences", data = { { label = "Movement Speed", { format = "x {2:output:EffectiveMovementSpeedMod}", { breakdown = "EffectiveMovementSpeedMod" }, { modName = { "MovementSpeed", "MovementSpeedEqualHighestLinkedPlayers" } }, }, }, { label = "Effect of Elusive", haveOutput = "ElusiveEffectMod", { format = "{0:output:ElusiveEffectMod}%", { breakdown = "ElusiveEffectMod" }, { modName = { "ElusiveEffect", "BuffEffectOnSelf", "NightbladeSupportedElusiveEffect" }, }, } }, { label = "Light Radius Mod", { format = "x {2:output:LightRadiusMod}", { breakdown = "LightRadiusMod" }, { modName = "LightRadius" }, }, }, @@ -1887,7 +1887,7 @@ return { { breakdown = "BlockDuration" }, { modName = { "StunDuration", "StunRecovery", "BlockRecovery" }, }, }, }, -} }, { defaultCollapsed = true, label = "Other Avoidance", data = { +} }, { defaultCollapsed = false, label = "Other Avoidance", data = { { label = "Blind Avoid Ch.", haveOutput = "BlindAvoidChance", { format = "{0:output:BlindAvoidChance}%", { modName = { "AvoidBlind", "BlindImmune" } }, }, }, { label = "Shock Avoid Ch.", haveOutput = "ShockAvoidChance", { format = "{0:output:ShockAvoidChance}%", { modName = { "AvoidShock", "AvoidElementalAilments", "AvoidAilments", "ShockImmune", "ElementalAilmentImmune" } }, }, }, { label = "Freeze Avoid Ch.", haveOutput = "FreezeAvoidChance", { format = "{0:output:FreezeAvoidChance}%", { modName = { "AvoidFreeze", "AvoidElementalAilments", "AvoidAilments", "AvoidShockAppliesToElementalAilments", "FreezeImmune", "ElementalAilmentImmune" } }, }, }, @@ -1907,7 +1907,7 @@ return { { label = "Hinder Immune", haveOutput = "HinderImmunity", { format = "True", { modName = "HinderImmune" }, }, }, { label = "Knockback Immune", haveOutput = "KnockbackImmunity", { format = "True", { modName = "KnockbackImmune" }, }, }, { label = "Blind Duration", haveOutput = "SelfBlindDuration", { format = "{0:output:SelfBlindDuration}%", { modName = "SelfBlindDuration" }, }, }, -} }, { defaultCollapsed = true, label = "Other Ailment Defences", data = { +} }, { defaultCollapsed = false, label = "Other Ailment Defences", data = { { label = "Freeze Duration", { format = "{1:output:SelfFreezeDuration}%", { modName = { "SelfFreezeDuration", "SelfDebuffExpirationRate", "SelfFreezeDebuffExpirationRate", "SelfAilmentDuration", "SelfElementalAilmentDuration", "SelfIgniteDurationToElementalAilments" }, }, }, }, { label = "Chill Duration", { format = "{1:output:SelfChillDuration}%", { modName = { "SelfChillDuration", "SelfDebuffExpirationRate", "SelfChillDebuffExpirationRate", "SelfAilmentDuration", "SelfElementalAilmentDuration", "SelfIgniteDurationToElementalAilments" }, }, }, }, { label = "Shock Duration", { format = "{1:output:SelfShockDuration}%", { modName = { "SelfShockDuration", "SelfDebuffExpirationRate", "SelfShockDebuffExpirationRate", "SelfAilmentDuration", "SelfElementalAilmentDuration", "SelfIgniteDurationToElementalAilments" }, }, }, }, diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 70301168360..13242a8c3e9 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -1893,19 +1893,7 @@ function calcs.initEnv(build, mode, override, specEnv) end if env.mode == "MAIN" then - -- Create display label for the socket group if the user didn't specify one - if group.label and group.label:match("%S") then - group.displayLabel = group.label - else - group.displayLabel = nil - for _, gemInstance in ipairs(group.gemList) do - local grantedEffect = gemInstance.gemData and gemInstance.gemData.grantedEffect or gemInstance.grantedEffect - if grantedEffect and not grantedEffect.support and gemInstance.enabled then - group.displayLabel = (group.displayLabel and group.displayLabel..", " or "") .. grantedEffect.name - end - end - group.displayLabel = group.displayLabel or "" - end + build.skillsTab:UpdateGroupLabel(group) -- Save the active skill list for display in the socket group tooltip group.displaySkillList = socketGroupSkillList diff --git a/src/Modules/ConfigModBrowser.lua b/src/Modules/ConfigModBrowser.lua index 2408963bb6f..0b0cde3bdf8 100644 --- a/src/Modules/ConfigModBrowser.lua +++ b/src/Modules/ConfigModBrowser.lua @@ -264,10 +264,8 @@ function M.OpenAddModPopup(configTab, blockData) end updateDisplayList(controls, displayList, supportedList) - local helpSize = 24 - controls.whatDoesItDo = new("ButtonControl"):ButtonControl({ "BOTTOM", nil, "BOTTOM" }, { 0, -10, helpSize, helpSize }, "?", function() end) + controls.whatDoesItDo = new("ButtonControl"):ButtonControl({ "BOTTOMRIGHT", nil, "BOTTOMRIGHT" }, { -8, -8, 24, 24 }, "?", function() end) controls.whatDoesItDo.forceTooltip = true - controls.whatDoesItDo.tooltipText = table.concat( main:WrapString( [[This menu currently contains supported mod lines from tree nodes and item modifiers only. @@ -277,7 +275,7 @@ This menu is not a representation of what PoB can parse, and this is only a limi A mod being supported does not necessarily mean that it will be included in calculations, and only means that the mod parser accepts it.]], 16, 270), "\n") - controls.save = new("ButtonControl"):ButtonControl({ "BOTTOMRIGHT", controls.whatDoesItDo, "TOP" }, { -2, -4, 80, 20 }, "Add", function() + controls.save = new("ButtonControl"):ButtonControl({ "BOTTOMRIGHT", nil, "BOTTOM" }, { -4, -8, 80, 20 }, "Add", function() local selIndex = controls.listControl.selIndex or 1 local selected = displayList[selIndex] if selected and selected.text ~= NO_MATCH_TEXT then @@ -299,13 +297,11 @@ A mod being supported does not necessarily mean that it will be included in calc end - controls.close = new("ButtonControl"):ButtonControl({ "BOTTOMLEFT", controls.whatDoesItDo, "TOP" }, { 2, -4, 80, 20 }, "Cancel", function() + controls.close = new("ButtonControl"):ButtonControl({ "BOTTOMLEFT", nil, "BOTTOM" }, { 4, -8, 80, 20 }, "Cancel", function() main:ClosePopup() end) - local popupHeight = controls.search.y + controls.search.height + helpSize - controls.whatDoesItDo.y - controls.close.y + controls.close.height + 8 - - main:OpenPopup(720, popupHeight, "Mod Browser", controls, "save", "search", "close") + main:OpenPopup(720, 566, "Mod Browser", controls, "save", "search", "close") end return M diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index 26afb5f4dfb..3e52eb2c9bc 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -635,11 +635,11 @@ data.enchantmentSource = { { name = "RUNESMITH", label = "Runecraft Bench" }, { name = "HEIST", label = "Heist" }, { name = "HARVEST", label = "Harvest" }, - { name = "DEDICATION", label = "Dedication to the Goddess" }, - { name = "ENDGAME", label = "Eternal Labyrinth" }, - { name = "MERCILESS", label = "Merciless Labyrinth" }, - { name = "CRUEL", label = "Cruel Labyrinth" }, - { name = "NORMAL", label = "Normal Labyrinth" }, + { name = "DEDICATION", label = "Dedication to the Goddess (Legacy)" }, + { name = "ENDGAME", label = "Eternal Labyrinth (Legacy)" }, + { name = "MERCILESS", label = "Merciless Labyrinth (Legacy)" }, + { name = "CRUEL", label = "Cruel Labyrinth (Legacy)" }, + { name = "NORMAL", label = "Normal Labyrinth (Legacy)" }, } -- Stat descriptions diff --git a/src/Modules/ItemSocketControls.lua b/src/Modules/ItemSocketControls.lua new file mode 100644 index 00000000000..5c5d7c47be1 --- /dev/null +++ b/src/Modules/ItemSocketControls.lua @@ -0,0 +1,162 @@ +-- Path of Building +-- +-- Shared item socket editing controls for the Items and Skills tabs. +-- The owning tab supplies the item and commits changes to its own state. +local t_insert = table.insert +local m_min = math.min +local m_max = math.max +local m_ceil = math.ceil + +local socketDropList = { + { label = colorCodes.STRENGTH.."R", color = "R" }, + { label = colorCodes.DEXTERITY.."G", color = "G" }, + { label = colorCodes.INTELLIGENCE.."B", color = "B" }, + { label = colorCodes.SCION.."W", color = "W" } +} + +local socketColorDropList = { + { label = "" }, + { label = colorCodes.SCION.."W", color = "W" }, + { label = colorCodes.INTELLIGENCE.."B", color = "B" }, + { label = colorCodes.DEXTERITY.."G", color = "G" }, + { label = colorCodes.STRENGTH.."R", color = "R" } +} + +local socketControls = { } + +function socketControls.create(controls, anchor, getItem, onChange, includeBulkUpdate) + local function editItem() + local item = getItem() + local previousSockets = item.sockets + -- Item undo snapshots share nested tables until the next edit. + item.sockets = copyTable(item.sockets) + return item, previousSockets + end + local function commit(previousSockets) + onChange(previousSockets) + socketControls.update(controls, getItem()) + end + for i = 1, 6 do + local drop = new("DropDownControl"):DropDownControl({"LEFT",anchor,"RIGHT"}, {6 + (i-1) * 48, 0, 32, 20}, socketDropList, function(index, value) + local item, previousSockets = editItem() + item.sockets[i].color = value.color + commit(previousSockets) + end) + drop.arrowSize = 6 + drop.shown = function() + local item = getItem() + return item.selectableSocketCount >= i and item.sockets[i] and item.sockets[i].color ~= "A" + end + controls["displayItemSocket"..i] = drop + if i < 6 then + local link = new("CheckBoxControl"):CheckBoxControl({"LEFT",drop,"RIGHT"}, {0, 0, 16}, nil, function(state) + local item, previousSockets = editItem() + if state and item.sockets[i].group ~= item.sockets[i+1].group then + for s = i + 1, #item.sockets do + item.sockets[s].group = item.sockets[s].group - 1 + end + elseif not state and item.sockets[i].group == item.sockets[i+1].group then + for s = i + 1, #item.sockets do + item.sockets[s].group = item.sockets[s].group + 1 + end + end + commit(previousSockets) + end) + link.height = 20 + link.linkStyle = true + link.shown = function() + local item = getItem() + return item.selectableSocketCount > i and item.sockets[i+1] and item.sockets[i+1].color ~= "A" + end + controls["displayItemLink"..i] = link + end + end + controls.displayItemAddSocket = new("ButtonControl"):ButtonControl({"LEFT",anchor,"RIGHT"}, {function() return (#getItem().sockets - getItem().abyssalSocketCount) * 48 - 4 end, 0, 20, 20}, "+", function() + local item, previousSockets = editItem() + local insertIndex = #item.sockets - item.abyssalSocketCount + 1 + t_insert(item.sockets, insertIndex, { + color = item.defaultSocketColor, + group = (item.sockets[insertIndex - 1] and item.sockets[insertIndex - 1].group or 0) + 1 + }) + for s = insertIndex + 1, #item.sockets do + item.sockets[s].group = item.sockets[s].group + 1 + end + commit(previousSockets) + end) + controls.displayItemAddSocket.shown = function() + return #getItem().sockets < getItem().selectableSocketCount + getItem().abyssalSocketCount + end + + if includeBulkUpdate == false then + return + end + + controls.displayItemSetColorsLabel = new("LabelControl"):LabelControl({"LEFT",anchor,"RIGHT"}, {function() + local socketCount = #getItem().sockets - getItem().abyssalSocketCount + return socketCount * 48 + (controls.displayItemAddSocket:IsShown() and 28 or 2) + end, 0, 0, 16}, "^7Bulk Update:") + controls.displayItemSetColorsLabel.shown = function() return anchor:IsShown() and getItem().selectableSocketCount > 0 end + controls.displayItemSetColors = new("DropDownControl"):DropDownControl({"LEFT",controls.displayItemSetColorsLabel,"RIGHT"}, {6, 0, m_ceil(DrawStringWidth(16, "VAR", "W")) + 18, 20}, socketColorDropList, function(index, value) + if not value.color then + return + end + local item, previousSockets = editItem() + for i, socket in ipairs(item.sockets) do + if i <= item.selectableSocketCount and socket.color ~= "A" then + socket.color = value.color + end + end + commit(previousSockets) + controls.displayItemSetColors:SelByValue("", "label") + end) + controls.displayItemSetColors.arrowSize = 6 + controls.displayItemSetColors.shown = controls.displayItemSetColorsLabel.shown + controls.displayItemSetLinks = new("DropDownControl"):DropDownControl({"LEFT",controls.displayItemSetColors,"RIGHT"}, {6, 0, 18, 20}, { "" }, function(index) + if index == 1 then + return + end + local item, previousSockets = editItem() + local linkedSockets = m_min(index - 1, #item.sockets - item.abyssalSocketCount) + for i, socket in ipairs(item.sockets) do + socket.group = i <= linkedSockets and 1 or i - linkedSockets + 1 + end + commit(previousSockets) + controls.displayItemSetLinks:SelByValue("") + end) + controls.displayItemSetLinks.arrowSize = 6 + controls.displayItemSetLinks.shown = controls.displayItemSetColorsLabel.shown + controls.displayItemSetLinks.tooltipText = "Link the first selected number of existing sockets; leave the remaining sockets unlinked." + +end + +function socketControls.update(controls, item) + local sockets = item.sockets + for i = 1, #sockets - item.abyssalSocketCount do + controls["displayItemSocket"..i]:SelByValue(sockets[i].color, "color") + if i > 1 then + controls["displayItemLink"..(i-1)].state = sockets[i].group == sockets[i-1].group + end + end + local links = controls.displayItemSetLinks + if not links then + return + end + local socketCount = #sockets - item.abyssalSocketCount + if #links.list ~= socketCount + 1 then + local linkList = { "" } + local linkWidth = 0 + for count = 1, socketCount do + local label = count .. "L" + t_insert(linkList, label) + linkWidth = m_max(linkWidth, DrawStringWidth(16, "VAR", label)) + end + if links.selIndex > #linkList then + links:SetSel(1, true) + end + links.width = m_ceil(linkWidth) + 18 + links:SetList(linkList) + links:UpdateSearch() + end +end + +return socketControls diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index 294fb64c24b..ba137678649 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -221,7 +221,7 @@ function main:Init() self.defaultItemAffixQuality = saved end - self.anchorMain = new("Control"):Control(nil, {4, 0, 0, 0}) + self.anchorMain = new("Control"):Control(nil, {6, 0, 0, 0}) self.anchorMain.y = function() return self.screenH - 4 end @@ -249,11 +249,11 @@ function main:Init() self.controls.checkUpdate.enabled = function() return not launch.updateCheckRunning end - self.controls.forkLabel = new("LabelControl"):LabelControl({"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, {148, -26, 0, 16}, "") + self.controls.forkLabel = new("LabelControl"):LabelControl({"BOTTOMRIGHT",self.anchorMain,"BOTTOMLEFT"}, {306, -26, 0, 16}, "") self.controls.forkLabel.label = function() return "^8PoB Community Fork" end - self.controls.versionLabel = new("LabelControl"):LabelControl({"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, {148, -2, 0, 16}, "") + self.controls.versionLabel = new("LabelControl"):LabelControl({"BOTTOMRIGHT",self.anchorMain,"BOTTOMLEFT"}, {306, -2, 0, 16}, "") self.controls.versionLabel.label = function() return "^8" .. (launch.versionBranch == "beta" and "Beta: " or "Version: ") .. launch.versionNumber .. (launch.versionBranch == "dev" and " (Dev)" or "") end @@ -429,9 +429,9 @@ function main:OnFrame() -- Draw main controls SetDrawColor(0.85, 0.85, 0.85) - DrawImage(nil, 0, self.screenH - 58, 312, 58) + DrawImage(nil, 0, self.screenH - 58, 322, 58) SetDrawColor(0.1, 0.1, 0.1) - DrawImage(nil, 0, self.screenH - 54, 308, 54) + DrawImage(nil, 0, self.screenH - 54, 318, 55) self:DrawControls(self.viewPort) if self.popups[1] then From 73fc797387473bb2da3a37f8eabed0fb6523f60a Mon Sep 17 00:00:00 2001 From: AdamZ Date: Mon, 21 Sep 2026 20:09:29 -0700 Subject: [PATCH 2/2] Adjust configuration and item control spacing and alignment --- src/Classes/ConfigTab.lua | 8 ++++---- src/Classes/ItemsTab.lua | 13 +++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 8750b093957..090985ab6ff 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -306,14 +306,14 @@ function ConfigTabClass:ConfigTab(build) else local control if varData.type == "check" then - control = new("CheckBoxControl"):CheckBoxControl({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 18}, varData.label, function(state) + control = new("CheckBoxControl"):CheckBoxControl({"TOPLEFT",lastSection,"TOPLEFT"}, {230, 0, 18}, varData.label, function(state) self.configSets[self.activeConfigSetId].input[varData.var] = state self:AddUndoState() self:BuildModList() self.build.buildFlag = true end) elseif varData.type == "count" or varData.type == "integer" or varData.type == "countAllowZero" or varData.type == "float" then - control = new("EditControl"):EditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 90, 18}, "", nil, ((varData.type == "integer" or varData.type == "countAllowZero") and "^%-%d") or (varData.type == "float" and "^%d.") or "%D", 10, function(buf, placeholder) + control = new("EditControl"):EditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {230, 0, 114, 18}, "", nil, ((varData.type == "integer" or varData.type == "countAllowZero") and "^%-%d") or (varData.type == "float" and "^%d.") or "%D", 10, function(buf, placeholder) if placeholder then self.configSets[self.activeConfigSetId].placeholder[varData.var] = tonumber(buf) else @@ -324,7 +324,7 @@ function ConfigTabClass:ConfigTab(build) self.build.buildFlag = true end) elseif varData.type == "list" then - control = new("DropDownControl"):DropDownControl({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 118, 18}, varData.list, function(index, value) + control = new("DropDownControl"):DropDownControl({"TOPLEFT",lastSection,"TOPLEFT"}, {230, 0, 142, 18}, varData.list, function(index, value) self.configSets[self.activeConfigSetId].input[varData.var] = value.val self:AddUndoState() self:BuildModList() @@ -353,7 +353,7 @@ function ConfigTabClass:ConfigTab(build) self.build.buildFlag = true end, 16) else - control = new("Control"):Control({"TOPLEFT",lastSection,"TOPLEFT"}, {254, 0, 16, 16}) + control = new("Control"):Control({"TOPLEFT",lastSection,"TOPLEFT"}, {230, 0, 16, 16}) end if varData.inactiveText then diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 75d8af8bdaf..7649e17228a 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -604,7 +604,7 @@ put it in the second.]]) return self.displayItem and self.displayItem.canBeInfluenced and 28 or 0 end}) -- Align the first influence dropdown with the right edge of the quality field below. - local influenceWidth = self.controls.displayItemSocketsLabel:GetSize() + 6 + 60 + local influenceWidth = self.controls.displayItemSocketsLabel:GetSize() + 6 + 62 local influenceTipText = table.concat(main:WrapString("Selecting an influence here will also allow the modifier dropdowns to contain influenced mods.", 16, 140), "\n") self.controls.displayItemInfluence = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.displayItemSectionInfluence,"TOPRIGHT"}, {0, 0, influenceWidth, 20}, influenceDisplayList1, function(index, value) local otherIndex = self.controls.displayItemInfluence2.selIndex @@ -633,7 +633,7 @@ put it in the second.]]) return self.displayItem and (self.displayItem.quality ~= nil or isCatalystEligible(self.displayItem)) end - self.controls.displayItemQualityEdit = new("EditControl"):EditControl({"LEFT",self.controls.displayItemQuality,"RIGHT"}, {6, 0, 60, 20}, nil, nil, "%D", 2, function(buf) + self.controls.displayItemQualityEdit = new("EditControl"):EditControl({"LEFT",self.controls.displayItemQuality,"RIGHT"}, {6, 0, 62, 20}, nil, nil, "%D", 2, function(buf) setEditableItemQuality(self.displayItem, tonumber(buf)) if isCatalystEligible(self.displayItem) and self.displayItem.crafted then for i = 1, self.displayItem.affixLimit do @@ -656,7 +656,7 @@ put it in the second.]]) end end self.controls.displayItemCatalyst = new("DropDownControl"):DropDownControl({"LEFT",self.controls.displayItemQualityEdit,"RIGHT",true}, {8, 0, 250, 20}, - {"Catalyst","Abrasive (Attack)","Accelerating (Speed)","Dextral (Suffix)","Fertile (Life & Mana)","Imbued (Caster)","Intrinsic (Attribute)","Noxious (Physical & Chaos Damage)", + {"Catalyst","Abrasive (Attack)","Accelerating (Speed)","Dextral (Suffix)","Fertile (Life & Mana)","Imbued (Caster)","Intrinsic (Attribute)","Noxious (Phys & Chaos Damage)", "Prismatic (Resistance)","Sinistral (Prefix)","Tempering (Defense)","Turbulent (Elemental)","Unstable (Critical)"}, function(index, value) local quality = tonumber(self.controls.displayItemQualityEdit.buf) or 20 @@ -714,12 +714,13 @@ put it in the second.]]) self.controls.craftingSorting = new("DropDownControl"):DropDownControl({ "LEFT", self.controls.craftingSortingLabel, "RIGHT" }, { 6, 0, affixX + affixRangeWidth - self.controls.craftingSortingLabel:GetSize() - 6, 20 }, sortingOptions, function() self:UpdateAffixControls() end) + self.controls.displayItemCatalyst.width = self.controls.craftingSorting:GetSize() - self.controls.displayItemQualityEdit:GetSize() - 8 -- Section: Affix Selection local maxModCount = 9 self.controls.displayItemSectionAffix = new("Control"):Control({ "TOPLEFT", self.controls.craftingSortingLabel, "BOTTOMLEFT", true }, { 0, function() if self.controls.craftingSortingLabel.shown() then - return 8 + return 13 else return -16 end @@ -1110,7 +1111,7 @@ put it in the second.]]) function box:Draw(...) local x, y = self:GetPos() SetDrawColor(1, 1, 1) - DrawImage(foulbornIcon, x - 24, y, 20, 20) + DrawImage(foulbornIcon, x - 24, y - 1, 20, 20) return box:RealDraw(...) end @@ -1138,7 +1139,7 @@ put it in the second.]]) self.controls["displayItemStackedRangeSlider" .. i] = slider - self.controls["displayItemStackedRangeLine" .. i] = new("LabelControl"):LabelControl({ "LEFT", slider, "RIGHT", true }, { 4, -2, 350, labelFontSize }, function() + self.controls["displayItemStackedRangeLine" .. i] = new("LabelControl"):LabelControl({ "LEFT", slider, "RIGHT", true }, { 4, -1, 350, labelFontSize }, function() local modLine = self.displayItem.rangeLineList[i] if self.displayItem and modLine then local colour = modLine.mutated and colorCodes.MUTATED or "^7"