499 lines
17 KiB
Lua
499 lines
17 KiB
Lua
|
-- ------------------------------------------------------------------------------ --
|
||
|
-- 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
|