-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- --- ProfessionScrollingTable UI Element Class. -- This is used to display the crafts within the currently-selected profession in the CraftingUI. It is a subclass of -- the @{ScrollingTable} class. -- @classmod ProfessionScrollingTable 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 Log = TSM.Include("Util.Log") local ScriptWrapper = TSM.Include("Util.ScriptWrapper") local Event = TSM.Include("Util.Event") local ItemInfo = TSM.Include("Service.ItemInfo") local Tooltip = TSM.Include("UI.Tooltip") local ProfessionScrollingTable = TSM.Include("LibTSMClass").DefineClass("ProfessionScrollingTable", TSM.UI.ScrollingTable) local UIElements = TSM.Include("UI.UIElements") UIElements.Register(ProfessionScrollingTable) TSM.UI.ProfessionScrollingTable = ProfessionScrollingTable local private = { activeElements = {}, categoryInfoCache = { parent = {}, numIndents = {}, name = {}, currentSkillLevel = {}, maxSkillLevel = {}, }, } -- ============================================================================ -- Public Class Methods -- ============================================================================ function ProfessionScrollingTable.__init(self) self.__super:__init() self._query = nil self._isSpellId = {} self._favoritesContextTable = nil end function ProfessionScrollingTable.Acquire(self) self.__super:Acquire() self:GetScrollingTableInfo() :SetMenuInfo(private.MenuIterator, private.MenuClickHandler) :NewColumn("name") :SetTitle(L["Recipe Name"]) :SetFont("ITEM_BODY3") :SetJustifyH("LEFT") :SetTextFunction(private.GetNameCellText) :SetExpanderStateFunction(private.GetExpanderState) :SetActionIconInfo(1, 14, private.GetFavoriteIcon, true) :SetActionIconClickHandler(private.OnFavoriteIconClick) :DisableHiding() :Commit() :NewColumn("qty") :SetTitle(L["Craft"]) :SetFont("BODY_BODY3_MEDIUM") :SetJustifyH("CENTER") :SetTextFunction(private.GetQtyCellText) :Commit() if not TSM.IsWowClassic() then self:GetScrollingTableInfo() :NewColumn("rank") :SetTitle(RANK) :SetFont("BODY_BODY3_MEDIUM") :SetJustifyH("CENTER") :SetTextFunction(private.GetRankCellText) :Commit() end self:GetScrollingTableInfo() :NewColumn("craftingCost") :SetTitle(L["Crafting Cost"]) :SetFont("TABLE_TABLE1") :SetJustifyH("RIGHT") :SetTextFunction(private.GetCraftingCostCellText) :Commit() :NewColumn("itemValue") :SetTitle(L["Item Value"]) :SetFont("TABLE_TABLE1") :SetJustifyH("RIGHT") :SetTextFunction(private.GetItemValueCellIndex) :Commit() :NewColumn("profit") :SetTitle(L["Profit"]) :SetFont("TABLE_TABLE1") :SetJustifyH("RIGHT") :SetTextFunction(private.GetProfitCellText) :Commit() :NewColumn("profitPct") :SetTitle("%") :SetFont("TABLE_TABLE1") :SetJustifyH("RIGHT") :SetTextFunction(private.GetProfitPctCellText) :Commit() :NewColumn("saleRate") :SetTitleIcon("iconPack.14x14/SaleRate") :SetFont("TABLE_TABLE1") :SetJustifyH("RIGHT") :SetTextFunction(private.GetSaleRateCellText) :Commit() :Commit() if not next(private.activeElements) then Event.Register("CHAT_MSG_SKILL", private.OnChatMsgSkill) end private.activeElements[self] = true end function ProfessionScrollingTable.Release(self) private.activeElements[self] = nil if not next(private.activeElements) then Event.Unregister("CHAT_MSG_SKILL", private.OnChatMsgSkill) end if self._query then self._query:SetUpdateCallback() self._query = nil end wipe(self._isSpellId) self._favoritesContextTable = nil for _, row in ipairs(self._rows) do ScriptWrapper.Clear(row._frame, "OnDoubleClick") end self.__super:Release() end -- ============================================================================ -- Public Class Methods -- ============================================================================ --- Sets the @{DatabaseQuery} source for this table. -- This query is used to populate the entries in the profession scrolling table. -- @tparam ProfessionScrollingTable self The profession scrolling table object -- @tparam DatabaseQuery query The query object -- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table -- @treturn ProfessionScrollingTable The profession scrolling table object function ProfessionScrollingTable.SetQuery(self, query, redraw) if query == self._query and not redraw then return self end if self._query then self._query:SetUpdateCallback() end self._query = query self._query:SetUpdateCallback(private.QueryUpdateCallback, self) self:_ForceLastDataUpdate() self:UpdateData(redraw) return self end --- Sets the context table to use to store favorite craft information. -- @tparam ProfessionScrollingTable self The profession scrolling table object -- @tparam table tbl The context table -- @treturn ProfessionScrollingTable The profession scrolling table object function ProfessionScrollingTable.SetFavoritesContext(self, tbl) assert(type(tbl) == "table") self._favoritesContextTable = tbl return self end --- Sets the context table. -- @tparam ProfessionScrollingTable self The profession scrolling table object -- @tparam table tbl The context table -- @tparam table defaultTbl The default table (required fields: `colWidth`, `colHidden`, `collapsed`) -- @treturn ProfessionScrollingTable The profession scrolling table object function ProfessionScrollingTable.SetContextTable(self, tbl, defaultTbl) assert(type(defaultTbl.collapsed) == "table") tbl.collapsed = tbl.collapsed or CopyTable(defaultTbl.collapsed) self.__super:SetContextTable(tbl, defaultTbl) return self end function ProfessionScrollingTable.IsSpellIdVisible(self, spellId) if not self._isSpellId[spellId] then -- this spellId isn't included in the query return false end local categoryId = TSM.Crafting.ProfessionScanner.GetCategoryIdBySpellId(spellId) return not self:_IsCategoryHidden(categoryId) and not self._contextTable.collapsed[categoryId] end function ProfessionScrollingTable.Draw(self) if self._lastDataUpdate == nil then self:_IgnoreLastDataUpdate() end self.__super:Draw() end -- ============================================================================ -- Private Class Methods -- ============================================================================ function ProfessionScrollingTable._ToggleCollapsed(self, categoryId) self._contextTable.collapsed[categoryId] = not self._contextTable.collapsed[categoryId] or nil if self._selection and not self:IsSpellIdVisible(self._selection) then self:SetSelection(nil, true) end end function ProfessionScrollingTable._GetTableRow(self, isHeader) local row = self.__super:_GetTableRow(isHeader) if not isHeader then ScriptWrapper.Set(row._frame, "OnClick", private.RowOnClick, row) ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnClick, row) local rankBtn = row:_GetButton() rankBtn:SetAllPoints(row._texts.rank) ScriptWrapper.SetPropagate(rankBtn, "OnClick") ScriptWrapper.Set(rankBtn, "OnEnter", private.RankOnEnter, row) ScriptWrapper.Set(rankBtn, "OnLeave", private.RankOnLeave, row) row._buttons.rank = rankBtn end return row end function ProfessionScrollingTable._UpdateData(self) local currentCategoryPath = TempTable.Acquire() local foundSelection = false -- populate the data wipe(self._data) wipe(self._isSpellId) for _, spellId in self._query:Iterator() do if self._favoritesContextTable[spellId] then local categoryId = -1 if categoryId ~= currentCategoryPath[#currentCategoryPath] then -- this is a new category local newCategoryPath = TempTable.Acquire() tinsert(newCategoryPath, 1, categoryId) -- create new category headers if currentCategoryPath[1] ~= categoryId then if not self:_IsCategoryHidden(categoryId) then tinsert(self._data, categoryId) end end TempTable.Release(currentCategoryPath) currentCategoryPath = newCategoryPath end foundSelection = foundSelection or spellId == self:GetSelection() if not self._contextTable.collapsed[categoryId] and not self:_IsCategoryHidden(categoryId) then tinsert(self._data, spellId) self._isSpellId[spellId] = true end end end for _, spellId, categoryId in self._query:Iterator() do if not self._favoritesContextTable[spellId] then if categoryId ~= currentCategoryPath[#currentCategoryPath] then -- this is a new category local newCategoryPath = TempTable.Acquire() local currentCategoryId = categoryId while currentCategoryId do tinsert(newCategoryPath, 1, currentCategoryId) currentCategoryId = private.CategoryGetParentCategoryId(currentCategoryId) end -- create new category headers for i = 1, #newCategoryPath do local newCategoryId = newCategoryPath[i] if currentCategoryPath[i] ~= newCategoryId then if not self:_IsCategoryHidden(newCategoryId) then tinsert(self._data, newCategoryId) end end end TempTable.Release(currentCategoryPath) currentCategoryPath = newCategoryPath end foundSelection = foundSelection or spellId == self:GetSelection() if not self._contextTable.collapsed[categoryId] and not self:_IsCategoryHidden(categoryId) then tinsert(self._data, spellId) self._isSpellId[spellId] = true end end end TempTable.Release(currentCategoryPath) if not foundSelection then -- try to select the first visible spellId local newSelection = nil for _, data in ipairs(self._data) do if not newSelection and self._isSpellId[data] then newSelection = data end end self:SetSelection(newSelection, true) end end function ProfessionScrollingTable._IsCategoryHidden(self, categoryId) if private.IsFavoriteCategory(categoryId) then return false end local parent = private.CategoryGetParentCategoryId(categoryId) while parent do if self._contextTable.collapsed[parent] then return true end parent = private.CategoryGetParentCategoryId(parent) end return false end function ProfessionScrollingTable._SetRowData(self, row, data) local rank = self._isSpellId[data] and TSM.Crafting.ProfessionScanner.GetRankBySpellId(data) or -1 if rank == -1 then row._buttons.rank:Hide() else row._buttons.rank:Show() end self.__super:_SetRowData(row, data) end function ProfessionScrollingTable._ToggleSort(self, id) -- do nothing end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.PopulateCategoryInfoCache(categoryId) -- numIndents always gets set, so use that to know whether or not this category is already cached if not private.categoryInfoCache.numIndents[categoryId] then local name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel = TSM.Crafting.ProfessionUtil.GetCategoryInfo(categoryId) private.categoryInfoCache.name[categoryId] = name private.categoryInfoCache.numIndents[categoryId] = numIndents private.categoryInfoCache.parent[categoryId] = parentCategoryId private.categoryInfoCache.currentSkillLevel[categoryId] = currentSkillLevel private.categoryInfoCache.maxSkillLevel[categoryId] = maxSkillLevel end end function private.CategoryGetParentCategoryId(categoryId) private.PopulateCategoryInfoCache(categoryId) return private.categoryInfoCache.parent[categoryId] end function private.CategoryGetNumIndents(categoryId) private.PopulateCategoryInfoCache(categoryId) return private.categoryInfoCache.numIndents[categoryId] end function private.CategoryGetName(categoryId) private.PopulateCategoryInfoCache(categoryId) return private.categoryInfoCache.name[categoryId] end function private.CategoryGetSkillLevel(categoryId) private.PopulateCategoryInfoCache(categoryId) return private.categoryInfoCache.currentSkillLevel[categoryId], private.categoryInfoCache.maxSkillLevel[categoryId] end function private.IsFavoriteCategory(categoryId) return categoryId == -1 end function private.QueryUpdateCallback(_, _, self) self:_ForceLastDataUpdate() self:UpdateData(true) end function private.MenuIterator(self, prevIndex) if prevIndex == "CREATE_GROUPS" then -- we're done return else return "CREATE_GROUPS", L["Create Groups from Table"] end end function private.MenuClickHandler(self, index1, index2) if index1 == "CREATE_GROUPS" then assert(not index2) self:GetBaseElement():HideDialog() local numCreated, numAdded = 0, 0 for _, spellId in self._query:Iterator() do local itemString = TSM.Crafting.GetItemString(spellId) if itemString then local groupPath = private.GetCategoryGroupPath(TSM.Crafting.ProfessionScanner.GetCategoryIdBySpellId(spellId)) if not TSM.Groups.Exists(groupPath) then TSM.Groups.Create(groupPath) numCreated = numCreated + 1 end if not TSM.Groups.IsItemInGroup(itemString) and not ItemInfo.IsSoulbound(itemString) then TSM.Groups.SetItemGroup(itemString, groupPath) numAdded = numAdded + 1 end end end Log.PrintfUser(L["%d groups were created and %d items were added from the table."], numCreated, numAdded) else error("Unexpected index1: "..tostring(index1)) end end function private.GetCategoryGroupPath(categoryId) local parts = TempTable.Acquire() while categoryId do tinsert(parts, 1, private.categoryInfoCache.name[categoryId]) categoryId = private.categoryInfoCache.parent[categoryId] end tinsert(parts, 1, TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()) return TSM.Groups.Path.Join(TempTable.UnpackAndRelease(parts)) end function private.GetNameCellText(self, data) if self._isSpellId[data] then local name = TSM.Crafting.ProfessionScanner.GetNameBySpellId(data) local color = nil if TSM.Crafting.ProfessionUtil.IsGuildProfession() then color = Theme.GetProfessionDifficultyColor("easy") elseif TSM.Crafting.ProfessionUtil.IsNPCProfession() then color = Theme.GetProfessionDifficultyColor("nodifficulty") else local difficulty = TSM.Crafting.ProfessionScanner.GetDifficultyBySpellId(data) color = Theme.GetProfessionDifficultyColor(difficulty) end return color:ColorText(name) else -- this is a category local name = nil if private.IsFavoriteCategory(data) then name = L["Favorited Patterns"] else local currentSkillLevel, maxSkillLevel = private.CategoryGetSkillLevel(data) name = private.CategoryGetName(data) if name and currentSkillLevel and maxSkillLevel then name = name.." ("..currentSkillLevel.."/"..maxSkillLevel..")" end end if not name then -- happens if we're switching to another profession return "?" end if private.IsFavoriteCategory(data) or private.CategoryGetNumIndents(data) == 0 then return Theme.GetColor("INDICATOR"):ColorText(name) else return Theme.GetColor("INDICATOR_ALT"):ColorText(name) end end end function private.GetExpanderState(self, data) local indentLevel = 0 if self._isSpellId[data] then indentLevel = 2 elseif not private.IsFavoriteCategory(data) then indentLevel = private.CategoryGetNumIndents(data) * 2 end return not self._isSpellId[data], not self._contextTable.collapsed[data], -indentLevel end function private.GetFavoriteIcon(self, data, iconIndex, isMouseOver) if iconIndex == 1 then if not self._isSpellId[data] then return false, nil, true else return true, self._favoritesContextTable[data] and "iconPack.12x12/Star/Filled" or "iconPack.12x12/Star/Unfilled", true end else error("Invalid index: "..tostring(iconIndex)) end end function private.OnFavoriteIconClick(self, data, iconIndex) if iconIndex == 1 then if self._isSpellId[data] and private.IsPlayerProfession() then self._favoritesContextTable[data] = not self._favoritesContextTable[data] or nil self:_ForceLastDataUpdate() self:UpdateData(true) end else error("Invalid index: "..tostring(iconIndex)) end end function private.GetQtyCellText(self, data) if not self._isSpellId[data] then return "" end local num, numAll = TSM.Crafting.ProfessionUtil.GetNumCraftable(data) if num == numAll then if num > 0 then return Theme.GetFeedbackColor("GREEN"):ColorText(num) end return tostring(num) else if num > 0 then return Theme.GetFeedbackColor("GREEN"):ColorText(num.."-"..numAll) elseif numAll > 0 then return Theme.GetFeedbackColor("YELLOW"):ColorText(num.."-"..numAll) else return num.."-"..numAll end end end function private.GetRankCellText(self, data) local rank = self._isSpellId[data] and TSM.Crafting.ProfessionScanner.GetRankBySpellId(data) or -1 if rank == -1 then return "" end local filled = TSM.UI.TexturePacks.GetTextureLink("iconPack.14x14/Star/Filled") local unfilled = TSM.UI.TexturePacks.GetTextureLink("iconPack.14x14/Star/Unfilled") assert(rank >= 1 and rank <= 3) return strrep(filled, rank)..strrep(unfilled, 3 - rank) end function private.GetCraftingCostCellText(self, data) if not self._isSpellId[data] then return "" end local craftingCost = TSM.Crafting.Cost.GetCostsBySpellId(data) return craftingCost and Money.ToString(craftingCost) or "" end function private.GetItemValueCellIndex(self, data) if not self._isSpellId[data] then return "" end local _, craftedItemValue = TSM.Crafting.Cost.GetCostsBySpellId(data) return craftedItemValue and Money.ToString(craftedItemValue) or "" end function private.GetProfitCellText(self, data, currentTitleIndex) if not self._isSpellId[data] then return "" end local _, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(data) local color = profit and Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED") return profit and Money.ToString(profit, color:GetTextColorPrefix()) or "" end function private.GetProfitPctCellText(self, data, currentTitleIndex) if not self._isSpellId[data] then return "" end local craftingCost, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(data) local color = profit and Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED") return profit and color:ColorText(floor(profit * 100 / craftingCost).."%") or "" end function private.GetSaleRateCellText(self, data) return self._isSpellId[data] and TSM.Crafting.Cost.GetSaleRateBySpellId(data) or "" end -- ============================================================================ -- Local Script Handlers -- ============================================================================ function private.RowOnClick(row, mouseButton) local scrollingTable = row._scrollingTable local data = row:GetData() if mouseButton == "LeftButton" then if scrollingTable._isSpellId[data] then scrollingTable:SetSelection(data) else scrollingTable:_ToggleCollapsed(data) end scrollingTable:UpdateData(true) if scrollingTable._isSpellId[data] then row:SetHighlightState("selectedHover") else row:SetHighlightState("hover") end end end function private.RankOnEnter(row) local data = row:GetData() local rank = row._scrollingTable._isSpellId[data] and TSM.Crafting.ProfessionScanner.GetRankBySpellId(data) or -1 if rank > 0 and not TSM.IsWowClassic() then assert(not Tooltip.IsVisible()) GameTooltip:SetOwner(row._buttons.rank, "ANCHOR_PRESERVE") GameTooltip:ClearAllPoints() GameTooltip:SetPoint("LEFT", row._buttons.rank, "RIGHT") GameTooltip:SetRecipeRankInfo(data, rank) GameTooltip:Show() end row._frame:GetScript("OnEnter")(row._frame) end function private.RankOnLeave(row) Tooltip.Hide() row._frame:GetScript("OnLeave")(row._frame) end function private.IsPlayerProfession() return not (TSM.Crafting.ProfessionUtil.IsNPCProfession() or TSM.Crafting.ProfessionUtil.IsLinkedProfession() or TSM.Crafting.ProfessionUtil.IsGuildProfession()) end function private.OnChatMsgSkill(_, msg) if not strmatch(msg, TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()) then return end for self in pairs(private.activeElements) do wipe(private.categoryInfoCache.numIndents) self:_ForceLastDataUpdate() self:_UpdateData(true) end end