-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- --- Crafting Queue List UI Element Class. -- The element used to show the queue in the Crafting UI. It is a subclass of the @{ScrollingTable} class. -- @classmod CraftingQueueList local _, TSM = ... local L = TSM.Include("Locale").GetTable() local TempTable = TSM.Include("Util.TempTable") local Money = TSM.Include("Util.Money") local Theme = TSM.Include("Util.Theme") local ScriptWrapper = TSM.Include("Util.ScriptWrapper") local ItemInfo = TSM.Include("Service.ItemInfo") local Inventory = TSM.Include("Service.Inventory") local CraftingQueueList = TSM.Include("LibTSMClass").DefineClass("CraftingQueueList", TSM.UI.ScrollingTable) local UIElements = TSM.Include("UI.UIElements") UIElements.Register(CraftingQueueList) TSM.UI.CraftingQueueList = CraftingQueueList local private = { categoryOrder = {}, sortSelf = nil, sortProfitCache = {}, } local CATEGORY_SEP = "\001" -- ============================================================================ -- Public Class Methods -- ============================================================================ function CraftingQueueList.__init(self) self.__super:__init() self._collapsed = {} self._query = nil self._numCraftableCache = {} self._onRowMouseDownHandler = nil end function CraftingQueueList.Acquire(self) self._headerHidden = true self.__super:Acquire() self:SetSelectionDisabled(true) self:GetScrollingTableInfo() :NewColumn("name") :SetFont("ITEM_BODY3") :SetJustifyH("LEFT") :SetIconSize(12) :SetExpanderStateFunction(private.GetExpanderState) :SetIconFunction(private.GetItemIcon) :SetIconHoverEnabled(true) :SetIconClickHandler(private.OnItemIconClick) :SetTextFunction(private.GetItemText) :SetTooltipFunction(private.GetItemTooltip) :SetActionIconInfo(1, 12, private.GetDeleteIcon, true) :SetActionIconClickHandler(private.OnDeleteIconClick) :Commit() :NewColumn("qty") :SetAutoWidth() :SetFont("TABLE_TABLE1") :SetJustifyH("CENTER") :SetTextFunction(private.GetQty) :SetActionIconInfo(1, 12, private.GetEditIcon, true) :SetActionIconClickHandler(private.OnEditIconClick) :Commit() :Commit() end function CraftingQueueList.Release(self) self._onRowMouseDownHandler = nil wipe(self._numCraftableCache) wipe(self._collapsed) if self._query then self._query:Release() self._query = nil end for _, row in ipairs(self._rows) do ScriptWrapper.Clear(row._frame, "OnDoubleClick") ScriptWrapper.Clear(row._frame, "OnMouseDown") for _, button in pairs(row._buttons) do ScriptWrapper.Clear(button, "OnMouseDown") end end self.__super:Release() end --- Gets the data of the first row. -- @tparam CraftingMatList self The crafting queue list object -- @treturn CraftingQueueList The crafting queue list object function CraftingQueueList.GetFirstData(self) for _, data in ipairs(self._data) do if type(data) ~= "string" then return data end end end --- Registers a script handler. -- @tparam CraftingQueueList self The crafting queue list object -- @tparam string script The script to register for (supported scripts: `OnRowClick`, `OnRowMouseDown`) -- @tparam function handler The script handler which will be called with the crafting queue list object followed by any -- arguments to the script -- @treturn CraftingQueueList The crafting queue list object function CraftingQueueList.SetScript(self, script, handler) if script == "OnRowMouseDown" then self._onRowMouseDownHandler = handler else self.__super:SetScript(script, handler) end return self end --- Sets the @{DatabaseQuery} source for this list. -- This query is used to populate the entries in the crafting queue list. -- @tparam CraftingQueueList self The crafting queue list object -- @tparam DatabaseQuery query The query object -- @treturn CraftingQueueList The crafting queue list object function CraftingQueueList.SetQuery(self, query) if self._query then self._query:Release() end self._query = query self._query:SetUpdateCallback(private.QueryUpdateCallback, self) self:_UpdateData() return self end -- ============================================================================ -- Private Class Methods -- ============================================================================ function CraftingQueueList._GetTableRow(self, isHeader) local row = self.__super:_GetTableRow(isHeader) if not isHeader then ScriptWrapper.Set(row._frame, "OnMouseDown", private.RowOnMouseDown, row) ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row) for _, button in pairs(row._buttons) do ScriptWrapper.Set(button, "OnMouseDown", private.RowOnMouseDown, row) end end return row end function CraftingQueueList._UpdateData(self) wipe(self._data) if not self._query then return end local categories = TempTable.Acquire() wipe(self._numCraftableCache) wipe(private.sortProfitCache) for _, row in self._query:Iterator() do local rawCategory = strjoin(CATEGORY_SEP, row:GetFields("profession", "players")) local category = strlower(rawCategory) if not categories[category] then tinsert(categories, category) end categories[category] = rawCategory if not self._collapsed[rawCategory] then local spellId = row:GetField("spellId") self._numCraftableCache[row] = TSM.Crafting.ProfessionUtil.GetNumCraftableFromDB(spellId) private.sortProfitCache[spellId] = TSM.Crafting.Cost.GetProfitBySpellId(spellId) tinsert(self._data, row) end end sort(categories, private.CategorySortComparator) wipe(private.categoryOrder) for i, category in ipairs(categories) do private.categoryOrder[category] = i tinsert(self._data, categories[category]) end TempTable.Release(categories) private.sortSelf = self sort(self._data, private.DataSortComparator) private.sortSelf = nil end function CraftingQueueList._SetCollapsed(self, data, collapsed) self._collapsed[data] = collapsed or nil end function CraftingQueueList._HandleRowClick(self, data, mouseButton) if type(data) == "string" then self:_SetCollapsed(data, not self._collapsed[data]) self:UpdateData(true) else local currentRow for _, row in ipairs(self._rows) do if row:GetData() == data then currentRow = row break end end if currentRow._texts.qty:IsMouseOver(0, 0, 0, 12) then private.OnEditIconClick(self, data, 1) else self.__super:_HandleRowClick(data, mouseButton) end end end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.RowOnMouseDown(row, mouseButton) local data = row:GetData() if type(data) == "string" then return end local self = row._scrollingTable if self._onRowMouseDownHandler then self:_onRowMouseDownHandler(data, mouseButton) end end function private.RowOnDoubleClick(row, mouseButton) local self = row._scrollingTable self:_HandleRowClick(row:GetData(), mouseButton) end function private.GetExpanderState(self, data) if type(data) == "string" then return true, not self._collapsed[data], 0 else return false, false, 0 end end function private.GetItemIcon(self, data) if type(data) == "string" then return end local spellId = data:GetField("spellId") local itemString = TSM.Crafting.GetItemString(spellId) local texture, tooltip = nil, nil if itemString then texture = ItemInfo.GetTexture(itemString) tooltip = itemString else texture = select(3, TSM.Crafting.ProfessionUtil.GetResultInfo(spellId)) if TSM.Crafting.ProfessionState.IsClassicCrafting() then tooltip = "craft:"..(TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId) else tooltip = "enchant:"..spellId end return end return texture, tooltip end function private.OnItemIconClick(self, data, mouseButton) self:_HandleRowClick(data, mouseButton) end function private.GetItemText(self, data) if type(data) == "string" then local profession, players = strsplit(CATEGORY_SEP, data) local isValid = private.PlayersContains(players, UnitName("player")) and strlower(profession) == strlower(TSM.Crafting.ProfessionUtil.GetCurrentProfessionName() or "") local text = Theme.GetColor("INDICATOR"):ColorText(profession.." ("..players..")") if isValid then return text else return text.." "..TSM.UI.TexturePacks.GetTextureLink("iconPack.12x12/Attention") end else local spellId = data:GetField("spellId") local itemString = TSM.Crafting.GetItemString(spellId) return itemString and TSM.UI.GetColoredItemName(itemString) or GetSpellInfo(spellId) or "?" end end function private.GetItemTooltip(self, data) if type(data) == "string" then local profession, players = strsplit(CATEGORY_SEP, data) if not private.PlayersContains(players, UnitName("player")) then return L["You are not on one of the listed characters."] elseif strlower(profession) ~= strlower(TSM.Crafting.ProfessionUtil.GetCurrentProfessionName() or "") then return L["This profession is not open."] end return end local spellId = data:GetField("spellId") local numQueued = data:GetField("num") local itemString = TSM.Crafting.GetItemString(spellId) local name = itemString and TSM.UI.GetColoredItemName(itemString) or GetSpellInfo(spellId) or "?" local tooltipLines = TempTable.Acquire() tinsert(tooltipLines, name.." (x"..numQueued..")") local numResult = TSM.Crafting.GetNumResult(spellId) local profit = TSM.Crafting.Cost.GetProfitBySpellId(spellId) local profitStr = profit and Money.ToString(profit * numResult, Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED"):GetTextColorPrefix()) or "---" local totalProfitStr = profit and Money.ToString(profit * numResult * numQueued, Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED"):GetTextColorPrefix()) or "---" tinsert(tooltipLines, L["Profit (Total)"]..": "..profitStr.." ("..totalProfitStr..")") for _, matItemString, quantity in TSM.Crafting.MatIterator(spellId) do local numHave = Inventory.GetBagQuantity(matItemString) if not TSM.IsWowClassic() then numHave = numHave + Inventory.GetReagentBankQuantity(matItemString) + Inventory.GetBankQuantity(matItemString) end local numNeed = quantity * numQueued local color = Theme.GetFeedbackColor(numHave >= numNeed and "GREEN" or "RED") tinsert(tooltipLines, color:ColorText(numHave.."/"..numNeed).." - "..(ItemInfo.GetName(matItemString) or "?")) end if TSM.Crafting.ProfessionUtil.GetRemainingCooldown(spellId) then tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["On Cooldown"])) end return strjoin("\n", TempTable.UnpackAndRelease(tooltipLines)), true, true end function private.GetDeleteIcon(self, data, iconIndex) assert(iconIndex == 1) if type(data) == "string" then return false end return true, "iconPack.12x12/Close/Default", true end function private.OnDeleteIconClick(self, data, iconIndex) assert(iconIndex == 1 and type(data) ~= "string") TSM.Crafting.Queue.SetNum(data:GetField("spellId"), 0) end function private.GetEditIcon(self, data, iconIndex) assert(iconIndex == 1) if type(data) == "string" then return false end return true, "iconPack.12x12/Edit", true end function private.OnEditIconClick(self, data, iconIndex) assert(iconIndex == 1 and type(data) ~= "string") local currentRow = nil for _, row in ipairs(self._rows) do if row:GetData() == data then currentRow = row break end end local name = private.GetItemText(self, data) local texture, tooltip = private.GetItemIcon(self, data) local dialogFrame = UIElements.New("Frame", "qty") :SetLayout("HORIZONTAL") :AddAnchor("LEFT", currentRow._frame, Theme.GetColSpacing() / 2, 0) :AddAnchor("RIGHT", currentRow._frame, -Theme.GetColSpacing(), 0) :SetHeight(20) :SetContext(self) :SetBackgroundColor("PRIMARY_BG") :SetScript("OnHide", private.DialogOnHide) :AddChild(UIElements.New("Button", "icon") :SetSize(12, 12) :SetMargin(16, 4, 0, 0) :SetBackground(texture) :SetTooltip(tooltip) ) :AddChild(UIElements.New("Text", "name") :SetWidth("AUTO") :SetFont("ITEM_BODY3") :SetText(name) ) :AddChild(UIElements.New("Spacer", "spacer")) :AddChild(UIElements.New("Input", "input") :SetWidth(75) :SetBackgroundColor("ACTIVE_BG") :SetJustifyH("CENTER") :SetContext(currentRow:GetData():GetField("spellId")) :SetSubAddEnabled(true) :SetValidateFunc("NUMBER", "1:9999") :SetValue(currentRow:GetData():GetField("num")) :SetScript("OnFocusLost", private.QtyInputOnFocusLost) ) local baseFrame = self:GetBaseElement() baseFrame:ShowDialogFrame(dialogFrame) dialogFrame:GetElement("input"):SetFocused(true) end function private.DialogOnHide(frame) local input = frame:GetElement("input") TSM.Crafting.Queue.SetNum(input:GetContext(), tonumber(input:GetValue())) frame:GetContext():Draw() end function private.QtyInputOnFocusLost(input) input:GetBaseElement():HideDialog() end function private.GetQty(self, data) if type(data) == "string" then return "" end local numQueued = data:GetFields("num") local numCraftable = min(self._numCraftableCache[data], numQueued) local onCooldown = TSM.Crafting.ProfessionUtil.GetRemainingCooldown(data:GetField("spellId")) local color = Theme.GetFeedbackColor(((numCraftable == 0 or onCooldown) and "RED") or (numCraftable < numQueued and "YELLOW") or "GREEN") return color:ColorText(format("%s / %s", numCraftable, numQueued)) end function private.PlayersContains(players, player) players = strlower(players) player = strlower(player) return players == player or strmatch(players, "^"..player..",") or strmatch(players, ","..player..",") or strmatch(players, ","..player.."$") end function private.CategorySortComparator(a, b) local aProfession, aPlayers = strsplit(CATEGORY_SEP, a) local bProfession, bPlayers = strsplit(CATEGORY_SEP, b) if aProfession ~= bProfession then local currentProfession = TSM.Crafting.ProfessionUtil.GetCurrentProfessionName() currentProfession = strlower(currentProfession or "") if aProfession == currentProfession then return true elseif bProfession == currentProfession then return false else return aProfession < bProfession end end local playerName = UnitName("player") local aContainsPlayer = private.PlayersContains(aPlayers, playerName) local bContainsPlayer = private.PlayersContains(bPlayers, playerName) if aContainsPlayer and not bContainsPlayer then return true elseif bContainsPlayer and not aContainsPlayer then return false else return aPlayers < bPlayers end end function private.DataSortComparator(a, b) -- sort by category local aCategory, bCategory = nil, nil if type(a) == "string" and type(b) == "string" then return private.categoryOrder[strlower(a)] < private.categoryOrder[strlower(b)] elseif type(a) == "string" then aCategory = strlower(a) bCategory = strlower(strjoin(CATEGORY_SEP, b:GetFields("profession", "players"))) if aCategory == bCategory then return true end elseif type(b) == "string" then aCategory = strlower(strjoin(CATEGORY_SEP, a:GetFields("profession", "players"))) bCategory = strlower(b) if aCategory == bCategory then return false end else aCategory = strlower(strjoin(CATEGORY_SEP, a:GetFields("profession", "players"))) bCategory = strlower(strjoin(CATEGORY_SEP, b:GetFields("profession", "players"))) end if aCategory ~= bCategory then return private.categoryOrder[aCategory] < private.categoryOrder[bCategory] end -- sort spells within a category local aSpellId = a:GetField("spellId") local bSpellId = b:GetField("spellId") local aNumCraftable = private.sortSelf._numCraftableCache[a] local bNumCraftable = private.sortSelf._numCraftableCache[b] local aNumQueued = a:GetField("num") local bNumQueued = b:GetField("num") local aCanCraftAll = aNumCraftable >= aNumQueued local bCanCraftAll = bNumCraftable >= bNumQueued if aCanCraftAll and not bCanCraftAll then return true elseif not aCanCraftAll and bCanCraftAll then return false end local aCanCraftSome = aNumCraftable > 0 local bCanCraftSome = bNumCraftable > 0 if aCanCraftSome and not bCanCraftSome then return true elseif not aCanCraftSome and bCanCraftSome then return false end local aProfit = private.sortProfitCache[aSpellId] local bProfit = private.sortProfitCache[bSpellId] if aProfit and not bProfit then return true elseif not aProfit and bProfit then return false end if aProfit ~= bProfit then return aProfit > bProfit end return aSpellId < bSpellId end function private.QueryUpdateCallback(_, _, self) self:UpdateData(true) end